mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
feat: row document (#2792)
* chore: create orphan view handler * feat: save icon url and cover url in view * feat: implement emoji picker UI * chore: config ui * chore: config ui again * chore: replace RowPB with RowMetaPB to exposing more row information * fix: compile error * feat: show emoji in row * chore: update * test: insert emoji test * test: add update emoji test * test: add remove emoji test * test: add create field tests * test: add create row and delete row integration tests * test: add create row from row menu * test: document in row detail page * test: delete, duplicate row in row detail page * test: check the row count displayed in grid page * test: rename existing field in grid page * test: update field type of exisiting field in grid page * test: delete field test * test: add duplicate field test * test: add hide field test * test: add edit text cell test * test: add insert text to text cell test * test: add edit number cell test * test: add edit multiple number cells * test: add edit checkbox cell test * feat: integrate editor into database row * test: add edit create time and last edit time cell test * test: add edit date cell by selecting a date test * chore: remove unused code * chore: update checklist bg color * test: add update database layout test --------- Co-authored-by: Lucas.Xu <lucas.xu@appflowy.io>
This commit is contained in:
@ -1,5 +1,6 @@
|
||||
import 'dart:async';
|
||||
import 'package:appflowy/plugins/database_view/application/field/field_listener.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/row/row_meta_listener.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/row/row_service.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';
|
||||
@ -22,39 +23,45 @@ import 'cell_service.dart';
|
||||
///
|
||||
// ignore: must_be_immutable
|
||||
class CellController<T, D> extends Equatable {
|
||||
final DatabaseCellContext cellContext;
|
||||
DatabaseCellContext _cellContext;
|
||||
final CellCache _cellCache;
|
||||
final CellCacheKey _cacheKey;
|
||||
final FieldBackendService _fieldBackendSvc;
|
||||
final SingleFieldListener _fieldListener;
|
||||
final CellDataLoader<T> _cellDataLoader;
|
||||
final CellDataPersistence<D> _cellDataPersistence;
|
||||
|
||||
CellListener? _cellListener;
|
||||
RowMetaListener? _rowMetaListener;
|
||||
SingleFieldListener? _fieldListener;
|
||||
CellDataNotifier<T?>? _cellDataNotifier;
|
||||
|
||||
VoidCallback? _onCellFieldChanged;
|
||||
VoidCallback? _onRowMetaChanged;
|
||||
Timer? _loadDataOperation;
|
||||
Timer? _saveDataOperation;
|
||||
|
||||
String get viewId => cellContext.viewId;
|
||||
String get viewId => _cellContext.viewId;
|
||||
|
||||
RowId get rowId => cellContext.rowId;
|
||||
RowId get rowId => _cellContext.rowId;
|
||||
|
||||
String get fieldId => cellContext.fieldInfo.id;
|
||||
String get fieldId => _cellContext.fieldInfo.id;
|
||||
|
||||
FieldInfo get fieldInfo => cellContext.fieldInfo;
|
||||
FieldInfo get fieldInfo => _cellContext.fieldInfo;
|
||||
|
||||
FieldType get fieldType => cellContext.fieldInfo.fieldType;
|
||||
FieldType get fieldType => _cellContext.fieldInfo.fieldType;
|
||||
|
||||
String? get emoji => _cellContext.emoji;
|
||||
|
||||
CellController({
|
||||
required this.cellContext,
|
||||
required DatabaseCellContext cellContext,
|
||||
required CellCache cellCache,
|
||||
required CellDataLoader<T> cellDataLoader,
|
||||
required CellDataPersistence<D> cellDataPersistence,
|
||||
}) : _cellCache = cellCache,
|
||||
}) : _cellContext = cellContext,
|
||||
_cellCache = cellCache,
|
||||
_cellDataLoader = cellDataLoader,
|
||||
_cellDataPersistence = cellDataPersistence,
|
||||
_rowMetaListener = RowMetaListener(cellContext.rowId),
|
||||
_fieldListener = SingleFieldListener(fieldId: cellContext.fieldId),
|
||||
_fieldBackendSvc = FieldBackendService(
|
||||
viewId: cellContext.viewId,
|
||||
@ -84,20 +91,22 @@ class CellController<T, D> extends Equatable {
|
||||
);
|
||||
|
||||
/// 2.Listen on the field event and load the cell data if needed.
|
||||
_fieldListener.start(
|
||||
onFieldChanged: (result) {
|
||||
result.fold(
|
||||
(fieldPB) {
|
||||
/// reloadOnFieldChanged should be true if you need to load the data when the corresponding field is changed
|
||||
/// For example:
|
||||
/// ¥12 -> $12
|
||||
if (_cellDataLoader.reloadOnFieldChanged) {
|
||||
_loadData();
|
||||
}
|
||||
_onCellFieldChanged?.call();
|
||||
},
|
||||
(err) => Log.error(err),
|
||||
);
|
||||
_fieldListener?.start(
|
||||
onFieldChanged: (fieldPB) {
|
||||
/// reloadOnFieldChanged should be true if you need to load the data when the corresponding field is changed
|
||||
/// For example:
|
||||
/// ¥12 -> $12
|
||||
if (_cellDataLoader.reloadOnFieldChanged) {
|
||||
_loadData();
|
||||
}
|
||||
_onCellFieldChanged?.call();
|
||||
},
|
||||
);
|
||||
|
||||
_rowMetaListener?.start(
|
||||
callback: (newRowMeta) {
|
||||
_cellContext = _cellContext.copyWith(rowMeta: newRowMeta);
|
||||
_onRowMetaChanged?.call();
|
||||
},
|
||||
);
|
||||
}
|
||||
@ -105,9 +114,11 @@ class CellController<T, D> extends Equatable {
|
||||
/// Listen on the cell content or field changes
|
||||
VoidCallback? startListening({
|
||||
required void Function(T?) onCellChanged,
|
||||
VoidCallback? onRowMetaChanged,
|
||||
VoidCallback? onCellFieldChanged,
|
||||
}) {
|
||||
_onCellFieldChanged = onCellFieldChanged;
|
||||
_onRowMetaChanged = onRowMetaChanged;
|
||||
|
||||
/// Notify the listener, the cell data was changed.
|
||||
onCellChangedFn() => onCellChanged(_cellDataNotifier?.value);
|
||||
@ -186,18 +197,26 @@ class CellController<T, D> extends Equatable {
|
||||
}
|
||||
|
||||
Future<void> dispose() async {
|
||||
await _rowMetaListener?.stop();
|
||||
_rowMetaListener = null;
|
||||
|
||||
await _cellListener?.stop();
|
||||
_cellListener = null;
|
||||
|
||||
await _fieldListener?.stop();
|
||||
_fieldListener = null;
|
||||
|
||||
_loadDataOperation?.cancel();
|
||||
_saveDataOperation?.cancel();
|
||||
_cellDataNotifier?.dispose();
|
||||
await _fieldListener.stop();
|
||||
_cellDataNotifier = null;
|
||||
_onRowMetaChanged = null;
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object> get props => [
|
||||
_cellCache.get(_cacheKey) ?? "",
|
||||
cellContext.rowId + cellContext.fieldInfo.id
|
||||
_cellContext.rowId + _cellContext.fieldInfo.id
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -2,6 +2,7 @@ import 'dart:async';
|
||||
import 'dart:collection';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/checklist_entities.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/row_entities.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/url_entities.pb.dart';
|
||||
import 'package:dartz/dartz.dart';
|
||||
@ -52,18 +53,23 @@ class CellBackendService {
|
||||
class DatabaseCellContext with _$DatabaseCellContext {
|
||||
const factory DatabaseCellContext({
|
||||
required String viewId,
|
||||
required RowId rowId,
|
||||
required RowMetaPB rowMeta,
|
||||
required FieldInfo fieldInfo,
|
||||
}) = _DatabaseCellContext;
|
||||
|
||||
// ignore: unused_element
|
||||
const DatabaseCellContext._();
|
||||
|
||||
String get rowId => rowMeta.id;
|
||||
|
||||
String get fieldId => fieldInfo.id;
|
||||
|
||||
FieldType get fieldType => fieldInfo.fieldType;
|
||||
|
||||
ValueKey key() {
|
||||
return ValueKey("$rowId$fieldId${fieldInfo.fieldType}");
|
||||
return ValueKey("${rowMeta.id}$fieldId${fieldInfo.fieldType}");
|
||||
}
|
||||
|
||||
/// Only the primary field can have an emoji.
|
||||
String? get emoji => fieldInfo.isPrimary ? rowMeta.icon : null;
|
||||
}
|
||||
|
@ -1,4 +1,3 @@
|
||||
import 'package:appflowy/plugins/database_view/application/cell/cell_service.dart';
|
||||
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/cell_entities.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/checklist_entities.pb.dart';
|
||||
@ -7,17 +6,23 @@ import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:dartz/dartz.dart';
|
||||
|
||||
class ChecklistCellBackendService {
|
||||
final DatabaseCellContext cellContext;
|
||||
final String viewId;
|
||||
final String fieldId;
|
||||
final String rowId;
|
||||
|
||||
ChecklistCellBackendService({required this.cellContext});
|
||||
ChecklistCellBackendService({
|
||||
required this.viewId,
|
||||
required this.fieldId,
|
||||
required this.rowId,
|
||||
});
|
||||
|
||||
Future<Either<Unit, FlowyError>> create({
|
||||
required String name,
|
||||
}) {
|
||||
final payload = ChecklistCellDataChangesetPB.create()
|
||||
..viewId = cellContext.viewId
|
||||
..fieldId = cellContext.fieldInfo.id
|
||||
..rowId = cellContext.rowId
|
||||
..viewId = viewId
|
||||
..fieldId = fieldId
|
||||
..rowId = rowId
|
||||
..insertOptions.add(name);
|
||||
|
||||
return DatabaseEventUpdateChecklistCell(payload).send();
|
||||
@ -27,9 +32,9 @@ class ChecklistCellBackendService {
|
||||
required List<String> optionIds,
|
||||
}) {
|
||||
final payload = ChecklistCellDataChangesetPB.create()
|
||||
..viewId = cellContext.viewId
|
||||
..fieldId = cellContext.fieldInfo.id
|
||||
..rowId = cellContext.rowId
|
||||
..viewId = viewId
|
||||
..fieldId = fieldId
|
||||
..rowId = rowId
|
||||
..deleteOptionIds.addAll(optionIds);
|
||||
|
||||
return DatabaseEventUpdateChecklistCell(payload).send();
|
||||
@ -39,9 +44,9 @@ class ChecklistCellBackendService {
|
||||
required String optionId,
|
||||
}) {
|
||||
final payload = ChecklistCellDataChangesetPB.create()
|
||||
..viewId = cellContext.viewId
|
||||
..fieldId = cellContext.fieldInfo.id
|
||||
..rowId = cellContext.rowId
|
||||
..viewId = viewId
|
||||
..fieldId = fieldId
|
||||
..rowId = rowId
|
||||
..selectedOptionIds.add(optionId);
|
||||
|
||||
return DatabaseEventUpdateChecklistCell(payload).send();
|
||||
@ -51,9 +56,9 @@ class ChecklistCellBackendService {
|
||||
required SelectOptionPB option,
|
||||
}) {
|
||||
final payload = ChecklistCellDataChangesetPB.create()
|
||||
..viewId = cellContext.viewId
|
||||
..fieldId = cellContext.fieldInfo.id
|
||||
..rowId = cellContext.rowId
|
||||
..viewId = viewId
|
||||
..fieldId = fieldId
|
||||
..rowId = rowId
|
||||
..updateOptions.add(option);
|
||||
|
||||
return DatabaseEventUpdateChecklistCell(payload).send();
|
||||
@ -61,10 +66,9 @@ class ChecklistCellBackendService {
|
||||
|
||||
Future<Either<ChecklistCellDataPB, FlowyError>> getCellData() {
|
||||
final payload = CellIdPB.create()
|
||||
..fieldId = cellContext.fieldInfo.id
|
||||
..viewId = cellContext.viewId
|
||||
..rowId = cellContext.rowId
|
||||
..rowId = cellContext.rowId;
|
||||
..viewId = viewId
|
||||
..fieldId = fieldId
|
||||
..rowId = rowId;
|
||||
|
||||
return DatabaseEventGetChecklistCellData(payload).send();
|
||||
}
|
||||
|
@ -1,6 +1,4 @@
|
||||
import 'package:appflowy/plugins/database_view/application/cell/cell_service.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_service.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/row/row_service.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart';
|
||||
import 'package:dartz/dartz.dart';
|
||||
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
||||
@ -8,12 +6,15 @@ import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/cell_entities.pb.dart';
|
||||
|
||||
class SelectOptionCellBackendService {
|
||||
final DatabaseCellContext cellContext;
|
||||
SelectOptionCellBackendService({required this.cellContext});
|
||||
final String viewId;
|
||||
final String fieldId;
|
||||
final String rowId;
|
||||
|
||||
String get viewId => cellContext.viewId;
|
||||
String get fieldId => cellContext.fieldInfo.id;
|
||||
RowId get rowId => cellContext.rowId;
|
||||
SelectOptionCellBackendService({
|
||||
required this.viewId,
|
||||
required this.fieldId,
|
||||
required this.rowId,
|
||||
});
|
||||
|
||||
Future<Either<Unit, FlowyError>> create({
|
||||
required String name,
|
||||
|
@ -160,7 +160,7 @@ class DatabaseController {
|
||||
});
|
||||
}
|
||||
|
||||
Future<Either<RowPB, FlowyError>> createRow({
|
||||
Future<Either<RowMetaPB, FlowyError>> createRow({
|
||||
RowId? startRowId,
|
||||
String? groupId,
|
||||
void Function(RowDataBuilder builder)? withCells,
|
||||
@ -181,9 +181,9 @@ class DatabaseController {
|
||||
}
|
||||
|
||||
Future<Either<Unit, FlowyError>> moveGroupRow({
|
||||
required RowPB fromRow,
|
||||
required RowMetaPB fromRow,
|
||||
required String groupId,
|
||||
RowPB? toRow,
|
||||
RowMetaPB? toRow,
|
||||
}) {
|
||||
return _databaseViewBackendSvc.moveGroupRow(
|
||||
fromRowId: fromRow.id,
|
||||
@ -193,12 +193,12 @@ class DatabaseController {
|
||||
}
|
||||
|
||||
Future<Either<Unit, FlowyError>> moveRow({
|
||||
required RowPB fromRow,
|
||||
required RowPB toRow,
|
||||
required String fromRowId,
|
||||
required String toRowId,
|
||||
}) {
|
||||
return _databaseViewBackendSvc.moveRow(
|
||||
fromRowId: fromRow.id,
|
||||
toRowId: toRow.id,
|
||||
fromRowId: fromRowId,
|
||||
toRowId: toRowId,
|
||||
);
|
||||
}
|
||||
|
||||
@ -269,8 +269,8 @@ class DatabaseController {
|
||||
onRowsDeleted: (ids) {
|
||||
_databaseCallbacks?.onRowsDeleted?.call(ids);
|
||||
},
|
||||
onRowsUpdated: (ids) {
|
||||
_databaseCallbacks?.onRowsUpdated?.call(ids);
|
||||
onRowsUpdated: (ids, reason) {
|
||||
_databaseCallbacks?.onRowsUpdated?.call(ids, reason);
|
||||
},
|
||||
onRowsCreated: (ids) {
|
||||
_databaseCallbacks?.onRowsCreated?.call(ids);
|
||||
|
@ -30,7 +30,7 @@ class DatabaseViewBackendService {
|
||||
return DatabaseEventGetDatabase(payload).send();
|
||||
}
|
||||
|
||||
Future<Either<RowPB, FlowyError>> createRow({
|
||||
Future<Either<RowMetaPB, FlowyError>> createRow({
|
||||
RowId? startRowId,
|
||||
String? groupId,
|
||||
Map<String, String>? cellDataByFieldId,
|
||||
|
@ -13,7 +13,10 @@ typedef OnFiltersChanged = void Function(List<FilterInfo>);
|
||||
typedef OnDatabaseChanged = void Function(DatabasePB);
|
||||
|
||||
typedef OnRowsCreated = void Function(List<RowId> ids);
|
||||
typedef OnRowsUpdated = void Function(List<RowId> ids);
|
||||
typedef OnRowsUpdated = void Function(
|
||||
List<RowId> ids,
|
||||
RowsChangedReason reason,
|
||||
);
|
||||
typedef OnRowsDeleted = void Function(List<RowId> ids);
|
||||
typedef OnNumOfRowsChanged = void Function(
|
||||
UnmodifiableListView<RowInfo> rows,
|
||||
|
@ -1,4 +1,3 @@
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
@ -52,14 +51,11 @@ class FieldCellBloc extends Bloc<FieldCellEvent, FieldCellState> {
|
||||
|
||||
void _startListening() {
|
||||
_fieldListener.start(
|
||||
onFieldChanged: (result) {
|
||||
onFieldChanged: (updatedField) {
|
||||
if (isClosed) {
|
||||
return;
|
||||
}
|
||||
result.fold(
|
||||
(field) => add(FieldCellEvent.didReceiveFieldUpdate(field)),
|
||||
(err) => Log.error(err),
|
||||
);
|
||||
add(FieldCellEvent.didReceiveFieldUpdate(updatedField));
|
||||
},
|
||||
);
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
import 'package:appflowy/core/notification/grid_notification.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:dartz/dartz.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/notification.pb.dart';
|
||||
@ -7,12 +8,11 @@ import 'dart:async';
|
||||
import 'dart:typed_data';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
|
||||
|
||||
typedef UpdateFieldNotifiedValue = Either<FieldPB, FlowyError>;
|
||||
typedef UpdateFieldNotifiedValue = FieldPB;
|
||||
|
||||
class SingleFieldListener {
|
||||
final String fieldId;
|
||||
PublishNotifier<UpdateFieldNotifiedValue>? _updateFieldNotifier =
|
||||
PublishNotifier();
|
||||
void Function(UpdateFieldNotifiedValue)? _updateFieldNotifier;
|
||||
DatabaseNotificationListener? _listener;
|
||||
|
||||
SingleFieldListener({required this.fieldId});
|
||||
@ -20,7 +20,7 @@ class SingleFieldListener {
|
||||
void start({
|
||||
required void Function(UpdateFieldNotifiedValue) onFieldChanged,
|
||||
}) {
|
||||
_updateFieldNotifier?.addPublishListener(onFieldChanged);
|
||||
_updateFieldNotifier = onFieldChanged;
|
||||
_listener = DatabaseNotificationListener(
|
||||
objectId: fieldId,
|
||||
handler: _handler,
|
||||
@ -34,9 +34,8 @@ class SingleFieldListener {
|
||||
switch (ty) {
|
||||
case DatabaseNotification.DidUpdateField:
|
||||
result.fold(
|
||||
(payload) =>
|
||||
_updateFieldNotifier?.value = left(FieldPB.fromBuffer(payload)),
|
||||
(error) => _updateFieldNotifier?.value = right(error),
|
||||
(payload) => _updateFieldNotifier?.call(FieldPB.fromBuffer(payload)),
|
||||
(error) => Log.error(error),
|
||||
);
|
||||
break;
|
||||
default:
|
||||
@ -46,7 +45,6 @@ class SingleFieldListener {
|
||||
|
||||
Future<void> stop() async {
|
||||
await _listener?.stop();
|
||||
_updateFieldNotifier?.dispose();
|
||||
_updateFieldNotifier = null;
|
||||
}
|
||||
}
|
||||
|
@ -99,6 +99,14 @@ class FieldBackendService {
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/// Returns the primary field of the view.
|
||||
static Future<Either<FieldPB, FlowyError>> getPrimaryField({
|
||||
required String viewId,
|
||||
}) {
|
||||
final payload = DatabaseViewIdPB.create()..value = viewId;
|
||||
return DatabaseEventGetPrimaryField(payload).send();
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
|
@ -0,0 +1,163 @@
|
||||
import 'package:appflowy/plugins/database_view/application/field/field_listener.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/field/field_service.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/row/row_service.dart';
|
||||
import 'package:appflowy/workspace/application/view/prelude.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/row_entities.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
import 'row_meta_listener.dart';
|
||||
|
||||
part 'row_banner_bloc.freezed.dart';
|
||||
|
||||
class RowBannerBloc extends Bloc<RowBannerEvent, RowBannerState> {
|
||||
final String viewId;
|
||||
final RowBackendService _rowBackendSvc;
|
||||
final RowMetaListener _metaListener;
|
||||
SingleFieldListener? _fieldListener;
|
||||
|
||||
RowBannerBloc({
|
||||
required this.viewId,
|
||||
required RowMetaPB rowMeta,
|
||||
}) : _rowBackendSvc = RowBackendService(viewId: viewId),
|
||||
_metaListener = RowMetaListener(rowMeta.id),
|
||||
super(RowBannerState.initial(rowMeta)) {
|
||||
on<RowBannerEvent>(
|
||||
(event, emit) async {
|
||||
event.when(
|
||||
initial: () async {
|
||||
_loadPrimaryField();
|
||||
await _listenRowMeteChanged();
|
||||
},
|
||||
didReceiveRowMeta: (RowMetaPB rowMeta) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
rowMeta: rowMeta,
|
||||
),
|
||||
);
|
||||
},
|
||||
setCover: (String coverURL) {
|
||||
_updateMeta(coverURL: coverURL);
|
||||
},
|
||||
setIcon: (String iconURL) {
|
||||
_updateMeta(iconURL: iconURL);
|
||||
},
|
||||
didReceiveFieldUpdate: (updatedField) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
primaryField: updatedField,
|
||||
loadingState: const LoadingState.finish(),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() async {
|
||||
await _metaListener.stop();
|
||||
await _fieldListener?.stop();
|
||||
_fieldListener = null;
|
||||
|
||||
return super.close();
|
||||
}
|
||||
|
||||
Future<void> _loadPrimaryField() async {
|
||||
final fieldOrError =
|
||||
await FieldBackendService.getPrimaryField(viewId: viewId);
|
||||
fieldOrError.fold(
|
||||
(primaryField) {
|
||||
if (!isClosed) {
|
||||
_fieldListener = SingleFieldListener(fieldId: primaryField.id);
|
||||
_fieldListener?.start(
|
||||
onFieldChanged: (updatedField) {
|
||||
if (!isClosed) {
|
||||
add(RowBannerEvent.didReceiveFieldUpdate(updatedField));
|
||||
}
|
||||
},
|
||||
);
|
||||
add(RowBannerEvent.didReceiveFieldUpdate(primaryField));
|
||||
}
|
||||
},
|
||||
(r) => Log.error(r),
|
||||
);
|
||||
}
|
||||
|
||||
/// Listen the changes of the row meta and then update the banner
|
||||
Future<void> _listenRowMeteChanged() async {
|
||||
_metaListener.start(
|
||||
callback: (rowMeta) {
|
||||
add(RowBannerEvent.didReceiveRowMeta(rowMeta));
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Update the meta of the row and the view
|
||||
Future<void> _updateMeta({
|
||||
String? iconURL,
|
||||
String? coverURL,
|
||||
}) async {
|
||||
// Most of the time, the result is success, so we don't need to handle it.
|
||||
await _rowBackendSvc
|
||||
.updateMeta(
|
||||
iconURL: iconURL,
|
||||
coverURL: coverURL,
|
||||
rowId: state.rowMeta.id,
|
||||
)
|
||||
.then((result) {
|
||||
result.fold(
|
||||
(l) => null,
|
||||
(err) => Log.error(err),
|
||||
);
|
||||
});
|
||||
|
||||
// Set the icon and cover of the view
|
||||
ViewBackendService.updateView(
|
||||
viewId: viewId,
|
||||
iconURL: iconURL,
|
||||
coverURL: coverURL,
|
||||
).then((result) {
|
||||
result.fold(
|
||||
(l) => null,
|
||||
(err) => Log.error(err),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
class RowBannerEvent with _$RowBannerEvent {
|
||||
const factory RowBannerEvent.initial() = _Initial;
|
||||
const factory RowBannerEvent.didReceiveRowMeta(RowMetaPB rowMeta) =
|
||||
_DidReceiveRowMeta;
|
||||
const factory RowBannerEvent.didReceiveFieldUpdate(FieldPB field) =
|
||||
_DidReceiveFieldUdate;
|
||||
const factory RowBannerEvent.setIcon(String iconURL) = _SetIcon;
|
||||
const factory RowBannerEvent.setCover(String coverURL) = _SetCover;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class RowBannerState with _$RowBannerState {
|
||||
const factory RowBannerState({
|
||||
ViewPB? view,
|
||||
FieldPB? primaryField,
|
||||
required RowMetaPB rowMeta,
|
||||
required LoadingState loadingState,
|
||||
}) = _RowBannerState;
|
||||
|
||||
factory RowBannerState.initial(RowMetaPB rowMetaPB) => RowBannerState(
|
||||
rowMeta: rowMetaPB,
|
||||
loadingState: const LoadingState.loading(),
|
||||
);
|
||||
}
|
||||
|
||||
@freezed
|
||||
class LoadingState with _$LoadingState {
|
||||
const factory LoadingState.loading() = _Loading;
|
||||
const factory LoadingState.finish() = _Finish;
|
||||
}
|
@ -38,17 +38,21 @@ class RowCache {
|
||||
final RowCacheDelegate _delegate;
|
||||
final RowChangesetNotifier _rowChangeReasonNotifier;
|
||||
|
||||
/// Returns a unmodifiable list of RowInfo
|
||||
UnmodifiableListView<RowInfo> get rowInfos {
|
||||
final visibleRows = [..._rowList.rows];
|
||||
return UnmodifiableListView(visibleRows);
|
||||
}
|
||||
|
||||
/// Returns a unmodifiable map of rowId to RowInfo
|
||||
UnmodifiableMapView<RowId, RowInfo> get rowByRowId {
|
||||
return UnmodifiableMapView(_rowList.rowInfoByRowId);
|
||||
}
|
||||
|
||||
CellCache get cellCache => _cellCache;
|
||||
|
||||
RowsChangedReason get changeReason => _rowChangeReasonNotifier.reason;
|
||||
|
||||
RowCache({
|
||||
required this.viewId,
|
||||
required RowFieldsDelegate fieldsDelegate,
|
||||
@ -70,7 +74,7 @@ class RowCache {
|
||||
return _rowList.get(rowId);
|
||||
}
|
||||
|
||||
void setInitialRows(List<RowPB> rows) {
|
||||
void setInitialRows(List<RowMetaPB> rows) {
|
||||
for (final row in rows) {
|
||||
final rowInfo = buildGridRow(row);
|
||||
_rowList.add(rowInfo);
|
||||
@ -128,7 +132,7 @@ class RowCache {
|
||||
void _insertRows(List<InsertedRowPB> insertRows) {
|
||||
for (final insertedRow in insertRows) {
|
||||
final insertedIndex =
|
||||
_rowList.insert(insertedRow.index, buildGridRow(insertedRow.row));
|
||||
_rowList.insert(insertedRow.index, buildGridRow(insertedRow.rowMeta));
|
||||
if (insertedIndex != null) {
|
||||
_rowChangeReasonNotifier
|
||||
.receive(RowsChangedReason.insert(insertedIndex));
|
||||
@ -138,20 +142,23 @@ class RowCache {
|
||||
|
||||
void _updateRows(List<UpdatedRowPB> updatedRows) {
|
||||
if (updatedRows.isEmpty) return;
|
||||
final List<RowPB> rowPBs = [];
|
||||
final List<RowMetaPB> updatedList = [];
|
||||
for (final updatedRow in updatedRows) {
|
||||
for (final fieldId in updatedRow.fieldIds) {
|
||||
final key = CellCacheKey(
|
||||
fieldId: fieldId,
|
||||
rowId: updatedRow.row.id,
|
||||
rowId: updatedRow.rowId,
|
||||
);
|
||||
_cellCache.remove(key);
|
||||
}
|
||||
rowPBs.add(updatedRow.row);
|
||||
if (updatedRow.hasRowMeta()) {
|
||||
updatedList.add(updatedRow.rowMeta);
|
||||
}
|
||||
}
|
||||
|
||||
final updatedIndexs =
|
||||
_rowList.updateRows(rowPBs, (rowPB) => buildGridRow(rowPB));
|
||||
_rowList.updateRows(updatedList, (rowId) => buildGridRow(rowId));
|
||||
|
||||
if (updatedIndexs.isNotEmpty) {
|
||||
_rowChangeReasonNotifier.receive(RowsChangedReason.update(updatedIndexs));
|
||||
}
|
||||
@ -169,7 +176,7 @@ class RowCache {
|
||||
void _showRows(List<InsertedRowPB> visibleRows) {
|
||||
for (final insertedRow in visibleRows) {
|
||||
final insertedIndex =
|
||||
_rowList.insert(insertedRow.index, buildGridRow(insertedRow.row));
|
||||
_rowList.insert(insertedRow.index, buildGridRow(insertedRow.rowMeta));
|
||||
if (insertedIndex != null) {
|
||||
_rowChangeReasonNotifier
|
||||
.receive(RowsChangedReason.insert(insertedIndex));
|
||||
@ -197,8 +204,9 @@ class RowCache {
|
||||
if (onCellUpdated != null) {
|
||||
final rowInfo = _rowList.get(rowId);
|
||||
if (rowInfo != null) {
|
||||
final CellContextByFieldId cellDataMap =
|
||||
_makeGridCells(rowId, rowInfo.rowPB);
|
||||
final CellContextByFieldId cellDataMap = _makeGridCells(
|
||||
rowInfo.rowMeta,
|
||||
);
|
||||
onCellUpdated(cellDataMap, _rowChangeReasonNotifier.reason);
|
||||
}
|
||||
}
|
||||
@ -220,12 +228,12 @@ class RowCache {
|
||||
_rowChangeReasonNotifier.removeListener(callback);
|
||||
}
|
||||
|
||||
CellContextByFieldId loadGridCells(RowId rowId) {
|
||||
final RowPB? data = _rowList.get(rowId)?.rowPB;
|
||||
if (data == null) {
|
||||
_loadRow(rowId);
|
||||
CellContextByFieldId loadGridCells(RowMetaPB rowMeta) {
|
||||
final rowInfo = _rowList.get(rowMeta.id);
|
||||
if (rowInfo == null) {
|
||||
_loadRow(rowMeta.id);
|
||||
}
|
||||
return _makeGridCells(rowId, data);
|
||||
return _makeGridCells(rowMeta);
|
||||
}
|
||||
|
||||
Future<void> _loadRow(RowId rowId) async {
|
||||
@ -233,57 +241,51 @@ class RowCache {
|
||||
..viewId = viewId
|
||||
..rowId = rowId;
|
||||
|
||||
final result = await DatabaseEventGetRow(payload).send();
|
||||
final result = await DatabaseEventGetRowMeta(payload).send();
|
||||
result.fold(
|
||||
(optionRow) => _refreshRow(optionRow),
|
||||
(rowMetaPB) {
|
||||
final rowInfo = _rowList.get(rowMetaPB.id);
|
||||
final rowIndex = _rowList.indexOfRow(rowMetaPB.id);
|
||||
if (rowInfo != null && rowIndex != null) {
|
||||
final updatedRowInfo = rowInfo.copyWith(rowMeta: rowMetaPB);
|
||||
_rowList.remove(rowMetaPB.id);
|
||||
_rowList.insert(rowIndex, updatedRowInfo);
|
||||
|
||||
final UpdatedIndexMap updatedIndexs = UpdatedIndexMap();
|
||||
updatedIndexs[rowMetaPB.id] = UpdatedIndex(
|
||||
index: rowIndex,
|
||||
rowId: rowMetaPB.id,
|
||||
);
|
||||
|
||||
_rowChangeReasonNotifier
|
||||
.receive(RowsChangedReason.update(updatedIndexs));
|
||||
}
|
||||
},
|
||||
(err) => Log.error(err),
|
||||
);
|
||||
}
|
||||
|
||||
CellContextByFieldId _makeGridCells(RowId rowId, RowPB? row) {
|
||||
CellContextByFieldId _makeGridCells(RowMetaPB rowMeta) {
|
||||
// ignore: prefer_collection_literals
|
||||
final cellDataMap = CellContextByFieldId();
|
||||
final cellContextMap = CellContextByFieldId();
|
||||
for (final field in _delegate.fields) {
|
||||
if (field.visibility) {
|
||||
cellDataMap[field.id] = DatabaseCellContext(
|
||||
rowId: rowId,
|
||||
cellContextMap[field.id] = DatabaseCellContext(
|
||||
rowMeta: rowMeta,
|
||||
viewId: viewId,
|
||||
fieldInfo: field,
|
||||
);
|
||||
}
|
||||
}
|
||||
return cellDataMap;
|
||||
return cellContextMap;
|
||||
}
|
||||
|
||||
void _refreshRow(OptionalRowPB optionRow) {
|
||||
if (!optionRow.hasRow()) {
|
||||
return;
|
||||
}
|
||||
final updatedRow = optionRow.row;
|
||||
updatedRow.freeze();
|
||||
|
||||
final rowInfo = _rowList.get(updatedRow.id);
|
||||
final rowIndex = _rowList.indexOfRow(updatedRow.id);
|
||||
if (rowInfo != null && rowIndex != null) {
|
||||
final updatedRowInfo = rowInfo.copyWith(rowPB: updatedRow);
|
||||
_rowList.remove(updatedRow.id);
|
||||
_rowList.insert(rowIndex, updatedRowInfo);
|
||||
|
||||
final UpdatedIndexMap updatedIndexs = UpdatedIndexMap();
|
||||
updatedIndexs[rowInfo.rowPB.id] = UpdatedIndex(
|
||||
index: rowIndex,
|
||||
rowId: updatedRowInfo.rowPB.id,
|
||||
);
|
||||
|
||||
_rowChangeReasonNotifier.receive(RowsChangedReason.update(updatedIndexs));
|
||||
}
|
||||
}
|
||||
|
||||
RowInfo buildGridRow(RowPB rowPB) {
|
||||
RowInfo buildGridRow(RowMetaPB rowMetaPB) {
|
||||
return RowInfo(
|
||||
viewId: viewId,
|
||||
fields: _delegate.fields,
|
||||
rowPB: rowPB,
|
||||
rowId: rowMetaPB.id,
|
||||
rowMeta: rowMetaPB,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -310,9 +312,10 @@ class RowChangesetNotifier extends ChangeNotifier {
|
||||
@unfreezed
|
||||
class RowInfo with _$RowInfo {
|
||||
factory RowInfo({
|
||||
required String rowId,
|
||||
required String viewId,
|
||||
required UnmodifiableListView<FieldInfo> fields,
|
||||
required RowPB rowPB,
|
||||
required RowMetaPB rowMeta,
|
||||
}) = _RowInfo;
|
||||
}
|
||||
|
||||
|
@ -1,12 +1,12 @@
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/row_entities.pb.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import '../cell/cell_service.dart';
|
||||
import 'row_cache.dart';
|
||||
import 'row_service.dart';
|
||||
|
||||
typedef OnRowChanged = void Function(CellContextByFieldId, RowsChangedReason);
|
||||
|
||||
class RowController {
|
||||
final RowId rowId;
|
||||
final RowMetaPB rowMeta;
|
||||
final String? groupId;
|
||||
final String viewId;
|
||||
final List<VoidCallback> _onRowChangedListeners = [];
|
||||
@ -14,24 +14,27 @@ class RowController {
|
||||
|
||||
get cellCache => _rowCache.cellCache;
|
||||
|
||||
get rowId => rowMeta.id;
|
||||
|
||||
RowController({
|
||||
required this.rowId,
|
||||
required this.rowMeta,
|
||||
required this.viewId,
|
||||
required RowCache rowCache,
|
||||
this.groupId,
|
||||
}) : _rowCache = rowCache;
|
||||
|
||||
CellContextByFieldId loadData() {
|
||||
return _rowCache.loadGridCells(rowId);
|
||||
return _rowCache.loadGridCells(rowMeta);
|
||||
}
|
||||
|
||||
void addListener({OnRowChanged? onRowChanged}) {
|
||||
_onRowChangedListeners.add(
|
||||
_rowCache.addListener(
|
||||
rowId: rowId,
|
||||
onCellUpdated: onRowChanged,
|
||||
),
|
||||
final fn = _rowCache.addListener(
|
||||
rowId: rowMeta.id,
|
||||
onCellUpdated: onRowChanged,
|
||||
);
|
||||
|
||||
// Add the listener to the list so that we can remove it later.
|
||||
_onRowChangedListeners.add(fn);
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
|
@ -25,10 +25,9 @@ class RowList {
|
||||
}
|
||||
|
||||
void add(RowInfo rowInfo) {
|
||||
final rowId = rowInfo.rowPB.id;
|
||||
final rowId = rowInfo.rowId;
|
||||
if (contains(rowId)) {
|
||||
final index =
|
||||
_rowInfos.indexWhere((element) => element.rowPB.id == rowId);
|
||||
final index = _rowInfos.indexWhere((element) => element.rowId == rowId);
|
||||
_rowInfos.removeAt(index);
|
||||
_rowInfos.insert(index, rowInfo);
|
||||
} else {
|
||||
@ -38,7 +37,7 @@ class RowList {
|
||||
}
|
||||
|
||||
InsertedIndex? insert(int index, RowInfo rowInfo) {
|
||||
final rowId = rowInfo.rowPB.id;
|
||||
final rowId = rowInfo.rowId;
|
||||
var insertedIndex = index;
|
||||
if (_rowInfos.length <= insertedIndex) {
|
||||
insertedIndex = _rowInfos.length;
|
||||
@ -62,7 +61,7 @@ class RowList {
|
||||
if (rowInfo != null) {
|
||||
final index = _rowInfos.indexOf(rowInfo);
|
||||
if (index != -1) {
|
||||
rowInfoByRowId.remove(rowInfo.rowPB.id);
|
||||
rowInfoByRowId.remove(rowInfo.rowId);
|
||||
_rowInfos.remove(rowInfo);
|
||||
}
|
||||
return DeletedIndex(index: index, rowInfo: rowInfo);
|
||||
@ -73,23 +72,23 @@ class RowList {
|
||||
|
||||
InsertedIndexs insertRows(
|
||||
List<InsertedRowPB> insertedRows,
|
||||
RowInfo Function(RowPB) builder,
|
||||
RowInfo Function(RowMetaPB) builder,
|
||||
) {
|
||||
final InsertedIndexs insertIndexs = [];
|
||||
for (final insertRow in insertedRows) {
|
||||
final isContains = contains(insertRow.row.id);
|
||||
final isContains = contains(insertRow.rowMeta.id);
|
||||
|
||||
var index = insertRow.index;
|
||||
if (_rowInfos.length < index) {
|
||||
index = _rowInfos.length;
|
||||
}
|
||||
insert(index, builder(insertRow.row));
|
||||
insert(index, builder(insertRow.rowMeta));
|
||||
|
||||
if (!isContains) {
|
||||
insertIndexs.add(
|
||||
InsertedIndex(
|
||||
index: index,
|
||||
rowId: insertRow.row.id,
|
||||
rowId: insertRow.rowMeta.id,
|
||||
),
|
||||
);
|
||||
}
|
||||
@ -105,10 +104,10 @@ class RowList {
|
||||
};
|
||||
|
||||
_rowInfos.asMap().forEach((index, RowInfo rowInfo) {
|
||||
if (deletedRowByRowId[rowInfo.rowPB.id] == null) {
|
||||
if (deletedRowByRowId[rowInfo.rowId] == null) {
|
||||
newRows.add(rowInfo);
|
||||
} else {
|
||||
rowInfoByRowId.remove(rowInfo.rowPB.id);
|
||||
rowInfoByRowId.remove(rowInfo.rowId);
|
||||
deletedIndex.add(DeletedIndex(index: index, rowInfo: rowInfo));
|
||||
}
|
||||
});
|
||||
@ -117,19 +116,21 @@ class RowList {
|
||||
}
|
||||
|
||||
UpdatedIndexMap updateRows(
|
||||
List<RowPB> updatedRows,
|
||||
RowInfo Function(RowPB) builder,
|
||||
List<RowMetaPB> rowMetas,
|
||||
RowInfo Function(RowMetaPB) builder,
|
||||
) {
|
||||
final UpdatedIndexMap updatedIndexs = UpdatedIndexMap();
|
||||
for (final RowPB updatedRow in updatedRows) {
|
||||
final rowId = updatedRow.id;
|
||||
for (final rowMeta in rowMetas) {
|
||||
final index = _rowInfos.indexWhere(
|
||||
(rowInfo) => rowInfo.rowPB.id == rowId,
|
||||
(rowInfo) => rowInfo.rowId == rowMeta.id,
|
||||
);
|
||||
if (index != -1) {
|
||||
final rowInfo = builder(updatedRow);
|
||||
final rowInfo = builder(rowMeta);
|
||||
insert(index, rowInfo);
|
||||
updatedIndexs[rowId] = UpdatedIndex(index: index, rowId: rowId);
|
||||
updatedIndexs[rowMeta.id] = UpdatedIndex(
|
||||
index: index,
|
||||
rowId: rowMeta.id,
|
||||
);
|
||||
}
|
||||
}
|
||||
return updatedIndexs;
|
||||
@ -148,7 +149,7 @@ class RowList {
|
||||
|
||||
void moveRow(RowId rowId, int oldIndex, int newIndex) {
|
||||
final index = _rowInfos.indexWhere(
|
||||
(rowInfo) => rowInfo.rowPB.id == rowId,
|
||||
(rowInfo) => rowInfo.rowId == rowId,
|
||||
);
|
||||
if (index != -1) {
|
||||
final rowInfo = remove(rowId)!.rowInfo;
|
||||
|
@ -0,0 +1,49 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:appflowy/core/notification/grid_notification.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/protobuf.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
||||
import 'package:dartz/dartz.dart';
|
||||
|
||||
typedef RowMetaCallback = void Function(RowMetaPB);
|
||||
|
||||
class RowMetaListener {
|
||||
final String rowId;
|
||||
RowMetaCallback? _callback;
|
||||
DatabaseNotificationListener? _listener;
|
||||
RowMetaListener(this.rowId);
|
||||
|
||||
void start({required RowMetaCallback callback}) {
|
||||
_callback = callback;
|
||||
_listener = DatabaseNotificationListener(
|
||||
objectId: rowId,
|
||||
handler: _handler,
|
||||
);
|
||||
}
|
||||
|
||||
void _handler(
|
||||
DatabaseNotification ty,
|
||||
Either<Uint8List, FlowyError> result,
|
||||
) {
|
||||
switch (ty) {
|
||||
case DatabaseNotification.DidUpdateRowMeta:
|
||||
result.fold(
|
||||
(payload) {
|
||||
if (_callback != null) {
|
||||
_callback!(RowMetaPB.fromBuffer(payload));
|
||||
}
|
||||
},
|
||||
(error) => Log.error(error),
|
||||
);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> stop() async {
|
||||
await _listener?.stop();
|
||||
_callback = null;
|
||||
}
|
||||
}
|
@ -12,7 +12,7 @@ class RowBackendService {
|
||||
required this.viewId,
|
||||
});
|
||||
|
||||
Future<Either<RowPB, FlowyError>> createRow(RowId rowId) {
|
||||
Future<Either<RowMetaPB, FlowyError>> createRowAfterRow(RowId rowId) {
|
||||
final payload = CreateRowPayloadPB.create()
|
||||
..viewId = viewId
|
||||
..startRowId = rowId;
|
||||
@ -28,6 +28,33 @@ class RowBackendService {
|
||||
return DatabaseEventGetRow(payload).send();
|
||||
}
|
||||
|
||||
Future<Either<RowMetaPB, FlowyError>> getRowMeta(RowId rowId) {
|
||||
final payload = RowIdPB.create()
|
||||
..viewId = viewId
|
||||
..rowId = rowId;
|
||||
|
||||
return DatabaseEventGetRowMeta(payload).send();
|
||||
}
|
||||
|
||||
Future<Either<Unit, FlowyError>> updateMeta({
|
||||
required String rowId,
|
||||
String? iconURL,
|
||||
String? coverURL,
|
||||
}) {
|
||||
final payload = UpdateRowMetaChangesetPB.create()
|
||||
..viewId = viewId
|
||||
..id = rowId;
|
||||
|
||||
if (iconURL != null) {
|
||||
payload.iconUrl = iconURL;
|
||||
}
|
||||
if (coverURL != null) {
|
||||
payload.coverUrl = coverURL;
|
||||
}
|
||||
|
||||
return DatabaseEventUpdateRowMeta(payload).send();
|
||||
}
|
||||
|
||||
Future<Either<Unit, FlowyError>> deleteRow(RowId rowId) {
|
||||
final payload = RowIdPB.create()
|
||||
..viewId = viewId
|
||||
|
@ -65,14 +65,16 @@ class DatabaseViewCache {
|
||||
}
|
||||
|
||||
if (changeset.updatedRows.isNotEmpty) {
|
||||
_callbacks?.onRowsUpdated
|
||||
?.call(changeset.updatedRows.map((e) => e.row.id).toList());
|
||||
_callbacks?.onRowsUpdated?.call(
|
||||
changeset.updatedRows.map((e) => e.rowId).toList(),
|
||||
_rowCache.changeReason,
|
||||
);
|
||||
}
|
||||
|
||||
if (changeset.insertedRows.isNotEmpty) {
|
||||
_callbacks?.onRowsCreated?.call(
|
||||
changeset.insertedRows
|
||||
.map((insertedRow) => insertedRow.row.id)
|
||||
.map((insertedRow) => insertedRow.rowMeta.id)
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
|
@ -156,7 +156,7 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
|
||||
);
|
||||
}
|
||||
|
||||
void _groupItemStartEditing(GroupPB group, RowPB row, bool isEdit) {
|
||||
void _groupItemStartEditing(GroupPB group, RowMetaPB row, bool isEdit) {
|
||||
final fieldInfo = fieldController.getField(group.fieldId);
|
||||
if (fieldInfo == null) {
|
||||
Log.warn("fieldInfo should not be null");
|
||||
@ -302,12 +302,12 @@ class BoardEvent with _$BoardEvent {
|
||||
const factory BoardEvent.createHeaderRow(String groupId) = _CreateHeaderRow;
|
||||
const factory BoardEvent.didCreateRow(
|
||||
GroupPB group,
|
||||
RowPB row,
|
||||
RowMetaPB row,
|
||||
int? index,
|
||||
) = _DidCreateRow;
|
||||
const factory BoardEvent.startEditingRow(
|
||||
GroupPB group,
|
||||
RowPB row,
|
||||
RowMetaPB row,
|
||||
) = _StartEditRow;
|
||||
const factory BoardEvent.endEditingRow(RowId rowId) = _EndEditRow;
|
||||
const factory BoardEvent.didReceiveError(FlowyError error) = _DidReceiveError;
|
||||
@ -371,7 +371,7 @@ class GridFieldEquatable extends Equatable {
|
||||
}
|
||||
|
||||
class GroupItem extends AppFlowyGroupItem {
|
||||
final RowPB row;
|
||||
final RowMetaPB row;
|
||||
final FieldInfo fieldInfo;
|
||||
|
||||
GroupItem({
|
||||
@ -389,7 +389,7 @@ class GroupItem extends AppFlowyGroupItem {
|
||||
class GroupControllerDelegateImpl extends GroupControllerDelegate {
|
||||
final FieldController fieldController;
|
||||
final AppFlowyBoardController controller;
|
||||
final void Function(String, RowPB, int?) onNewColumnItem;
|
||||
final void Function(String, RowMetaPB, int?) onNewColumnItem;
|
||||
|
||||
GroupControllerDelegateImpl({
|
||||
required this.controller,
|
||||
@ -398,7 +398,7 @@ class GroupControllerDelegateImpl extends GroupControllerDelegate {
|
||||
});
|
||||
|
||||
@override
|
||||
void insertRow(GroupPB group, RowPB row, int? index) {
|
||||
void insertRow(GroupPB group, RowMetaPB row, int? index) {
|
||||
final fieldInfo = fieldController.getField(group.fieldId);
|
||||
if (fieldInfo == null) {
|
||||
Log.warn("fieldInfo should not be null");
|
||||
@ -426,7 +426,7 @@ class GroupControllerDelegateImpl extends GroupControllerDelegate {
|
||||
}
|
||||
|
||||
@override
|
||||
void updateRow(GroupPB group, RowPB row) {
|
||||
void updateRow(GroupPB group, RowMetaPB row) {
|
||||
final fieldInfo = fieldController.getField(group.fieldId);
|
||||
if (fieldInfo == null) {
|
||||
Log.warn("fieldInfo should not be null");
|
||||
@ -442,7 +442,7 @@ class GroupControllerDelegateImpl extends GroupControllerDelegate {
|
||||
}
|
||||
|
||||
@override
|
||||
void addNewRow(GroupPB group, RowPB row, int? index) {
|
||||
void addNewRow(GroupPB group, RowMetaPB row, int? index) {
|
||||
final fieldInfo = fieldController.getField(group.fieldId);
|
||||
if (fieldInfo == null) {
|
||||
Log.warn("fieldInfo should not be null");
|
||||
@ -465,7 +465,7 @@ class GroupControllerDelegateImpl extends GroupControllerDelegate {
|
||||
|
||||
class BoardEditingRow {
|
||||
GroupPB group;
|
||||
RowPB row;
|
||||
RowMetaPB row;
|
||||
int? index;
|
||||
|
||||
BoardEditingRow({
|
||||
|
@ -12,9 +12,9 @@ typedef OnGroupError = void Function(FlowyError);
|
||||
|
||||
abstract class GroupControllerDelegate {
|
||||
void removeRow(GroupPB group, RowId rowId);
|
||||
void insertRow(GroupPB group, RowPB row, int? index);
|
||||
void updateRow(GroupPB group, RowPB row);
|
||||
void addNewRow(GroupPB group, RowPB row, int? index);
|
||||
void insertRow(GroupPB group, RowMetaPB row, int? index);
|
||||
void updateRow(GroupPB group, RowMetaPB row);
|
||||
void addNewRow(GroupPB group, RowMetaPB row, int? index);
|
||||
}
|
||||
|
||||
class GroupController {
|
||||
@ -28,7 +28,7 @@ class GroupController {
|
||||
required this.delegate,
|
||||
}) : _listener = SingleGroupListener(group);
|
||||
|
||||
RowPB? rowAtIndex(int index) {
|
||||
RowMetaPB? rowAtIndex(int index) {
|
||||
if (index < group.rows.length) {
|
||||
return group.rows[index];
|
||||
} else {
|
||||
@ -36,7 +36,7 @@ class GroupController {
|
||||
}
|
||||
}
|
||||
|
||||
RowPB? lastRow() {
|
||||
RowMetaPB? lastRow() {
|
||||
if (group.rows.isEmpty) return null;
|
||||
return group.rows.last;
|
||||
}
|
||||
@ -55,15 +55,15 @@ class GroupController {
|
||||
final index = insertedRow.hasIndex() ? insertedRow.index : null;
|
||||
if (insertedRow.hasIndex() &&
|
||||
group.rows.length > insertedRow.index) {
|
||||
group.rows.insert(insertedRow.index, insertedRow.row);
|
||||
group.rows.insert(insertedRow.index, insertedRow.rowMeta);
|
||||
} else {
|
||||
group.rows.add(insertedRow.row);
|
||||
group.rows.add(insertedRow.rowMeta);
|
||||
}
|
||||
|
||||
if (insertedRow.isNew) {
|
||||
delegate.addNewRow(group, insertedRow.row, index);
|
||||
delegate.addNewRow(group, insertedRow.rowMeta, index);
|
||||
} else {
|
||||
delegate.insertRow(group, insertedRow.row, index);
|
||||
delegate.insertRow(group, insertedRow.rowMeta, index);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -45,8 +45,12 @@ class BoardPlugin extends Plugin {
|
||||
BoardPlugin({
|
||||
required ViewPB view,
|
||||
required PluginType pluginType,
|
||||
bool listenOnViewChanged = false,
|
||||
}) : _pluginType = pluginType,
|
||||
notifier = ViewPluginNotifier(view: view);
|
||||
notifier = ViewPluginNotifier(
|
||||
view: view,
|
||||
listenOnViewChanged: listenOnViewChanged,
|
||||
);
|
||||
|
||||
@override
|
||||
PluginWidgetBuilder get widgetBuilder =>
|
||||
|
@ -231,7 +231,7 @@ class _BoardContentState extends State<BoardContent> {
|
||||
) {
|
||||
final groupItem = afGroupItem as GroupItem;
|
||||
final groupData = afGroupData.customData as GroupData;
|
||||
final rowPB = groupItem.row;
|
||||
final rowMeta = groupItem.row;
|
||||
final rowCache = context.read<BoardBloc>().getRowCache();
|
||||
|
||||
/// Return placeholder widget if the rowCache is null.
|
||||
@ -255,7 +255,7 @@ class _BoardContentState extends State<BoardContent> {
|
||||
margin: config.cardPadding,
|
||||
decoration: _makeBoxDecoration(context),
|
||||
child: RowCard<String>(
|
||||
row: rowPB,
|
||||
rowMeta: rowMeta,
|
||||
viewId: viewId,
|
||||
rowCache: rowCache,
|
||||
cardData: groupData.group.groupId,
|
||||
@ -267,7 +267,7 @@ class _BoardContentState extends State<BoardContent> {
|
||||
viewId,
|
||||
groupData.group.groupId,
|
||||
fieldController,
|
||||
rowPB,
|
||||
rowMeta,
|
||||
rowCache,
|
||||
context,
|
||||
),
|
||||
@ -305,18 +305,19 @@ class _BoardContentState extends State<BoardContent> {
|
||||
String viewId,
|
||||
String groupId,
|
||||
FieldController fieldController,
|
||||
RowPB rowPB,
|
||||
RowMetaPB rowMetaPB,
|
||||
RowCache rowCache,
|
||||
BuildContext context,
|
||||
) {
|
||||
final rowInfo = RowInfo(
|
||||
viewId: viewId,
|
||||
fields: UnmodifiableListView(fieldController.fieldInfos),
|
||||
rowPB: rowPB,
|
||||
rowMeta: rowMetaPB,
|
||||
rowId: rowMetaPB.id,
|
||||
);
|
||||
|
||||
final dataController = RowController(
|
||||
rowId: rowInfo.rowPB.id,
|
||||
rowMeta: rowInfo.rowMeta,
|
||||
viewId: rowInfo.viewId,
|
||||
rowCache: rowCache,
|
||||
groupId: groupId,
|
||||
|
@ -268,7 +268,7 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
|
||||
|
||||
final eventData = CalendarDayEvent(
|
||||
event: eventPB,
|
||||
eventId: eventPB.rowId,
|
||||
eventId: eventPB.rowMeta.id,
|
||||
dateFieldId: eventPB.dateFieldId,
|
||||
date: date,
|
||||
);
|
||||
@ -310,7 +310,7 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
|
||||
}
|
||||
add(CalendarEvent.didDeleteEvents(rowIds));
|
||||
},
|
||||
onRowsUpdated: (rowIds) async {
|
||||
onRowsUpdated: (rowIds, reason) async {
|
||||
if (isClosed) {
|
||||
return;
|
||||
}
|
||||
|
@ -45,8 +45,12 @@ class CalendarPlugin extends Plugin {
|
||||
CalendarPlugin({
|
||||
required ViewPB view,
|
||||
required PluginType pluginType,
|
||||
bool listenOnViewChanged = false,
|
||||
}) : _pluginType = pluginType,
|
||||
notifier = ViewPluginNotifier(view: view);
|
||||
notifier = ViewPluginNotifier(
|
||||
view: view,
|
||||
listenOnViewChanged: listenOnViewChanged,
|
||||
);
|
||||
|
||||
@override
|
||||
PluginWidgetBuilder get widgetBuilder =>
|
||||
|
@ -301,7 +301,7 @@ class _EventCard extends StatelessWidget {
|
||||
// Add the key here to make sure the card is rebuilt when the cells
|
||||
// in this row are updated.
|
||||
key: ValueKey(event.eventId),
|
||||
row: rowInfo!.rowPB,
|
||||
rowMeta: rowInfo!.rowMeta,
|
||||
viewId: viewId,
|
||||
rowCache: rowCache,
|
||||
cardData: event.dateFieldId,
|
||||
|
@ -243,7 +243,7 @@ void showEventDetails({
|
||||
required RowCache rowCache,
|
||||
}) {
|
||||
final dataController = RowController(
|
||||
rowId: event.eventId,
|
||||
rowMeta: event.event.rowMeta,
|
||||
viewId: viewId,
|
||||
rowCache: rowCache,
|
||||
);
|
||||
|
@ -1,61 +0,0 @@
|
||||
import 'package:appflowy/startup/plugin/plugin.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_ext.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_listener.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
||||
import '../../workspace/presentation/home/home_stack.dart';
|
||||
|
||||
/// [DatabaseViewPlugin] is used to build the grid, calendar, and board.
|
||||
/// It is a wrapper of the [Plugin] class. The underlying [Plugin] is
|
||||
/// determined by the [ViewPB.pluginType] field.
|
||||
///
|
||||
class DatabaseViewPlugin extends Plugin {
|
||||
final ViewListener _viewListener;
|
||||
ViewPB _view;
|
||||
Plugin _innerPlugin;
|
||||
|
||||
DatabaseViewPlugin({
|
||||
required ViewPB view,
|
||||
}) : _view = view,
|
||||
_innerPlugin = _makeInnerPlugin(view),
|
||||
_viewListener = ViewListener(view: view) {
|
||||
_listenOnLayoutChanged();
|
||||
}
|
||||
|
||||
@override
|
||||
PluginId get id => _innerPlugin.id;
|
||||
|
||||
@override
|
||||
PluginType get pluginType => _innerPlugin.pluginType;
|
||||
|
||||
@override
|
||||
PluginWidgetBuilder get widgetBuilder => _innerPlugin.widgetBuilder;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_viewListener.stop();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _listenOnLayoutChanged() {
|
||||
_viewListener.start(
|
||||
onViewUpdated: (result) {
|
||||
result.fold(
|
||||
(updatedView) {
|
||||
if (_view.layout != updatedView.layout) {
|
||||
_innerPlugin = _makeInnerPlugin(updatedView);
|
||||
|
||||
getIt<HomeStackManager>().setPlugin(_innerPlugin);
|
||||
}
|
||||
_view = updatedView;
|
||||
},
|
||||
(r) => null,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Plugin _makeInnerPlugin(ViewPB view) {
|
||||
return makePlugin(pluginType: view.pluginType, data: view);
|
||||
}
|
@ -33,18 +33,18 @@ class GridBloc extends Bloc<GridEvent, GridState> {
|
||||
final rowService = RowBackendService(
|
||||
viewId: rowInfo.viewId,
|
||||
);
|
||||
await rowService.deleteRow(rowInfo.rowPB.id);
|
||||
await rowService.deleteRow(rowInfo.rowId);
|
||||
},
|
||||
moveRow: (int from, int to) {
|
||||
final List<RowInfo> rows = [...state.rowInfos];
|
||||
|
||||
final fromRow = rows[from].rowPB;
|
||||
final toRow = rows[to].rowPB;
|
||||
final fromRow = rows[from].rowId;
|
||||
final toRow = rows[to].rowId;
|
||||
|
||||
rows.insert(to, rows.removeAt(from));
|
||||
emit(state.copyWith(rowInfos: rows));
|
||||
|
||||
databaseController.moveRow(fromRow: fromRow, toRow: toRow);
|
||||
databaseController.moveRow(fromRowId: fromRow, toRowId: toRow);
|
||||
},
|
||||
didReceiveGridUpdate: (grid) {
|
||||
emit(state.copyWith(grid: Some(grid)));
|
||||
@ -56,7 +56,7 @@ class GridBloc extends Bloc<GridEvent, GridState> {
|
||||
),
|
||||
);
|
||||
},
|
||||
didReceiveRowUpdate: (newRowInfos, reason) {
|
||||
didLoadRows: (newRowInfos, reason) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
rowInfos: newRowInfos,
|
||||
@ -76,7 +76,7 @@ class GridBloc extends Bloc<GridEvent, GridState> {
|
||||
return super.close();
|
||||
}
|
||||
|
||||
RowCache? getRowCache(RowId rowId) {
|
||||
RowCache getRowCache(RowId rowId) {
|
||||
return databaseController.rowCache;
|
||||
}
|
||||
|
||||
@ -89,9 +89,14 @@ class GridBloc extends Bloc<GridEvent, GridState> {
|
||||
},
|
||||
onNumOfRowsChanged: (rowInfos, _, reason) {
|
||||
if (!isClosed) {
|
||||
add(GridEvent.didReceiveRowUpdate(rowInfos, reason));
|
||||
add(GridEvent.didLoadRows(rowInfos, reason));
|
||||
}
|
||||
},
|
||||
onRowsUpdated: (rows, reason) {
|
||||
add(
|
||||
GridEvent.didLoadRows(databaseController.rowCache.rowInfos, reason),
|
||||
);
|
||||
},
|
||||
onFieldsChanged: (fields) {
|
||||
if (!isClosed) {
|
||||
add(GridEvent.didReceiveFieldUpdate(fields));
|
||||
@ -122,9 +127,9 @@ class GridEvent with _$GridEvent {
|
||||
const factory GridEvent.createRow() = _CreateRow;
|
||||
const factory GridEvent.deleteRow(RowInfo rowInfo) = _DeleteRow;
|
||||
const factory GridEvent.moveRow(int from, int to) = _MoveRow;
|
||||
const factory GridEvent.didReceiveRowUpdate(
|
||||
const factory GridEvent.didLoadRows(
|
||||
List<RowInfo> rows,
|
||||
RowsChangedReason listState,
|
||||
RowsChangedReason reason,
|
||||
) = _DidReceiveRowUpdate;
|
||||
const factory GridEvent.didReceiveFieldUpdate(
|
||||
List<FieldInfo> fields,
|
||||
|
@ -4,7 +4,6 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:dartz/dartz.dart';
|
||||
|
||||
import '../../../application/row/row_cache.dart';
|
||||
import '../../../application/row/row_service.dart';
|
||||
|
||||
part 'row_action_sheet_bloc.freezed.dart';
|
||||
@ -13,19 +12,20 @@ class RowActionSheetBloc
|
||||
extends Bloc<RowActionSheetEvent, RowActionSheetState> {
|
||||
final RowBackendService _rowService;
|
||||
|
||||
RowActionSheetBloc({required RowInfo rowInfo})
|
||||
: _rowService = RowBackendService(viewId: rowInfo.viewId),
|
||||
super(RowActionSheetState.initial(rowInfo)) {
|
||||
RowActionSheetBloc({
|
||||
required String viewId,
|
||||
required RowId rowId,
|
||||
}) : _rowService = RowBackendService(viewId: viewId),
|
||||
super(RowActionSheetState.initial(rowId)) {
|
||||
on<RowActionSheetEvent>(
|
||||
(event, emit) async {
|
||||
await event.when(
|
||||
deleteRow: () async {
|
||||
final result = await _rowService.deleteRow(state.rowData.rowPB.id);
|
||||
final result = await _rowService.deleteRow(state.rowId);
|
||||
logResult(result);
|
||||
},
|
||||
duplicateRow: () async {
|
||||
final result =
|
||||
await _rowService.duplicateRow(rowId: state.rowData.rowPB.id);
|
||||
final result = await _rowService.duplicateRow(rowId: state.rowId);
|
||||
logResult(result);
|
||||
},
|
||||
);
|
||||
@ -47,10 +47,10 @@ class RowActionSheetEvent with _$RowActionSheetEvent {
|
||||
@freezed
|
||||
class RowActionSheetState with _$RowActionSheetState {
|
||||
const factory RowActionSheetState({
|
||||
required RowInfo rowData,
|
||||
required RowId rowId,
|
||||
}) = _RowActionSheetState;
|
||||
|
||||
factory RowActionSheetState.initial(RowInfo rowData) => RowActionSheetState(
|
||||
rowData: rowData,
|
||||
factory RowActionSheetState.initial(RowId rowId) => RowActionSheetState(
|
||||
rowId: rowId,
|
||||
);
|
||||
}
|
||||
|
@ -15,13 +15,16 @@ part 'row_bloc.freezed.dart';
|
||||
class RowBloc extends Bloc<RowEvent, RowState> {
|
||||
final RowBackendService _rowBackendSvc;
|
||||
final RowController _dataController;
|
||||
final String viewId;
|
||||
final String rowId;
|
||||
|
||||
RowBloc({
|
||||
required RowInfo rowInfo,
|
||||
required this.rowId,
|
||||
required this.viewId,
|
||||
required RowController dataController,
|
||||
}) : _rowBackendSvc = RowBackendService(viewId: rowInfo.viewId),
|
||||
}) : _rowBackendSvc = RowBackendService(viewId: viewId),
|
||||
_dataController = dataController,
|
||||
super(RowState.initial(rowInfo, dataController.loadData())) {
|
||||
super(RowState.initial(dataController.loadData())) {
|
||||
on<RowEvent>(
|
||||
(event, emit) async {
|
||||
await event.when(
|
||||
@ -29,7 +32,7 @@ class RowBloc extends Bloc<RowEvent, RowState> {
|
||||
await _startListening();
|
||||
},
|
||||
createRow: () {
|
||||
_rowBackendSvc.createRow(rowInfo.rowPB.id);
|
||||
_rowBackendSvc.createRowAfterRow(rowId);
|
||||
},
|
||||
didReceiveCells: (cellByFieldId, reason) async {
|
||||
final cells = cellByFieldId.values
|
||||
@ -78,18 +81,15 @@ class RowEvent with _$RowEvent {
|
||||
@freezed
|
||||
class RowState with _$RowState {
|
||||
const factory RowState({
|
||||
required RowInfo rowInfo,
|
||||
required CellContextByFieldId cellByFieldId,
|
||||
required UnmodifiableListView<GridCellEquatable> cells,
|
||||
RowsChangedReason? changeReason,
|
||||
}) = _RowState;
|
||||
|
||||
factory RowState.initial(
|
||||
RowInfo rowInfo,
|
||||
CellContextByFieldId cellByFieldId,
|
||||
) =>
|
||||
RowState(
|
||||
rowInfo: rowInfo,
|
||||
cellByFieldId: cellByFieldId,
|
||||
cells: UnmodifiableListView(
|
||||
cellByFieldId.values
|
||||
|
@ -27,7 +27,7 @@ class RowDetailBloc extends Bloc<RowDetailEvent, RowDetailState> {
|
||||
}
|
||||
},
|
||||
didReceiveCellDatas: (cells) {
|
||||
emit(state.copyWith(gridCells: cells));
|
||||
emit(state.copyWith(cells: cells));
|
||||
},
|
||||
deleteField: (fieldId) {
|
||||
_fieldBackendService(fieldId).deleteField();
|
||||
@ -95,10 +95,10 @@ class RowDetailEvent with _$RowDetailEvent {
|
||||
@freezed
|
||||
class RowDetailState with _$RowDetailState {
|
||||
const factory RowDetailState({
|
||||
required List<DatabaseCellContext> gridCells,
|
||||
required List<DatabaseCellContext> cells,
|
||||
}) = _RowDetailState;
|
||||
|
||||
factory RowDetailState.initial() => RowDetailState(
|
||||
gridCells: List.empty(),
|
||||
cells: List.empty(),
|
||||
);
|
||||
}
|
||||
|
@ -0,0 +1,126 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_service.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/code.pbenum.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||
|
||||
import '../../../application/row/row_service.dart';
|
||||
|
||||
part 'row_document_bloc.freezed.dart';
|
||||
|
||||
class RowDocumentBloc extends Bloc<RowDocumentEvent, RowDocumentState> {
|
||||
final String rowId;
|
||||
final RowBackendService _rowBackendSvc;
|
||||
|
||||
RowDocumentBloc({
|
||||
required this.rowId,
|
||||
required String viewId,
|
||||
}) : _rowBackendSvc = RowBackendService(viewId: viewId),
|
||||
super(RowDocumentState.initial()) {
|
||||
on<RowDocumentEvent>(
|
||||
(event, emit) async {
|
||||
await event.when(
|
||||
initial: () async {
|
||||
_getRowDocumentView();
|
||||
},
|
||||
didReceiveRowDocument: (view) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
viewPB: view,
|
||||
loadingState: const LoadingState.finish(),
|
||||
),
|
||||
);
|
||||
},
|
||||
didReceiveError: (FlowyError error) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
loadingState: LoadingState.error(error),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _getRowDocumentView() async {
|
||||
final rowDetailOrError = await _rowBackendSvc.getRowMeta(rowId);
|
||||
rowDetailOrError.fold(
|
||||
(RowMetaPB rowMeta) async {
|
||||
final viewsOrError =
|
||||
await ViewBackendService.getView(rowMeta.documentId);
|
||||
|
||||
if (isClosed) {
|
||||
return;
|
||||
}
|
||||
|
||||
viewsOrError.fold(
|
||||
(view) => add(RowDocumentEvent.didReceiveRowDocument(view)),
|
||||
(error) async {
|
||||
if (error.code == ErrorCode.RecordNotFound.value) {
|
||||
// By default, the document of the row is not exist. So creating a
|
||||
// new document for the given document id of the row.
|
||||
final documentView =
|
||||
await _createRowDocumentView(rowMeta.documentId);
|
||||
if (documentView != null) {
|
||||
add(RowDocumentEvent.didReceiveRowDocument(documentView));
|
||||
}
|
||||
} else {
|
||||
add(RowDocumentEvent.didReceiveError(error));
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
(err) => Log.error('Failed to get row detail: $err'),
|
||||
);
|
||||
}
|
||||
|
||||
Future<ViewPB?> _createRowDocumentView(String viewId) async {
|
||||
final result = await ViewBackendService.createOrphanView(
|
||||
viewId: viewId,
|
||||
name: LocaleKeys.menuAppHeader_defaultNewPageName.tr(),
|
||||
desc: '',
|
||||
layoutType: ViewLayoutPB.Document,
|
||||
);
|
||||
return result.fold(
|
||||
(view) => view,
|
||||
(error) {
|
||||
Log.error(error);
|
||||
return null;
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
class RowDocumentEvent with _$RowDocumentEvent {
|
||||
const factory RowDocumentEvent.initial() = _InitialRow;
|
||||
const factory RowDocumentEvent.didReceiveRowDocument(ViewPB view) =
|
||||
_DidReceiveRowDocument;
|
||||
const factory RowDocumentEvent.didReceiveError(FlowyError error) =
|
||||
_DidReceiveError;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class RowDocumentState with _$RowDocumentState {
|
||||
const factory RowDocumentState({
|
||||
ViewPB? viewPB,
|
||||
required LoadingState loadingState,
|
||||
}) = _RowDocumentState;
|
||||
|
||||
factory RowDocumentState.initial() => const RowDocumentState(
|
||||
loadingState: LoadingState.loading(),
|
||||
);
|
||||
}
|
||||
|
||||
@freezed
|
||||
class LoadingState with _$LoadingState {
|
||||
const factory LoadingState.loading() = _Loading;
|
||||
const factory LoadingState.error(FlowyError error) = _Error;
|
||||
const factory LoadingState.finish() = _Finish;
|
||||
}
|
@ -45,8 +45,12 @@ class GridPlugin extends Plugin {
|
||||
GridPlugin({
|
||||
required ViewPB view,
|
||||
required PluginType pluginType,
|
||||
bool listenOnViewChanged = false,
|
||||
}) : _pluginType = pluginType,
|
||||
notifier = ViewPluginNotifier(view: view);
|
||||
notifier = ViewPluginNotifier(
|
||||
view: view,
|
||||
listenOnViewChanged: listenOnViewChanged,
|
||||
);
|
||||
|
||||
@override
|
||||
PluginWidgetBuilder get widgetBuilder =>
|
||||
|
@ -1,5 +1,7 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/row/row_service.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/row/cell_builder.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui_web.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/scrolling/styled_scroll_bar.dart';
|
||||
@ -78,7 +80,11 @@ class _GridPageState extends State<GridPage> {
|
||||
loading: (_) =>
|
||||
const Center(child: CircularProgressIndicator.adaptive()),
|
||||
finish: (result) => result.successOrFail.fold(
|
||||
(_) => const GridShortcuts(child: FlowyGrid()),
|
||||
(_) => GridShortcuts(
|
||||
child: FlowyGrid(
|
||||
viewId: widget.view.id,
|
||||
),
|
||||
),
|
||||
(err) => FlowyErrorPage(err.toString()),
|
||||
),
|
||||
);
|
||||
@ -89,7 +95,9 @@ class _GridPageState extends State<GridPage> {
|
||||
}
|
||||
|
||||
class FlowyGrid extends StatefulWidget {
|
||||
final String viewId;
|
||||
const FlowyGrid({
|
||||
required this.viewId,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@ -125,6 +133,7 @@ class _FlowyGridState extends State<FlowyGrid> {
|
||||
scrollController: _scrollController,
|
||||
contentWidth: contentWidth,
|
||||
child: _GridRows(
|
||||
viewId: widget.viewId,
|
||||
verticalScrollController: _scrollController.verticalController,
|
||||
),
|
||||
);
|
||||
@ -155,7 +164,9 @@ class _FlowyGridState extends State<FlowyGrid> {
|
||||
}
|
||||
|
||||
class _GridRows extends StatelessWidget {
|
||||
final String viewId;
|
||||
const _GridRows({
|
||||
required this.viewId,
|
||||
required this.verticalScrollController,
|
||||
});
|
||||
|
||||
@ -207,7 +218,7 @@ class _GridRows extends StatelessWidget {
|
||||
final rowInfo = rowInfos[index];
|
||||
return _renderRow(
|
||||
context,
|
||||
rowInfo,
|
||||
rowInfo.rowId,
|
||||
index: index,
|
||||
isSortEnabled: sortState.sortInfos.isNotEmpty,
|
||||
isFilterEnabled: filterState.filters.isNotEmpty,
|
||||
@ -223,38 +234,38 @@ class _GridRows extends StatelessWidget {
|
||||
|
||||
Widget _renderRow(
|
||||
BuildContext context,
|
||||
RowInfo rowInfo, {
|
||||
RowId rowId, {
|
||||
int? index,
|
||||
bool isSortEnabled = false,
|
||||
bool isFilterEnabled = false,
|
||||
Animation<double>? animation,
|
||||
}) {
|
||||
final rowCache = context.read<GridBloc>().getRowCache(
|
||||
rowInfo.rowPB.id,
|
||||
);
|
||||
final rowCache = context.read<GridBloc>().getRowCache(rowId);
|
||||
final rowMeta = rowCache.getRow(rowId)?.rowMeta;
|
||||
|
||||
/// Return placeholder widget if the rowCache is null.
|
||||
if (rowCache == null) return const SizedBox.shrink();
|
||||
/// Return placeholder widget if the rowMeta is null.
|
||||
if (rowMeta == null) return const SizedBox.shrink();
|
||||
|
||||
final fieldController =
|
||||
context.read<GridBloc>().databaseController.fieldController;
|
||||
final dataController = RowController(
|
||||
rowId: rowInfo.rowPB.id,
|
||||
viewId: rowInfo.viewId,
|
||||
viewId: viewId,
|
||||
rowMeta: rowMeta,
|
||||
rowCache: rowCache,
|
||||
);
|
||||
|
||||
final child = GridRow(
|
||||
key: ValueKey(rowInfo.rowPB.id),
|
||||
key: ValueKey(rowMeta.id),
|
||||
rowId: rowId,
|
||||
viewId: viewId,
|
||||
index: index,
|
||||
isDraggable: !isSortEnabled && !isFilterEnabled,
|
||||
rowInfo: rowInfo,
|
||||
dataController: dataController,
|
||||
cellBuilder: GridCellBuilder(cellCache: dataController.cellCache),
|
||||
openDetailPage: (context, cellBuilder) {
|
||||
_openRowDetailPage(
|
||||
context,
|
||||
rowInfo,
|
||||
rowId,
|
||||
fieldController,
|
||||
rowCache,
|
||||
cellBuilder,
|
||||
@ -274,26 +285,32 @@ class _GridRows extends StatelessWidget {
|
||||
|
||||
void _openRowDetailPage(
|
||||
BuildContext context,
|
||||
RowInfo rowInfo,
|
||||
RowId rowId,
|
||||
FieldController fieldController,
|
||||
RowCache rowCache,
|
||||
GridCellBuilder cellBuilder,
|
||||
) {
|
||||
final dataController = RowController(
|
||||
viewId: rowInfo.viewId,
|
||||
rowId: rowInfo.rowPB.id,
|
||||
rowCache: rowCache,
|
||||
);
|
||||
final rowMeta = rowCache.getRow(rowId)?.rowMeta;
|
||||
// Most of the cases, the rowMeta should not be null.
|
||||
if (rowMeta != null) {
|
||||
final dataController = RowController(
|
||||
viewId: viewId,
|
||||
rowMeta: rowMeta,
|
||||
rowCache: rowCache,
|
||||
);
|
||||
|
||||
FlowyOverlay.show(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return RowDetailPage(
|
||||
cellBuilder: cellBuilder,
|
||||
rowController: dataController,
|
||||
);
|
||||
},
|
||||
);
|
||||
FlowyOverlay.show(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return RowDetailPage(
|
||||
cellBuilder: cellBuilder,
|
||||
rowController: dataController,
|
||||
);
|
||||
},
|
||||
);
|
||||
} else {
|
||||
Log.warn('RowMeta is null for rowId: $rowId');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -357,10 +374,9 @@ class _RowCountBadge extends StatelessWidget {
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
FlowyText.medium(
|
||||
'${LocaleKeys.grid_row_count.tr()} : ',
|
||||
rowCountString(rowCount),
|
||||
color: Theme.of(context).hintColor,
|
||||
),
|
||||
FlowyText.medium(rowCount.toString()),
|
||||
],
|
||||
),
|
||||
);
|
||||
@ -368,3 +384,7 @@ class _RowCountBadge extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String rowCountString(int count) {
|
||||
return '${LocaleKeys.grid_row_count.tr()} : $count';
|
||||
}
|
||||
|
@ -52,11 +52,11 @@ class _FieldEditorState extends State<FieldEditor> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final List<Widget> children = [
|
||||
_FieldNameTextField(popoverMutex: popoverMutex),
|
||||
FieldNameTextField(popoverMutex: popoverMutex),
|
||||
if (widget.onDeleted != null) _addDeleteFieldButton(),
|
||||
if (widget.onHidden != null) _addHideFieldButton(),
|
||||
if (!widget.typeOptionLoader.field.isPrimary)
|
||||
_FieldTypeOptionCell(popoverMutex: popoverMutex),
|
||||
FieldTypeOptionCell(popoverMutex: popoverMutex),
|
||||
];
|
||||
return BlocProvider(
|
||||
create: (context) {
|
||||
@ -116,10 +116,10 @@ class _FieldEditorState extends State<FieldEditor> {
|
||||
}
|
||||
}
|
||||
|
||||
class _FieldTypeOptionCell extends StatelessWidget {
|
||||
class FieldTypeOptionCell extends StatelessWidget {
|
||||
final PopoverMutex popoverMutex;
|
||||
|
||||
const _FieldTypeOptionCell({
|
||||
const FieldTypeOptionCell({
|
||||
Key? key,
|
||||
required this.popoverMutex,
|
||||
}) : super(key: key);
|
||||
@ -130,7 +130,7 @@ class _FieldTypeOptionCell extends StatelessWidget {
|
||||
buildWhen: (p, c) => p.field != c.field,
|
||||
builder: (context, state) {
|
||||
return state.field.fold(
|
||||
() => const SizedBox(),
|
||||
() => const SizedBox.shrink(),
|
||||
(fieldInfo) {
|
||||
final dataController =
|
||||
context.read<FieldEditorBloc>().dataController;
|
||||
@ -145,18 +145,18 @@ class _FieldTypeOptionCell extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _FieldNameTextField extends StatefulWidget {
|
||||
class FieldNameTextField extends StatefulWidget {
|
||||
final PopoverMutex popoverMutex;
|
||||
const _FieldNameTextField({
|
||||
const FieldNameTextField({
|
||||
required this.popoverMutex,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<_FieldNameTextField> createState() => _FieldNameTextFieldState();
|
||||
State<FieldNameTextField> createState() => _FieldNameTextFieldState();
|
||||
}
|
||||
|
||||
class _FieldNameTextFieldState extends State<_FieldNameTextField> {
|
||||
class _FieldNameTextFieldState extends State<FieldNameTextField> {
|
||||
final textController = TextEditingController();
|
||||
FocusNode focusNode = FocusNode();
|
||||
|
||||
|
@ -48,7 +48,7 @@ class FieldTypeOptionEditor extends StatelessWidget {
|
||||
);
|
||||
|
||||
final List<Widget> children = [
|
||||
_SwitchFieldButton(popoverMutex: popoverMutex),
|
||||
SwitchFieldButton(popoverMutex: popoverMutex),
|
||||
if (typeOptionWidget != null) typeOptionWidget
|
||||
];
|
||||
|
||||
@ -73,9 +73,9 @@ class FieldTypeOptionEditor extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _SwitchFieldButton extends StatelessWidget {
|
||||
class SwitchFieldButton extends StatelessWidget {
|
||||
final PopoverMutex popoverMutex;
|
||||
const _SwitchFieldButton({
|
||||
const SwitchFieldButton({
|
||||
required this.popoverMutex,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
@ -1,4 +1,4 @@
|
||||
import 'package:appflowy/plugins/database_view/application/row/row_cache.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/row/row_service.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/application/row/row_action_sheet_bloc.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
@ -14,13 +14,18 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../../layout/sizes.dart';
|
||||
|
||||
class RowActions extends StatelessWidget {
|
||||
final RowInfo rowData;
|
||||
const RowActions({required this.rowData, Key? key}) : super(key: key);
|
||||
final String viewId;
|
||||
final RowId rowId;
|
||||
const RowActions({
|
||||
required this.viewId,
|
||||
required this.rowId,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (context) => RowActionSheetBloc(rowInfo: rowData),
|
||||
create: (context) => RowActionSheetBloc(viewId: viewId, rowId: rowId),
|
||||
child: BlocBuilder<RowActionSheetBloc, RowActionSheetState>(
|
||||
builder: (context, state) {
|
||||
final cells = _RowAction.values
|
||||
|
@ -1,6 +1,6 @@
|
||||
import 'package:appflowy/plugins/database_view/application/cell/cell_service.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/row/row_cache.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/row/row_data_controller.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/row/row_service.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/application/row/row_bloc.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/row/cell_builder.dart';
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
@ -20,7 +20,8 @@ import "package:appflowy/generated/locale_keys.g.dart";
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
|
||||
class GridRow extends StatefulWidget {
|
||||
final RowInfo rowInfo;
|
||||
final RowId viewId;
|
||||
final RowId rowId;
|
||||
final RowController dataController;
|
||||
final GridCellBuilder cellBuilder;
|
||||
final void Function(BuildContext, GridCellBuilder) openDetailPage;
|
||||
@ -30,7 +31,8 @@ class GridRow extends StatefulWidget {
|
||||
|
||||
const GridRow({
|
||||
super.key,
|
||||
required this.rowInfo,
|
||||
required this.viewId,
|
||||
required this.rowId,
|
||||
required this.dataController,
|
||||
required this.cellBuilder,
|
||||
required this.openDetailPage,
|
||||
@ -49,8 +51,9 @@ class _GridRowState extends State<GridRow> {
|
||||
void initState() {
|
||||
super.initState();
|
||||
_rowBloc = RowBloc(
|
||||
rowInfo: widget.rowInfo,
|
||||
rowId: widget.rowId,
|
||||
dataController: widget.dataController,
|
||||
viewId: widget.viewId,
|
||||
);
|
||||
_rowBloc.add(const RowEvent.initial());
|
||||
}
|
||||
@ -61,7 +64,8 @@ class _GridRowState extends State<GridRow> {
|
||||
value: _rowBloc,
|
||||
child: _RowEnterRegion(
|
||||
child: BlocBuilder<RowBloc, RowState>(
|
||||
buildWhen: (p, c) => p.rowInfo.rowPB.height != c.rowInfo.rowPB.height,
|
||||
// The row need to rebuild when the cell count changes.
|
||||
buildWhen: (p, c) => p.cellByFieldId.length != c.cellByFieldId.length,
|
||||
builder: (context, state) {
|
||||
final content = Expanded(
|
||||
child: RowContent(
|
||||
@ -126,7 +130,11 @@ class _RowLeadingState extends State<_RowLeading> {
|
||||
direction: PopoverDirection.rightWithCenterAligned,
|
||||
margin: const EdgeInsets.all(6),
|
||||
popupBuilder: (BuildContext popoverContext) {
|
||||
return RowActions(rowData: context.read<RowBloc>().state.rowInfo);
|
||||
final bloc = context.read<RowBloc>();
|
||||
return RowActions(
|
||||
viewId: bloc.viewId,
|
||||
rowId: bloc.rowId,
|
||||
);
|
||||
},
|
||||
child: Consumer<RegionStateNotifier>(
|
||||
builder: (context, state, _) {
|
||||
@ -143,11 +151,11 @@ class _RowLeadingState extends State<_RowLeading> {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const _InsertButton(),
|
||||
const InsertRowButton(),
|
||||
if (isDraggable) ...[
|
||||
ReorderableDragStartListener(
|
||||
index: widget.index!,
|
||||
child: _MenuButton(
|
||||
child: RowMenuButton(
|
||||
isDragEnabled: isDraggable,
|
||||
openMenu: () {
|
||||
popoverController.show();
|
||||
@ -155,7 +163,7 @@ class _RowLeadingState extends State<_RowLeading> {
|
||||
),
|
||||
),
|
||||
] else ...[
|
||||
_MenuButton(
|
||||
RowMenuButton(
|
||||
openMenu: () {
|
||||
popoverController.show();
|
||||
},
|
||||
@ -168,8 +176,8 @@ class _RowLeadingState extends State<_RowLeading> {
|
||||
bool get isDraggable => widget.index != null && widget.isDraggable;
|
||||
}
|
||||
|
||||
class _InsertButton extends StatelessWidget {
|
||||
const _InsertButton({Key? key}) : super(key: key);
|
||||
class InsertRowButton extends StatelessWidget {
|
||||
const InsertRowButton({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -188,20 +196,21 @@ class _InsertButton extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _MenuButton extends StatefulWidget {
|
||||
class RowMenuButton extends StatefulWidget {
|
||||
final VoidCallback openMenu;
|
||||
final bool isDragEnabled;
|
||||
|
||||
const _MenuButton({
|
||||
const RowMenuButton({
|
||||
required this.openMenu,
|
||||
this.isDragEnabled = false,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
State<_MenuButton> createState() => _MenuButtonState();
|
||||
State<RowMenuButton> createState() => _RowMenuButtonState();
|
||||
}
|
||||
|
||||
class _MenuButtonState extends State<_MenuButton> {
|
||||
class _RowMenuButtonState extends State<RowMenuButton> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FlowyIconButton(
|
||||
|
@ -17,7 +17,7 @@ import 'container/card_container.dart';
|
||||
|
||||
/// Edit a database row with card style widget
|
||||
class RowCard<CustomCardData> extends StatefulWidget {
|
||||
final RowPB row;
|
||||
final RowMetaPB rowMeta;
|
||||
final String viewId;
|
||||
final String? groupingFieldId;
|
||||
|
||||
@ -46,7 +46,7 @@ class RowCard<CustomCardData> extends StatefulWidget {
|
||||
final RowCardStyleConfiguration styleConfiguration;
|
||||
|
||||
const RowCard({
|
||||
required this.row,
|
||||
required this.rowMeta,
|
||||
required this.viewId,
|
||||
this.groupingFieldId,
|
||||
required this.isEditing,
|
||||
@ -81,7 +81,7 @@ class _RowCardState<T> extends State<RowCard<T>> {
|
||||
viewId: widget.viewId,
|
||||
groupFieldId: widget.groupingFieldId,
|
||||
isEditing: widget.isEditing,
|
||||
row: widget.row,
|
||||
rowMeta: widget.rowMeta,
|
||||
rowCache: widget.rowCache,
|
||||
)..add(const RowCardEvent.initial());
|
||||
|
||||
@ -178,7 +178,8 @@ class _RowCardState<T> extends State<RowCard<T>> {
|
||||
throw UnimplementedError();
|
||||
case AccessoryType.more:
|
||||
return RowActions(
|
||||
rowData: context.read<CardBloc>().rowInfo(),
|
||||
viewId: context.read<CardBloc>().viewId,
|
||||
rowId: context.read<CardBloc>().rowMeta.id,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -12,24 +12,24 @@ import '../../application/row/row_service.dart';
|
||||
part 'card_bloc.freezed.dart';
|
||||
|
||||
class CardBloc extends Bloc<RowCardEvent, RowCardState> {
|
||||
final RowPB row;
|
||||
final RowMetaPB rowMeta;
|
||||
final String? groupFieldId;
|
||||
final RowBackendService _rowBackendSvc;
|
||||
final RowCache _rowCache;
|
||||
VoidCallback? _rowCallback;
|
||||
final String viewId;
|
||||
|
||||
CardBloc({
|
||||
required this.row,
|
||||
required this.rowMeta,
|
||||
required this.groupFieldId,
|
||||
required String viewId,
|
||||
required this.viewId,
|
||||
required RowCache rowCache,
|
||||
required bool isEditing,
|
||||
}) : _rowBackendSvc = RowBackendService(viewId: viewId),
|
||||
_rowCache = rowCache,
|
||||
super(
|
||||
RowCardState.initial(
|
||||
row,
|
||||
_makeCells(groupFieldId, rowCache.loadGridCells(row.id)),
|
||||
_makeCells(groupFieldId, rowCache.loadGridCells(rowMeta)),
|
||||
isEditing,
|
||||
),
|
||||
) {
|
||||
@ -70,13 +70,14 @@ class CardBloc extends Bloc<RowCardEvent, RowCardState> {
|
||||
fields: UnmodifiableListView(
|
||||
state.cells.map((cell) => cell.fieldInfo).toList(),
|
||||
),
|
||||
rowPB: state.rowPB,
|
||||
rowId: rowMeta.id,
|
||||
rowMeta: rowMeta,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _startListening() async {
|
||||
_rowCallback = _rowCache.addListener(
|
||||
rowId: row.id,
|
||||
rowId: rowMeta.id,
|
||||
onCellUpdated: (cellMap, reason) {
|
||||
if (!isClosed) {
|
||||
final cells = _makeCells(groupFieldId, cellMap);
|
||||
@ -118,19 +119,16 @@ class RowCardEvent with _$RowCardEvent {
|
||||
@freezed
|
||||
class RowCardState with _$RowCardState {
|
||||
const factory RowCardState({
|
||||
required RowPB rowPB,
|
||||
required List<DatabaseCellContext> cells,
|
||||
required bool isEditing,
|
||||
RowsChangedReason? changeReason,
|
||||
}) = _RowCardState;
|
||||
|
||||
factory RowCardState.initial(
|
||||
RowPB rowPB,
|
||||
List<DatabaseCellContext> cells,
|
||||
bool isEditing,
|
||||
) =>
|
||||
RowCardState(
|
||||
rowPB: rowPB,
|
||||
cells: cells,
|
||||
isEditing: isEditing,
|
||||
);
|
||||
|
@ -1,4 +1,3 @@
|
||||
import 'package:appflowy/startup/plugin/plugin.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_ext.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_listener.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
||||
@ -47,26 +46,19 @@ class _DatabaseViewWidgetState extends State<DatabaseViewWidget> {
|
||||
return ValueListenableBuilder<ViewLayoutPB>(
|
||||
valueListenable: _layoutTypeChangeNotifier,
|
||||
builder: (_, __, ___) {
|
||||
return makePlugin(pluginType: view.pluginType, data: view)
|
||||
.widgetBuilder
|
||||
.buildWidget();
|
||||
return view.plugin().widgetBuilder.buildWidget();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _listenOnViewUpdated() {
|
||||
_listener = ViewListener(view: widget.view)
|
||||
_listener = ViewListener(viewId: widget.view.id)
|
||||
..start(
|
||||
onViewUpdated: (result) {
|
||||
result.fold(
|
||||
(updatedView) {
|
||||
if (mounted) {
|
||||
view = updatedView;
|
||||
_layoutTypeChangeNotifier.value = view.layout;
|
||||
}
|
||||
},
|
||||
(r) => null,
|
||||
);
|
||||
onViewUpdated: (updatedView) {
|
||||
if (mounted) {
|
||||
view = updatedView;
|
||||
_layoutTypeChangeNotifier.value = view.layout;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
|
@ -14,6 +14,7 @@ import 'cells/select_option_cell/select_option_cell.dart';
|
||||
import 'cells/text_cell/text_cell.dart';
|
||||
import 'cells/url_cell/url_cell.dart';
|
||||
|
||||
/// Build the cell widget in Grid style.
|
||||
class GridCellBuilder {
|
||||
final CellCache cellCache;
|
||||
GridCellBuilder({
|
||||
|
@ -0,0 +1,7 @@
|
||||
export 'checkbox_cell/checkbox_cell.dart';
|
||||
export 'checklist_cell/checklist_cell.dart';
|
||||
export 'date_cell/date_cell.dart';
|
||||
export 'number_cell/number_cell.dart';
|
||||
export 'select_option_cell/select_option_cell.dart';
|
||||
export 'text_cell/text_cell.dart';
|
||||
export 'url_cell/url_cell.dart';
|
@ -40,8 +40,8 @@ class _CheckboxCellState extends GridCellState<GridCheckboxCell> {
|
||||
child: BlocBuilder<CheckboxCellBloc, CheckboxCellState>(
|
||||
builder: (context, state) {
|
||||
final icon = state.isSelected
|
||||
? svgWidget('editor/editor_check')
|
||||
: svgWidget('editor/editor_uncheck');
|
||||
? const CheckboxCellCheck()
|
||||
: const CheckboxCellUncheck();
|
||||
return Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Padding(
|
||||
@ -82,3 +82,21 @@ class _CheckboxCellState extends GridCellState<GridCheckboxCell> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class CheckboxCellCheck extends StatelessWidget {
|
||||
const CheckboxCellCheck({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return svgWidget('editor/editor_check');
|
||||
}
|
||||
}
|
||||
|
||||
class CheckboxCellUncheck extends StatelessWidget {
|
||||
const CheckboxCellUncheck({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return svgWidget('editor/editor_uncheck');
|
||||
}
|
||||
}
|
||||
|
@ -16,7 +16,9 @@ class ChecklistCardCellBloc
|
||||
ChecklistCardCellBloc({
|
||||
required this.cellController,
|
||||
}) : _checklistCellSvc = ChecklistCellBackendService(
|
||||
cellContext: cellController.cellContext,
|
||||
viewId: cellController.viewId,
|
||||
fieldId: cellController.fieldId,
|
||||
rowId: cellController.rowId,
|
||||
),
|
||||
super(ChecklistCellState.initial(cellController)) {
|
||||
on<ChecklistCellEvent>(
|
||||
|
@ -19,7 +19,9 @@ class ChecklistCellEditorBloc
|
||||
ChecklistCellEditorBloc({
|
||||
required this.cellController,
|
||||
}) : _checklistCellService = ChecklistCellBackendService(
|
||||
cellContext: cellController.cellContext,
|
||||
viewId: cellController.viewId,
|
||||
fieldId: cellController.fieldId,
|
||||
rowId: cellController.rowId,
|
||||
),
|
||||
super(ChecklistCellEditorState.initial(cellController)) {
|
||||
on<ChecklistCellEditorEvent>(
|
||||
|
@ -22,7 +22,7 @@ class ChecklistProgressBar extends StatelessWidget {
|
||||
percent: percent,
|
||||
padding: EdgeInsets.zero,
|
||||
progressColor: percent < 1.0
|
||||
? SelectOptionColorPB.Blue.toColor(context)
|
||||
? SelectOptionColorPB.Purple.toColor(context)
|
||||
: SelectOptionColorPB.Green.toColor(context),
|
||||
backgroundColor: AFThemeExtension.of(context).progressBarBGColor,
|
||||
barRadius: const Radius.circular(5),
|
||||
|
@ -17,7 +17,9 @@ class SelectOptionCellEditorBloc
|
||||
SelectOptionCellEditorBloc({
|
||||
required this.cellController,
|
||||
}) : _selectOptionService = SelectOptionCellBackendService(
|
||||
cellContext: cellController.cellContext,
|
||||
viewId: cellController.viewId,
|
||||
fieldId: cellController.fieldId,
|
||||
rowId: cellController.rowId,
|
||||
),
|
||||
super(SelectOptionEditorState.initial(cellController)) {
|
||||
on<SelectOptionEditorEvent>(
|
||||
|
@ -1,6 +1,7 @@
|
||||
import 'dart:async';
|
||||
import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/row/cells/text_cell/text_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 '../../../../grid/presentation/layout/sizes.dart';
|
||||
@ -10,17 +11,23 @@ class GridTextCellStyle extends GridCellStyle {
|
||||
String? placeholder;
|
||||
TextStyle? textStyle;
|
||||
bool? autofocus;
|
||||
double emojiFontSize;
|
||||
double emojiHPadding;
|
||||
bool showEmoji;
|
||||
|
||||
GridTextCellStyle({
|
||||
this.placeholder,
|
||||
this.textStyle,
|
||||
this.autofocus,
|
||||
this.showEmoji = true,
|
||||
this.emojiFontSize = 16,
|
||||
this.emojiHPadding = 0,
|
||||
});
|
||||
}
|
||||
|
||||
class GridTextCell extends GridCellWidget {
|
||||
final CellControllerBuilder cellControllerBuilder;
|
||||
late final GridTextCellStyle? cellStyle;
|
||||
late final GridTextCellStyle cellStyle;
|
||||
GridTextCell({
|
||||
required this.cellControllerBuilder,
|
||||
GridCellStyle? style,
|
||||
@ -29,7 +36,7 @@ class GridTextCell extends GridCellWidget {
|
||||
if (style != null) {
|
||||
cellStyle = (style as GridTextCellStyle);
|
||||
} else {
|
||||
cellStyle = null;
|
||||
cellStyle = GridTextCellStyle();
|
||||
}
|
||||
}
|
||||
|
||||
@ -66,22 +73,40 @@ class _GridTextCellState extends GridFocusNodeCellState<GridTextCell> {
|
||||
left: GridSize.cellContentInsets.left,
|
||||
right: GridSize.cellContentInsets.right,
|
||||
),
|
||||
child: TextField(
|
||||
controller: _controller,
|
||||
focusNode: focusNode,
|
||||
maxLines: null,
|
||||
style: widget.cellStyle?.textStyle ??
|
||||
Theme.of(context).textTheme.bodyMedium,
|
||||
autofocus: widget.cellStyle?.autofocus ?? false,
|
||||
decoration: InputDecoration(
|
||||
contentPadding: EdgeInsets.only(
|
||||
top: GridSize.cellContentInsets.top,
|
||||
bottom: GridSize.cellContentInsets.bottom,
|
||||
),
|
||||
border: InputBorder.none,
|
||||
hintText: widget.cellStyle?.placeholder,
|
||||
isDense: true,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
if (widget.cellStyle.showEmoji)
|
||||
// Only build the emoji when it changes
|
||||
BlocBuilder<TextCellBloc, TextCellState>(
|
||||
buildWhen: (p, c) => p.emoji != c.emoji,
|
||||
builder: (context, state) => Center(
|
||||
child: FlowyText(
|
||||
state.emoji,
|
||||
fontSize: widget.cellStyle.emojiFontSize,
|
||||
),
|
||||
),
|
||||
),
|
||||
HSpace(widget.cellStyle.emojiHPadding),
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: _controller,
|
||||
focusNode: focusNode,
|
||||
maxLines: null,
|
||||
style: widget.cellStyle.textStyle ??
|
||||
Theme.of(context).textTheme.bodyMedium,
|
||||
autofocus: widget.cellStyle.autofocus ?? false,
|
||||
decoration: InputDecoration(
|
||||
contentPadding: EdgeInsets.only(
|
||||
top: GridSize.cellContentInsets.top,
|
||||
bottom: GridSize.cellContentInsets.bottom,
|
||||
),
|
||||
border: InputBorder.none,
|
||||
hintText: widget.cellStyle.placeholder,
|
||||
isDense: true,
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -26,6 +26,9 @@ class TextCellBloc extends Bloc<TextCellEvent, TextCellState> {
|
||||
didReceiveCellUpdate: (content) {
|
||||
emit(state.copyWith(content: content));
|
||||
},
|
||||
didUpdateEmoji: (String emoji) {
|
||||
emit(state.copyWith(emoji: emoji));
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
@ -48,6 +51,11 @@ class TextCellBloc extends Bloc<TextCellEvent, TextCellState> {
|
||||
add(TextCellEvent.didReceiveCellUpdate(cellContent ?? ""));
|
||||
}
|
||||
}),
|
||||
onRowMetaChanged: () {
|
||||
if (!isClosed) {
|
||||
add(TextCellEvent.didUpdateEmoji(cellController.emoji ?? ""));
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -58,15 +66,18 @@ class TextCellEvent with _$TextCellEvent {
|
||||
const factory TextCellEvent.didReceiveCellUpdate(String cellContent) =
|
||||
_DidReceiveCellUpdate;
|
||||
const factory TextCellEvent.updateText(String text) = _UpdateText;
|
||||
const factory TextCellEvent.didUpdateEmoji(String emoji) = _UpdateEmoji;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class TextCellState with _$TextCellState {
|
||||
const factory TextCellState({
|
||||
required String content,
|
||||
required String emoji,
|
||||
}) = _TextCellState;
|
||||
|
||||
factory TextCellState.initial(TextCellController context) => TextCellState(
|
||||
content: context.getCellData() ?? "",
|
||||
emoji: context.emoji ?? "",
|
||||
);
|
||||
}
|
||||
|
@ -0,0 +1,174 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_context.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_service.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/row/row_data_controller.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/application/row/row_detail_bloc.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_editor.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/image.dart';
|
||||
import 'package:flowy_infra/theme_extension.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class RowActionList extends StatelessWidget {
|
||||
final RowController rowController;
|
||||
const RowActionList({
|
||||
required String viewId,
|
||||
required this.rowController,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 10),
|
||||
child: FlowyText(LocaleKeys.grid_row_action.tr()),
|
||||
),
|
||||
const VSpace(15),
|
||||
RowDetailPageDeleteButton(rowId: rowController.rowId),
|
||||
RowDetailPageDuplicateButton(
|
||||
rowId: rowController.rowId,
|
||||
groupId: rowController.groupId,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class RowDetailPageDeleteButton extends StatelessWidget {
|
||||
final String rowId;
|
||||
const RowDetailPageDeleteButton({required this.rowId, Key? key})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
height: GridSize.popoverItemHeight,
|
||||
child: FlowyButton(
|
||||
text: FlowyText.regular(LocaleKeys.grid_row_delete.tr()),
|
||||
leftIcon: const FlowySvg(name: "home/trash"),
|
||||
onTap: () {
|
||||
context.read<RowDetailBloc>().add(RowDetailEvent.deleteRow(rowId));
|
||||
FlowyOverlay.pop(context);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class RowDetailPageDuplicateButton extends StatelessWidget {
|
||||
final String rowId;
|
||||
final String? groupId;
|
||||
const RowDetailPageDuplicateButton({
|
||||
required this.rowId,
|
||||
this.groupId,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
height: GridSize.popoverItemHeight,
|
||||
child: FlowyButton(
|
||||
text: FlowyText.regular(LocaleKeys.grid_row_duplicate.tr()),
|
||||
leftIcon: const FlowySvg(name: "grid/duplicate"),
|
||||
onTap: () {
|
||||
context
|
||||
.read<RowDetailBloc>()
|
||||
.add(RowDetailEvent.duplicateRow(rowId, groupId));
|
||||
FlowyOverlay.pop(context);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class CreateRowFieldButton extends StatefulWidget {
|
||||
final String viewId;
|
||||
|
||||
const CreateRowFieldButton({
|
||||
required this.viewId,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<CreateRowFieldButton> createState() => _CreateRowFieldButtonState();
|
||||
}
|
||||
|
||||
class _CreateRowFieldButtonState extends State<CreateRowFieldButton> {
|
||||
late PopoverController popoverController;
|
||||
late TypeOptionPB typeOption;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
popoverController = PopoverController();
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AppFlowyPopover(
|
||||
constraints: BoxConstraints.loose(const Size(240, 200)),
|
||||
controller: popoverController,
|
||||
direction: PopoverDirection.topWithLeftAligned,
|
||||
triggerActions: PopoverTriggerFlags.none,
|
||||
margin: EdgeInsets.zero,
|
||||
child: SizedBox(
|
||||
height: 40,
|
||||
child: FlowyButton(
|
||||
text: FlowyText.medium(
|
||||
LocaleKeys.grid_field_newProperty.tr(),
|
||||
color: AFThemeExtension.of(context).textColor,
|
||||
),
|
||||
hoverColor: AFThemeExtension.of(context).lightGreyHover,
|
||||
onTap: () async {
|
||||
final result = await TypeOptionBackendService.createFieldTypeOption(
|
||||
viewId: widget.viewId,
|
||||
);
|
||||
result.fold(
|
||||
(l) {
|
||||
typeOption = l;
|
||||
popoverController.show();
|
||||
},
|
||||
(r) => Log.error("Failed to create field type option: $r"),
|
||||
);
|
||||
},
|
||||
leftIcon: svgWidget(
|
||||
"home/add",
|
||||
color: AFThemeExtension.of(context).textColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
popupBuilder: (BuildContext popOverContext) {
|
||||
return FieldEditor(
|
||||
viewId: widget.viewId,
|
||||
typeOptionLoader: FieldTypeOptionLoader(
|
||||
viewId: widget.viewId,
|
||||
field: typeOption.field_2,
|
||||
),
|
||||
onDeleted: (fieldId) {
|
||||
popoverController.close();
|
||||
NavigatorAlertDialog(
|
||||
title: LocaleKeys.grid_field_deleteFieldPromptMessage.tr(),
|
||||
confirm: () {
|
||||
context
|
||||
.read<RowDetailBloc>()
|
||||
.add(RowDetailEvent.deleteField(fieldId));
|
||||
},
|
||||
).show(context);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,268 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/row/row_banner_bloc.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/emoji_picker/emoji_picker.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/row_entities.pb.dart';
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
typedef RowBannerCellBuilder = Widget Function(String fieldId);
|
||||
|
||||
class RowBanner extends StatefulWidget {
|
||||
final String viewId;
|
||||
final RowMetaPB rowMeta;
|
||||
final RowBannerCellBuilder cellBuilder;
|
||||
const RowBanner({
|
||||
required this.viewId,
|
||||
required this.rowMeta,
|
||||
required this.cellBuilder,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
State<RowBanner> createState() => _RowBannerState();
|
||||
}
|
||||
|
||||
class _RowBannerState extends State<RowBanner> {
|
||||
final _isHovering = ValueNotifier(false);
|
||||
final popoverController = PopoverController();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider<RowBannerBloc>(
|
||||
create: (context) => RowBannerBloc(
|
||||
viewId: widget.viewId,
|
||||
rowMeta: widget.rowMeta,
|
||||
)..add(const RowBannerEvent.initial()),
|
||||
child: MouseRegion(
|
||||
onEnter: (event) => _isHovering.value = true,
|
||||
onExit: (event) => _isHovering.value = false,
|
||||
child: SizedBox(
|
||||
height: 80,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 30,
|
||||
child: _BannerAction(
|
||||
isHovering: _isHovering,
|
||||
popoverController: popoverController,
|
||||
),
|
||||
),
|
||||
_BannerTitle(
|
||||
cellBuilder: widget.cellBuilder,
|
||||
popoverController: popoverController,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _BannerAction extends StatelessWidget {
|
||||
final ValueNotifier<bool> isHovering;
|
||||
final PopoverController popoverController;
|
||||
const _BannerAction({
|
||||
required this.isHovering,
|
||||
required this.popoverController,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ValueListenableBuilder(
|
||||
valueListenable: isHovering,
|
||||
builder: (BuildContext context, bool value, Widget? child) {
|
||||
if (value) {
|
||||
return BlocBuilder<RowBannerBloc, RowBannerState>(
|
||||
builder: (context, state) {
|
||||
final children = <Widget>[];
|
||||
final rowMeta = state.rowMeta;
|
||||
if (rowMeta.icon.isEmpty) {
|
||||
children.add(
|
||||
EmojiPickerButton(
|
||||
showEmojiPicker: () => popoverController.show(),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
children.add(
|
||||
RemoveEmojiButton(
|
||||
onRemoved: () {
|
||||
context
|
||||
.read<RowBannerBloc>()
|
||||
.add(const RowBannerEvent.setIcon(''));
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: children,
|
||||
);
|
||||
},
|
||||
);
|
||||
} else {
|
||||
return const SizedBox(height: _kBannerActionHeight);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _BannerTitle extends StatefulWidget {
|
||||
final RowBannerCellBuilder cellBuilder;
|
||||
final PopoverController popoverController;
|
||||
const _BannerTitle({
|
||||
required this.cellBuilder,
|
||||
required this.popoverController,
|
||||
});
|
||||
|
||||
@override
|
||||
State<_BannerTitle> createState() => _BannerTitleState();
|
||||
}
|
||||
|
||||
class _BannerTitleState extends State<_BannerTitle> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<RowBannerBloc, RowBannerState>(
|
||||
builder: (context, state) {
|
||||
final children = <Widget>[];
|
||||
|
||||
if (state.rowMeta.icon.isNotEmpty) {
|
||||
children.add(
|
||||
EmojiButton(
|
||||
emoji: state.rowMeta.icon,
|
||||
showEmojiPicker: () => widget.popoverController.show(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (state.primaryField != null) {
|
||||
children.add(
|
||||
Expanded(
|
||||
child: widget.cellBuilder(state.primaryField!.id),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return AppFlowyPopover(
|
||||
controller: widget.popoverController,
|
||||
triggerActions: PopoverTriggerFlags.none,
|
||||
direction: PopoverDirection.bottomWithLeftAligned,
|
||||
popupBuilder: (popoverContext) => _buildEmojiPicker((emoji) {
|
||||
context
|
||||
.read<RowBannerBloc>()
|
||||
.add(RowBannerEvent.setIcon(emoji.emoji));
|
||||
widget.popoverController.close();
|
||||
}),
|
||||
child: Row(children: children),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
typedef OnSubmittedEmoji = void Function(Emoji emoji);
|
||||
const _kBannerActionHeight = 40.0;
|
||||
|
||||
class EmojiButton extends StatelessWidget {
|
||||
final String emoji;
|
||||
final VoidCallback showEmojiPicker;
|
||||
|
||||
const EmojiButton({
|
||||
required this.emoji,
|
||||
required this.showEmojiPicker,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
height: _kBannerActionHeight,
|
||||
width: _kBannerActionHeight,
|
||||
child: FlowyButton(
|
||||
margin: const EdgeInsets.all(4),
|
||||
text: FlowyText.medium(
|
||||
emoji,
|
||||
fontSize: 30,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
onTap: showEmojiPicker,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class EmojiPickerButton extends StatefulWidget {
|
||||
final VoidCallback showEmojiPicker;
|
||||
const EmojiPickerButton({
|
||||
super.key,
|
||||
required this.showEmojiPicker,
|
||||
});
|
||||
|
||||
@override
|
||||
State<EmojiPickerButton> createState() => _EmojiPickerButtonState();
|
||||
}
|
||||
|
||||
class _EmojiPickerButtonState extends State<EmojiPickerButton> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
height: 26,
|
||||
width: 160,
|
||||
child: FlowyButton(
|
||||
text: FlowyText.medium(
|
||||
LocaleKeys.document_plugins_cover_addIcon.tr(),
|
||||
),
|
||||
leftIcon: const Icon(
|
||||
Icons.emoji_emotions,
|
||||
size: 16,
|
||||
),
|
||||
onTap: widget.showEmojiPicker,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class RemoveEmojiButton extends StatelessWidget {
|
||||
final VoidCallback onRemoved;
|
||||
RemoveEmojiButton({
|
||||
super.key,
|
||||
required this.onRemoved,
|
||||
});
|
||||
|
||||
final popoverController = PopoverController();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
height: 26,
|
||||
width: 160,
|
||||
child: FlowyButton(
|
||||
text: FlowyText.medium(
|
||||
LocaleKeys.document_plugins_cover_removeIcon.tr(),
|
||||
),
|
||||
leftIcon: const Icon(
|
||||
Icons.emoji_emotions,
|
||||
size: 16,
|
||||
),
|
||||
onTap: onRemoved,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildEmojiPicker(OnSubmittedEmoji onSubmitted) {
|
||||
return SizedBox(
|
||||
height: 250,
|
||||
child: EmojiSelectionMenu(
|
||||
onSubmitted: onSubmitted,
|
||||
onExit: () {},
|
||||
),
|
||||
);
|
||||
}
|
@ -1,31 +1,20 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/cell/cell_service.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_context.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_service.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/row/row_data_controller.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/application/row/row_detail_bloc.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/row/row_document.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flowy_infra/theme_extension.dart';
|
||||
import 'package:flowy_infra/image.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
|
||||
import '../../grid/presentation/layout/sizes.dart';
|
||||
import 'accessory/cell_accessory.dart';
|
||||
import 'cell_builder.dart';
|
||||
import 'cells/date_cell/date_cell.dart';
|
||||
import 'cells/select_option_cell/select_option_cell.dart';
|
||||
import 'cells/text_cell/text_cell.dart';
|
||||
import 'cells/url_cell/url_cell.dart';
|
||||
import '../../grid/presentation/widgets/header/field_cell.dart';
|
||||
import '../../grid/presentation/widgets/header/field_editor.dart';
|
||||
import 'row_action.dart';
|
||||
import 'row_banner.dart';
|
||||
import 'row_property.dart';
|
||||
|
||||
class RowDetailPage extends StatefulWidget with FlowyOverlayDelegate {
|
||||
final RowController rowController;
|
||||
@ -46,6 +35,14 @@ class RowDetailPage extends StatefulWidget with FlowyOverlayDelegate {
|
||||
}
|
||||
|
||||
class _RowDetailPageState extends State<RowDetailPage> {
|
||||
final scrollController = ScrollController();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
scrollController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FlowyDialog(
|
||||
@ -55,43 +52,91 @@ class _RowDetailPageState extends State<RowDetailPage> {
|
||||
..add(const RowDetailEvent.initial());
|
||||
},
|
||||
child: ListView(
|
||||
controller: scrollController,
|
||||
children: [
|
||||
// using ListView here for future expansion:
|
||||
// - header and cover image
|
||||
// - lower rich text area
|
||||
_rowBanner(),
|
||||
IntrinsicHeight(child: _responsiveRowInfo()),
|
||||
const Divider(height: 1.0),
|
||||
const SizedBox(height: 10),
|
||||
const VSpace(10),
|
||||
RowDocument(
|
||||
viewId: widget.rowController.viewId,
|
||||
rowId: widget.rowController.rowId,
|
||||
scrollController: scrollController,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _rowBanner() {
|
||||
return BlocBuilder<RowDetailBloc, RowDetailState>(
|
||||
builder: (context, state) {
|
||||
final paddingOffset = getHorizontalPadding(context);
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(
|
||||
left: paddingOffset,
|
||||
right: paddingOffset,
|
||||
top: 20,
|
||||
),
|
||||
child: RowBanner(
|
||||
rowMeta: widget.rowController.rowMeta,
|
||||
viewId: widget.rowController.viewId,
|
||||
cellBuilder: (fieldId) {
|
||||
final fieldInfo = state.cells
|
||||
.firstWhereOrNull(
|
||||
(e) => e.fieldInfo.field.id == fieldId,
|
||||
)
|
||||
?.fieldInfo;
|
||||
|
||||
if (fieldInfo != null) {
|
||||
final style = GridTextCellStyle(
|
||||
placeholder: LocaleKeys.grid_row_textPlaceholder.tr(),
|
||||
textStyle: Theme.of(context).textTheme.titleLarge,
|
||||
showEmoji: false,
|
||||
autofocus: true,
|
||||
);
|
||||
final cellContext = DatabaseCellContext(
|
||||
viewId: widget.rowController.viewId,
|
||||
rowMeta: widget.rowController.rowMeta,
|
||||
fieldInfo: fieldInfo,
|
||||
);
|
||||
return widget.cellBuilder.build(cellContext, style: style);
|
||||
} else {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _responsiveRowInfo() {
|
||||
final rowDataColumn = _PropertyColumn(
|
||||
final rowDataColumn = RowPropertyList(
|
||||
cellBuilder: widget.cellBuilder,
|
||||
viewId: widget.rowController.viewId,
|
||||
);
|
||||
final rowOptionColumn = _RowOptionColumn(
|
||||
final rowOptionColumn = RowActionList(
|
||||
viewId: widget.rowController.viewId,
|
||||
rowController: widget.rowController,
|
||||
);
|
||||
final paddingOffset = getHorizontalPadding(context);
|
||||
if (MediaQuery.of(context).size.width > 800) {
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Flexible(
|
||||
flex: 4,
|
||||
flex: 3,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(50, 50, 20, 20),
|
||||
padding: EdgeInsets.fromLTRB(paddingOffset, 0, 20, 20),
|
||||
child: rowDataColumn,
|
||||
),
|
||||
),
|
||||
const VerticalDivider(width: 1.0),
|
||||
Flexible(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(20, 50, 20, 20),
|
||||
padding: EdgeInsets.fromLTRB(20, 0, paddingOffset, 0),
|
||||
child: rowOptionColumn,
|
||||
),
|
||||
),
|
||||
@ -103,12 +148,12 @@ class _RowDetailPageState extends State<RowDetailPage> {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(20, 50, 20, 20),
|
||||
padding: EdgeInsets.fromLTRB(paddingOffset, 0, 20, 20),
|
||||
child: rowDataColumn,
|
||||
),
|
||||
const Divider(height: 1.0),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
padding: EdgeInsets.symmetric(horizontal: paddingOffset),
|
||||
child: rowOptionColumn,
|
||||
)
|
||||
],
|
||||
@ -117,352 +162,10 @@ class _RowDetailPageState extends State<RowDetailPage> {
|
||||
}
|
||||
}
|
||||
|
||||
class _PropertyColumn extends StatelessWidget {
|
||||
final String viewId;
|
||||
final GridCellBuilder cellBuilder;
|
||||
const _PropertyColumn({
|
||||
required this.viewId,
|
||||
required this.cellBuilder,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<RowDetailBloc, RowDetailState>(
|
||||
buildWhen: (previous, current) => previous.gridCells != current.gridCells,
|
||||
builder: (context, state) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_RowTitle(
|
||||
cellContext: state.gridCells
|
||||
.firstWhereOrNull((e) => e.fieldInfo.isPrimary),
|
||||
cellBuilder: cellBuilder,
|
||||
),
|
||||
const VSpace(20),
|
||||
...state.gridCells
|
||||
.where((element) => !element.fieldInfo.isPrimary)
|
||||
.map(
|
||||
(cell) => Padding(
|
||||
padding: const EdgeInsets.only(bottom: 4.0),
|
||||
child: _PropertyCell(
|
||||
cellContext: cell,
|
||||
cellBuilder: cellBuilder,
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
const VSpace(20),
|
||||
_CreatePropertyButton(viewId: viewId),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _RowTitle extends StatelessWidget {
|
||||
final DatabaseCellContext? cellContext;
|
||||
final GridCellBuilder cellBuilder;
|
||||
const _RowTitle({this.cellContext, required this.cellBuilder, Key? key})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (cellContext == null) {
|
||||
return const SizedBox();
|
||||
}
|
||||
final style = GridTextCellStyle(
|
||||
placeholder: LocaleKeys.grid_row_textPlaceholder.tr(),
|
||||
textStyle: Theme.of(context).textTheme.titleLarge,
|
||||
autofocus: true,
|
||||
);
|
||||
return cellBuilder.build(cellContext!, style: style);
|
||||
}
|
||||
}
|
||||
|
||||
class _CreatePropertyButton extends StatefulWidget {
|
||||
final String viewId;
|
||||
|
||||
const _CreatePropertyButton({
|
||||
required this.viewId,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<_CreatePropertyButton> createState() => _CreatePropertyButtonState();
|
||||
}
|
||||
|
||||
class _CreatePropertyButtonState extends State<_CreatePropertyButton> {
|
||||
late PopoverController popoverController;
|
||||
late TypeOptionPB typeOption;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
popoverController = PopoverController();
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AppFlowyPopover(
|
||||
constraints: BoxConstraints.loose(const Size(240, 200)),
|
||||
controller: popoverController,
|
||||
direction: PopoverDirection.topWithLeftAligned,
|
||||
triggerActions: PopoverTriggerFlags.none,
|
||||
margin: EdgeInsets.zero,
|
||||
child: SizedBox(
|
||||
height: 40,
|
||||
child: FlowyButton(
|
||||
text: FlowyText.medium(
|
||||
LocaleKeys.grid_field_newProperty.tr(),
|
||||
color: AFThemeExtension.of(context).textColor,
|
||||
),
|
||||
hoverColor: AFThemeExtension.of(context).lightGreyHover,
|
||||
onTap: () async {
|
||||
final result = await TypeOptionBackendService.createFieldTypeOption(
|
||||
viewId: widget.viewId,
|
||||
);
|
||||
result.fold(
|
||||
(l) {
|
||||
typeOption = l;
|
||||
popoverController.show();
|
||||
},
|
||||
(r) => Log.error("Failed to create field type option: $r"),
|
||||
);
|
||||
},
|
||||
leftIcon: svgWidget(
|
||||
"home/add",
|
||||
color: AFThemeExtension.of(context).textColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
popupBuilder: (BuildContext popOverContext) {
|
||||
return FieldEditor(
|
||||
viewId: widget.viewId,
|
||||
typeOptionLoader: FieldTypeOptionLoader(
|
||||
viewId: widget.viewId,
|
||||
field: typeOption.field_2,
|
||||
),
|
||||
onDeleted: (fieldId) {
|
||||
popoverController.close();
|
||||
NavigatorAlertDialog(
|
||||
title: LocaleKeys.grid_field_deleteFieldPromptMessage.tr(),
|
||||
confirm: () {
|
||||
context
|
||||
.read<RowDetailBloc>()
|
||||
.add(RowDetailEvent.deleteField(fieldId));
|
||||
},
|
||||
).show(context);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PropertyCell extends StatefulWidget {
|
||||
final DatabaseCellContext cellContext;
|
||||
final GridCellBuilder cellBuilder;
|
||||
const _PropertyCell({
|
||||
required this.cellContext,
|
||||
required this.cellBuilder,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _PropertyCellState();
|
||||
}
|
||||
|
||||
class _PropertyCellState extends State<_PropertyCell> {
|
||||
final PopoverController popover = PopoverController();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final style = _customCellStyle(widget.cellContext.fieldType);
|
||||
final cell = widget.cellBuilder.build(widget.cellContext, style: style);
|
||||
|
||||
final gesture = GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onTap: () => cell.beginFocus.notify(),
|
||||
child: AccessoryHover(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 3, vertical: 3),
|
||||
child: cell,
|
||||
),
|
||||
);
|
||||
|
||||
return IntrinsicHeight(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(minHeight: 30),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
AppFlowyPopover(
|
||||
controller: popover,
|
||||
constraints: BoxConstraints.loose(const Size(240, 600)),
|
||||
margin: EdgeInsets.zero,
|
||||
triggerActions: PopoverTriggerFlags.none,
|
||||
popupBuilder: (popoverContext) => buildFieldEditor(),
|
||||
child: SizedBox(
|
||||
width: 150,
|
||||
child: FieldCellButton(
|
||||
field: widget.cellContext.fieldInfo.field,
|
||||
onTap: () => popover.show(),
|
||||
radius: BorderRadius.circular(6),
|
||||
),
|
||||
),
|
||||
),
|
||||
const HSpace(10),
|
||||
Expanded(child: gesture),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildFieldEditor() {
|
||||
return FieldEditor(
|
||||
viewId: widget.cellContext.viewId,
|
||||
isGroupingField: widget.cellContext.fieldInfo.isGroupField,
|
||||
typeOptionLoader: FieldTypeOptionLoader(
|
||||
viewId: widget.cellContext.viewId,
|
||||
field: widget.cellContext.fieldInfo.field,
|
||||
),
|
||||
onHidden: (fieldId) {
|
||||
popover.close();
|
||||
context.read<RowDetailBloc>().add(RowDetailEvent.hideField(fieldId));
|
||||
},
|
||||
onDeleted: (fieldId) {
|
||||
popover.close();
|
||||
|
||||
NavigatorAlertDialog(
|
||||
title: LocaleKeys.grid_field_deleteFieldPromptMessage.tr(),
|
||||
confirm: () {
|
||||
context
|
||||
.read<RowDetailBloc>()
|
||||
.add(RowDetailEvent.deleteField(fieldId));
|
||||
},
|
||||
).show(context);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
GridCellStyle? _customCellStyle(FieldType fieldType) {
|
||||
switch (fieldType) {
|
||||
case FieldType.Checkbox:
|
||||
return null;
|
||||
case FieldType.DateTime:
|
||||
case FieldType.LastEditedTime:
|
||||
case FieldType.CreatedTime:
|
||||
return DateCellStyle(
|
||||
alignment: Alignment.centerLeft,
|
||||
);
|
||||
case FieldType.MultiSelect:
|
||||
return SelectOptionCellStyle(
|
||||
placeholder: LocaleKeys.grid_row_textPlaceholder.tr(),
|
||||
);
|
||||
case FieldType.Checklist:
|
||||
return SelectOptionCellStyle(
|
||||
placeholder: LocaleKeys.grid_row_textPlaceholder.tr(),
|
||||
);
|
||||
case FieldType.Number:
|
||||
return null;
|
||||
case FieldType.RichText:
|
||||
return GridTextCellStyle(
|
||||
placeholder: LocaleKeys.grid_row_textPlaceholder.tr(),
|
||||
);
|
||||
case FieldType.SingleSelect:
|
||||
return SelectOptionCellStyle(
|
||||
placeholder: LocaleKeys.grid_row_textPlaceholder.tr(),
|
||||
);
|
||||
|
||||
case FieldType.URL:
|
||||
return GridURLCellStyle(
|
||||
placeholder: LocaleKeys.grid_row_textPlaceholder.tr(),
|
||||
accessoryTypes: [
|
||||
GridURLCellAccessoryType.copyURL,
|
||||
GridURLCellAccessoryType.visitURL,
|
||||
],
|
||||
);
|
||||
}
|
||||
throw UnimplementedError;
|
||||
}
|
||||
|
||||
class _RowOptionColumn extends StatelessWidget {
|
||||
final RowController rowController;
|
||||
const _RowOptionColumn({
|
||||
required String viewId,
|
||||
required this.rowController,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 10),
|
||||
child: FlowyText(LocaleKeys.grid_row_action.tr()),
|
||||
),
|
||||
const VSpace(15),
|
||||
_DeleteButton(rowId: rowController.rowId),
|
||||
_DuplicateButton(
|
||||
rowId: rowController.rowId,
|
||||
groupId: rowController.groupId,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _DeleteButton extends StatelessWidget {
|
||||
final String rowId;
|
||||
const _DeleteButton({required this.rowId, Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
height: GridSize.popoverItemHeight,
|
||||
child: FlowyButton(
|
||||
text: FlowyText.regular(LocaleKeys.grid_row_delete.tr()),
|
||||
leftIcon: const FlowySvg(name: "home/trash"),
|
||||
onTap: () {
|
||||
context.read<RowDetailBloc>().add(RowDetailEvent.deleteRow(rowId));
|
||||
FlowyOverlay.pop(context);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _DuplicateButton extends StatelessWidget {
|
||||
final String rowId;
|
||||
final String? groupId;
|
||||
const _DuplicateButton({
|
||||
required this.rowId,
|
||||
this.groupId,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
height: GridSize.popoverItemHeight,
|
||||
child: FlowyButton(
|
||||
text: FlowyText.regular(LocaleKeys.grid_row_duplicate.tr()),
|
||||
leftIcon: const FlowySvg(name: "grid/duplicate"),
|
||||
onTap: () {
|
||||
context
|
||||
.read<RowDetailBloc>()
|
||||
.add(RowDetailEvent.duplicateRow(rowId, groupId));
|
||||
FlowyOverlay.pop(context);
|
||||
},
|
||||
),
|
||||
);
|
||||
double getHorizontalPadding(BuildContext context) {
|
||||
if (MediaQuery.of(context).size.width > 800) {
|
||||
return 50;
|
||||
} else {
|
||||
return 20;
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,120 @@
|
||||
import 'package:appflowy/plugins/database_view/grid/application/row/row_document_bloc.dart';
|
||||
import 'package:appflowy/plugins/document/application/doc_bloc.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_page.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/more/cubit/document_appearance_cubit.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
||||
import 'package:flowy_infra_ui/widget/error_page.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class RowDocument extends StatelessWidget {
|
||||
const RowDocument({
|
||||
super.key,
|
||||
required this.viewId,
|
||||
required this.rowId,
|
||||
required this.scrollController,
|
||||
});
|
||||
|
||||
final String viewId;
|
||||
final String rowId;
|
||||
final ScrollController scrollController;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider<RowDocumentBloc>(
|
||||
create: (context) => RowDocumentBloc(
|
||||
viewId: viewId,
|
||||
rowId: rowId,
|
||||
)..add(
|
||||
const RowDocumentEvent.initial(),
|
||||
),
|
||||
child: BlocBuilder<RowDocumentBloc, RowDocumentState>(
|
||||
builder: (context, state) {
|
||||
return state.loadingState.when(
|
||||
loading: () => const Center(
|
||||
child: CircularProgressIndicator.adaptive(),
|
||||
),
|
||||
error: (error) => FlowyErrorPage(
|
||||
error.toString(),
|
||||
),
|
||||
finish: () => RowEditor(
|
||||
viewPB: state.viewPB!,
|
||||
scrollController: scrollController,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class RowEditor extends StatefulWidget {
|
||||
const RowEditor({
|
||||
super.key,
|
||||
required this.viewPB,
|
||||
required this.scrollController,
|
||||
});
|
||||
|
||||
final ViewPB viewPB;
|
||||
final ScrollController scrollController;
|
||||
|
||||
@override
|
||||
State<RowEditor> createState() => _RowEditorState();
|
||||
}
|
||||
|
||||
class _RowEditorState extends State<RowEditor> {
|
||||
late final DocumentBloc documentBloc;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
documentBloc = DocumentBloc(view: widget.viewPB)
|
||||
..add(const DocumentEvent.initial());
|
||||
}
|
||||
|
||||
@override
|
||||
dispose() {
|
||||
documentBloc.close();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider(create: (_) => DocumentAppearanceCubit()),
|
||||
BlocProvider.value(value: documentBloc),
|
||||
],
|
||||
child: BlocBuilder<DocumentBloc, DocumentState>(
|
||||
builder: (context, state) {
|
||||
return state.loadingState.when(
|
||||
loading: () => const Center(
|
||||
child: CircularProgressIndicator.adaptive(),
|
||||
),
|
||||
finish: (result) {
|
||||
return result.fold(
|
||||
(error) => FlowyErrorPage(
|
||||
error.toString(),
|
||||
),
|
||||
(_) {
|
||||
final editorState = documentBloc.editorState;
|
||||
if (editorState == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
return IntrinsicHeight(
|
||||
child: AppFlowyEditorPage(
|
||||
shrinkWrap: true,
|
||||
autoFocus: false,
|
||||
editorState: editorState,
|
||||
scrollController: widget.scrollController,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,192 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/cell/cell_service.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_context.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/application/row/row_detail_bloc.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_cell.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_editor.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/row/row_action.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import 'accessory/cell_accessory.dart';
|
||||
import 'cell_builder.dart';
|
||||
import 'cells/date_cell/date_cell.dart';
|
||||
import 'cells/select_option_cell/select_option_cell.dart';
|
||||
import 'cells/text_cell/text_cell.dart';
|
||||
import 'cells/url_cell/url_cell.dart';
|
||||
|
||||
/// Display the row properties in a list. Only use this widget in the
|
||||
/// [RowDetailPage].
|
||||
///
|
||||
class RowPropertyList extends StatelessWidget {
|
||||
final String viewId;
|
||||
final GridCellBuilder cellBuilder;
|
||||
const RowPropertyList({
|
||||
required this.viewId,
|
||||
required this.cellBuilder,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<RowDetailBloc, RowDetailState>(
|
||||
buildWhen: (previous, current) => previous.cells != current.cells,
|
||||
builder: (context, state) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// The rest of the fields are displayed in the order of the field
|
||||
// list
|
||||
...state.cells
|
||||
.where((element) => !element.fieldInfo.isPrimary)
|
||||
.map(
|
||||
(cell) => _PropertyCell(
|
||||
cellContext: cell,
|
||||
cellBuilder: cellBuilder,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
const VSpace(20),
|
||||
|
||||
// Create a new property(field) button
|
||||
CreateRowFieldButton(viewId: viewId),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PropertyCell extends StatefulWidget {
|
||||
final DatabaseCellContext cellContext;
|
||||
final GridCellBuilder cellBuilder;
|
||||
const _PropertyCell({
|
||||
required this.cellContext,
|
||||
required this.cellBuilder,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _PropertyCellState();
|
||||
}
|
||||
|
||||
class _PropertyCellState extends State<_PropertyCell> {
|
||||
final PopoverController popover = PopoverController();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final style = _customCellStyle(widget.cellContext.fieldType);
|
||||
final cell = widget.cellBuilder.build(widget.cellContext, style: style);
|
||||
|
||||
final gesture = GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onTap: () => cell.beginFocus.notify(),
|
||||
child: AccessoryHover(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 3, vertical: 3),
|
||||
child: cell,
|
||||
),
|
||||
);
|
||||
|
||||
return IntrinsicHeight(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(minHeight: 30),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
AppFlowyPopover(
|
||||
controller: popover,
|
||||
constraints: BoxConstraints.loose(const Size(240, 600)),
|
||||
margin: EdgeInsets.zero,
|
||||
triggerActions: PopoverTriggerFlags.none,
|
||||
popupBuilder: (popoverContext) => buildFieldEditor(),
|
||||
child: SizedBox(
|
||||
width: 150,
|
||||
child: FieldCellButton(
|
||||
field: widget.cellContext.fieldInfo.field,
|
||||
onTap: () => popover.show(),
|
||||
radius: BorderRadius.circular(6),
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(child: gesture),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildFieldEditor() {
|
||||
return FieldEditor(
|
||||
viewId: widget.cellContext.viewId,
|
||||
isGroupingField: widget.cellContext.fieldInfo.isGroupField,
|
||||
typeOptionLoader: FieldTypeOptionLoader(
|
||||
viewId: widget.cellContext.viewId,
|
||||
field: widget.cellContext.fieldInfo.field,
|
||||
),
|
||||
onHidden: (fieldId) {
|
||||
popover.close();
|
||||
context.read<RowDetailBloc>().add(RowDetailEvent.hideField(fieldId));
|
||||
},
|
||||
onDeleted: (fieldId) {
|
||||
popover.close();
|
||||
|
||||
NavigatorAlertDialog(
|
||||
title: LocaleKeys.grid_field_deleteFieldPromptMessage.tr(),
|
||||
confirm: () {
|
||||
context
|
||||
.read<RowDetailBloc>()
|
||||
.add(RowDetailEvent.deleteField(fieldId));
|
||||
},
|
||||
).show(context);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
GridCellStyle? _customCellStyle(FieldType fieldType) {
|
||||
switch (fieldType) {
|
||||
case FieldType.Checkbox:
|
||||
return null;
|
||||
case FieldType.DateTime:
|
||||
case FieldType.LastEditedTime:
|
||||
case FieldType.CreatedTime:
|
||||
return DateCellStyle(
|
||||
alignment: Alignment.centerLeft,
|
||||
);
|
||||
case FieldType.MultiSelect:
|
||||
return SelectOptionCellStyle(
|
||||
placeholder: LocaleKeys.grid_row_textPlaceholder.tr(),
|
||||
);
|
||||
case FieldType.Checklist:
|
||||
return SelectOptionCellStyle(
|
||||
placeholder: LocaleKeys.grid_row_textPlaceholder.tr(),
|
||||
);
|
||||
case FieldType.Number:
|
||||
return null;
|
||||
case FieldType.RichText:
|
||||
return GridTextCellStyle(
|
||||
placeholder: LocaleKeys.grid_row_textPlaceholder.tr(),
|
||||
);
|
||||
case FieldType.SingleSelect:
|
||||
return SelectOptionCellStyle(
|
||||
placeholder: LocaleKeys.grid_row_textPlaceholder.tr(),
|
||||
);
|
||||
|
||||
case FieldType.URL:
|
||||
return GridURLCellStyle(
|
||||
placeholder: LocaleKeys.grid_row_textPlaceholder.tr(),
|
||||
accessoryTypes: [
|
||||
GridURLCellAccessoryType.copyURL,
|
||||
GridURLCellAccessoryType.visitURL,
|
||||
],
|
||||
);
|
||||
}
|
||||
throw UnimplementedError;
|
||||
}
|
@ -23,7 +23,7 @@ class DatabaseSettingList extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
final cells = actionsForDatabaseLayout(databaseContoller.databaseLayout)
|
||||
.map((action) {
|
||||
return _SettingItem(
|
||||
return DatabaseSettingItem(
|
||||
action: action,
|
||||
onAction: (action) => onAction(action, databaseContoller),
|
||||
);
|
||||
@ -44,11 +44,11 @@ class DatabaseSettingList extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _SettingItem extends StatelessWidget {
|
||||
class DatabaseSettingItem extends StatelessWidget {
|
||||
final DatabaseSettingAction action;
|
||||
final Function(DatabaseSettingAction) onAction;
|
||||
|
||||
const _SettingItem({
|
||||
const DatabaseSettingItem({
|
||||
required this.action,
|
||||
required this.onAction,
|
||||
Key? key,
|
||||
|
@ -23,7 +23,7 @@ class DocumentBloc extends Bloc<DocumentEvent, DocumentState> {
|
||||
DocumentBloc({
|
||||
required this.view,
|
||||
}) : _documentListener = DocumentListener(id: view.id),
|
||||
_viewListener = ViewListener(view: view),
|
||||
_viewListener = ViewListener(viewId: view.id),
|
||||
_documentService = DocumentService(),
|
||||
_trashService = TrashService(),
|
||||
super(DocumentState.initial()) {
|
||||
|
@ -48,8 +48,12 @@ class DocumentPlugin extends Plugin<int> {
|
||||
DocumentPlugin({
|
||||
required PluginType pluginType,
|
||||
required ViewPB view,
|
||||
bool listenOnViewChanged = false,
|
||||
Key? key,
|
||||
}) : notifier = ViewPluginNotifier(view: view) {
|
||||
}) : notifier = ViewPluginNotifier(
|
||||
view: view,
|
||||
listenOnViewChanged: listenOnViewChanged,
|
||||
) {
|
||||
_pluginType = pluginType;
|
||||
_documentAppearanceCubit.fetch();
|
||||
}
|
||||
|
@ -14,17 +14,23 @@ class AppFlowyEditorPage extends StatefulWidget {
|
||||
super.key,
|
||||
required this.editorState,
|
||||
this.header,
|
||||
this.shrinkWrap = false,
|
||||
this.scrollController,
|
||||
this.autoFocus,
|
||||
});
|
||||
|
||||
final EditorState editorState;
|
||||
final Widget? header;
|
||||
final EditorState editorState;
|
||||
final ScrollController? scrollController;
|
||||
final bool shrinkWrap;
|
||||
final bool? autoFocus;
|
||||
|
||||
@override
|
||||
State<AppFlowyEditorPage> createState() => _AppFlowyEditorPageState();
|
||||
}
|
||||
|
||||
class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
|
||||
final scrollController = ScrollController();
|
||||
late final ScrollController effectiveScrollController;
|
||||
|
||||
final List<CommandShortcutEvent> commandShortcutEvents = [
|
||||
...codeBlockCommands,
|
||||
@ -90,6 +96,20 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
|
||||
);
|
||||
DocumentBloc get documentBloc => context.read<DocumentBloc>();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
effectiveScrollController = widget.scrollController ?? ScrollController();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
if (widget.scrollController == null) {
|
||||
effectiveScrollController.dispose();
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final (bool autoFocus, Selection? selection) =
|
||||
@ -98,9 +118,10 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
|
||||
final editor = AppFlowyEditor.custom(
|
||||
editorState: widget.editorState,
|
||||
editable: true,
|
||||
scrollController: scrollController,
|
||||
shrinkWrap: widget.shrinkWrap,
|
||||
scrollController: effectiveScrollController,
|
||||
// setup the auto focus parameters
|
||||
autoFocus: autoFocus,
|
||||
autoFocus: widget.autoFocus ?? autoFocus,
|
||||
focusedSelection: selection,
|
||||
// setup the theme
|
||||
editorStyle: styleCustomizer.style(),
|
||||
@ -122,7 +143,7 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
|
||||
style: styleCustomizer.floatingToolbarStyleBuilder(),
|
||||
items: toolbarItems,
|
||||
editorState: widget.editorState,
|
||||
scrollController: scrollController,
|
||||
scrollController: effectiveScrollController,
|
||||
child: editor,
|
||||
),
|
||||
),
|
||||
|
@ -61,6 +61,7 @@ extension InsertDatabase on EditorState {
|
||||
).then((value) => value.swap().toOption().toNullable());
|
||||
|
||||
// TODO(a-wallen): Show error dialog here.
|
||||
// Maybe extend the FlowyErrorPage.
|
||||
if (ref == null) {
|
||||
return;
|
||||
}
|
||||
|
@ -84,7 +84,7 @@ class ShareActionList extends StatefulWidget {
|
||||
@visibleForTesting
|
||||
class ShareActionListState extends State<ShareActionList> {
|
||||
late String name;
|
||||
late final ViewListener viewListener = ViewListener(view: widget.view);
|
||||
late final ViewListener viewListener = ViewListener(viewId: widget.view.id);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@ -134,7 +134,7 @@ class ShareActionListState extends State<ShareActionList> {
|
||||
name = widget.view.name;
|
||||
viewListener.start(
|
||||
onViewUpdated: (view) {
|
||||
name = view.fold((l) => l.name, (r) => '');
|
||||
name = view.name;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
@ -1,10 +1,14 @@
|
||||
import 'package:appflowy/startup/plugin/plugin.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_ext.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_listener.dart';
|
||||
import 'package:dartz/dartz.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../workspace/presentation/home/home_stack.dart';
|
||||
|
||||
class ViewPluginNotifier extends PluginNotifier<Option<DeletedViewPB>> {
|
||||
final ViewListener? _viewListener;
|
||||
ViewPB view;
|
||||
@ -12,35 +16,37 @@ class ViewPluginNotifier extends PluginNotifier<Option<DeletedViewPB>> {
|
||||
@override
|
||||
final ValueNotifier<Option<DeletedViewPB>> isDeleted = ValueNotifier(none());
|
||||
|
||||
@override
|
||||
final ValueNotifier<int> isDisplayChanged = ValueNotifier(0);
|
||||
|
||||
ViewPluginNotifier({
|
||||
required this.view,
|
||||
}) : _viewListener = ViewListener(view: view) {
|
||||
_viewListener?.start(
|
||||
onViewUpdated: (result) {
|
||||
result.fold(
|
||||
(updatedView) {
|
||||
required bool listenOnViewChanged,
|
||||
}) : _viewListener = ViewListener(viewId: view.id) {
|
||||
if (listenOnViewChanged) {
|
||||
_viewListener?.start(
|
||||
onViewUpdated: (updatedView) {
|
||||
// If the layout is changed, we need to create a new plugin for it.
|
||||
if (view.layout != updatedView.layout) {
|
||||
getIt<HomeStackManager>().setPlugin(
|
||||
updatedView.plugin(
|
||||
listenOnViewChanged: listenOnViewChanged,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
view = updatedView;
|
||||
isDisplayChanged.value = updatedView.hashCode;
|
||||
},
|
||||
(err) => Log.error(err),
|
||||
);
|
||||
},
|
||||
onViewMoveToTrash: (result) {
|
||||
result.fold(
|
||||
(deletedView) => isDeleted.value = some(deletedView),
|
||||
(err) => Log.error(err),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
},
|
||||
onViewMoveToTrash: (result) {
|
||||
result.fold(
|
||||
(deletedView) => isDeleted.value = some(deletedView),
|
||||
(err) => Log.error(err),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
isDeleted.dispose();
|
||||
isDisplayChanged.dispose();
|
||||
_viewListener?.stop();
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user