diff --git a/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart b/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart index 6d5eeace4c..1d73e247d0 100644 --- a/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart +++ b/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:app_flowy/plugins/grid/application/block/block_cache.dart'; import 'package:app_flowy/plugins/grid/application/grid_data_controller.dart'; import 'package:app_flowy/plugins/grid/application/row/row_cache.dart'; +import 'package:appflowy_board/appflowy_board.dart'; import 'package:dartz/dartz.dart'; import 'package:equatable/equatable.dart'; import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart'; @@ -14,11 +15,32 @@ import 'dart:collection'; part 'board_bloc.freezed.dart'; class BoardBloc extends Bloc { - final GridDataController dataController; + final GridDataController _gridDataController; + late final BoardDataController boardDataController; BoardBloc({required ViewPB view}) - : dataController = GridDataController(view: view), + : _gridDataController = GridDataController(view: view), super(BoardState.initial(view.id)) { + boardDataController = BoardDataController( + onMoveColumn: ( + fromIndex, + toIndex, + ) {}, + onMoveColumnItem: ( + columnId, + fromIndex, + toIndex, + ) {}, + onMoveColumnItemToColumn: ( + fromColumnId, + fromIndex, + toColumnId, + toIndex, + ) {}, + ); + + // boardDataController.addColumns(_buildColumns()); + on( (event, emit) async { await event.when( @@ -27,21 +49,19 @@ class BoardBloc extends Bloc { await _loadGrid(emit); }, createRow: () { - dataController.createRow(); + _gridDataController.createRow(); }, - didReceiveGridUpdate: (grid) { + didReceiveGridUpdate: (GridPB grid) { emit(state.copyWith(grid: Some(grid))); }, - didReceiveFieldUpdate: (fields) { - emit(state.copyWith( - fields: GridFieldEquatable(fields), - )); + didReceiveFieldUpdate: (UnmodifiableListView fields) { + emit(state.copyWith(fields: GridFieldEquatable(fields))); }, - didReceiveRowUpdate: (newRowInfos, reason) { - emit(state.copyWith( - rowInfos: newRowInfos, - reason: reason, - )); + didReceiveRowUpdate: ( + List newRowInfos, + GridRowChangeReason reason, + ) { + emit(state.copyWith(rowInfos: newRowInfos, reason: reason)); }, ); }, @@ -50,17 +70,17 @@ class BoardBloc extends Bloc { @override Future close() async { - await dataController.dispose(); + await _gridDataController.dispose(); return super.close(); } GridRowCache? getRowCache(String blockId, String rowId) { - final GridBlockCache? blockCache = dataController.blocks[blockId]; + final GridBlockCache? blockCache = _gridDataController.blocks[blockId]; return blockCache?.rowCache; } void _startListening() { - dataController.addListener( + _gridDataController.addListener( onGridChanged: (grid) { if (!isClosed) { add(BoardEvent.didReceiveGridUpdate(grid)); @@ -73,14 +93,43 @@ class BoardBloc extends Bloc { }, onFieldsChanged: (fields) { if (!isClosed) { + _buildColumns(fields); add(BoardEvent.didReceiveFieldUpdate(fields)); } }, ); } + void _buildColumns(UnmodifiableListView fields) { + List columns = []; + + for (final field in fields) { + if (field.fieldType == FieldType.SingleSelect) { + // return BoardColumnData(customData: field, id: field.id, desc: "1"); + } + } + + boardDataController.addColumns(columns); + + // final column1 = BoardColumnData(id: "To Do", items: [ + // TextItem("Card 1"), + // TextItem("Card 2"), + // RichTextItem(title: "Card 3", subtitle: 'Aug 1, 2020 4:05 PM'), + // TextItem("Card 4"), + // ]); + // final column2 = BoardColumnData(id: "In Progress", items: [ + // RichTextItem(title: "Card 5", subtitle: 'Aug 1, 2020 4:05 PM'), + // TextItem("Card 6"), + // ]); + + // final column3 = BoardColumnData(id: "Done", items: []); + // boardDataController.addColumn(column1); + // boardDataController.addColumn(column2); + // boardDataController.addColumn(column3); + } + Future _loadGrid(Emitter emit) async { - final result = await dataController.loadData(); + final result = await _gridDataController.loadData(); result.fold( (grid) => emit( state.copyWith(loadingState: GridLoadingState.finish(left(unit))), @@ -159,3 +208,22 @@ class GridFieldEquatable extends Equatable { UnmodifiableListView get value => UnmodifiableListView(_fields); } + +class TextItem extends ColumnItem { + final String s; + + TextItem(this.s); + + @override + String get id => s; +} + +class RichTextItem extends ColumnItem { + final String title; + final String subtitle; + + RichTextItem({required this.title, required this.subtitle}); + + @override + String get id => title; +} diff --git a/frontend/app_flowy/lib/plugins/board/presentation/board_page.dart b/frontend/app_flowy/lib/plugins/board/presentation/board_page.dart index 78d93b8e30..570c1207df 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/board_page.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/board_page.dart @@ -3,19 +3,20 @@ import 'package:appflowy_board/appflowy_board.dart'; import 'package:flowy_infra_ui/widget/error_page.dart'; import 'package:flowy_sdk/protobuf/flowy-folder/view.pb.dart'; +import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; - import '../application/board_bloc.dart'; -class BoardPage2 extends StatelessWidget { +class BoardPage extends StatelessWidget { final ViewPB view; - const BoardPage2({required this.view, Key? key}) : super(key: key); + const BoardPage({required this.view, Key? key}) : super(key: key); @override Widget build(BuildContext context) { return BlocProvider( - create: (context) => BoardBloc(view: view), + create: (context) => + BoardBloc(view: view)..add(const BoardEvent.initial()), child: BlocBuilder( builder: (context, state) { return state.loadingState.map( @@ -23,7 +24,7 @@ class BoardPage2 extends StatelessWidget { const Center(child: CircularProgressIndicator.adaptive()), finish: (result) { return result.successOrFail.fold( - (_) => const BoardContent(), + (_) => BoardContent(), (err) => FlowyErrorPage(err.toString()), ); }, @@ -35,100 +36,58 @@ class BoardPage2 extends StatelessWidget { } class BoardContent extends StatelessWidget { - const BoardContent({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - return Container(); - } -} - -class BoardPage extends StatefulWidget { - final ViewPB _view; - - const BoardPage({required ViewPB view, Key? key}) - : _view = view, - super(key: key); - - @override - State createState() => _BoardPageState(); -} - -class _BoardPageState extends State { - final BoardDataController boardDataController = BoardDataController( - onMoveColumn: (fromIndex, toIndex) { - debugPrint('Move column from $fromIndex to $toIndex'); - }, - onMoveColumnItem: (columnId, fromIndex, toIndex) { - debugPrint('Move $columnId:$fromIndex to $columnId:$toIndex'); - }, - onMoveColumnItemToColumn: (fromColumnId, fromIndex, toColumnId, toIndex) { - debugPrint('Move $fromColumnId:$fromIndex to $toColumnId:$toIndex'); - }, + final config = BoardConfig( + columnBackgroundColor: HexColor.fromHex('#F7F8FC'), ); - @override - void initState() { - final column1 = BoardColumnData(id: "To Do", items: [ - TextItem("Card 1"), - TextItem("Card 2"), - RichTextItem(title: "Card 3", subtitle: 'Aug 1, 2020 4:05 PM'), - TextItem("Card 4"), - ]); - final column2 = BoardColumnData(id: "In Progress", items: [ - RichTextItem(title: "Card 5", subtitle: 'Aug 1, 2020 4:05 PM'), - TextItem("Card 6"), - ]); - - final column3 = BoardColumnData(id: "Done", items: []); - - boardDataController.addColumn(column1); - boardDataController.addColumn(column2); - boardDataController.addColumn(column3); - super.initState(); - } + BoardContent({Key? key}) : super(key: key); @override Widget build(BuildContext context) { - final config = BoardConfig( - columnBackgroundColor: HexColor.fromHex('#F7F8FC'), - ); - return Container( - color: Colors.white, - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 30, horizontal: 20), - child: Board( - dataController: boardDataController, - footBuilder: (context, columnData) { - return AppFlowyColumnFooter( - icon: const Icon(Icons.add, size: 20), - title: const Text('New'), - height: 50, - margin: config.columnItemPadding, - ); - }, - headerBuilder: (context, columnData) { - return AppFlowyColumnHeader( - icon: const Icon(Icons.lightbulb_circle), - title: Text(columnData.id), - addIcon: const Icon(Icons.add, size: 20), - moreIcon: const Icon(Icons.more_horiz, size: 20), - height: 50, - margin: config.columnItemPadding, - ); - }, - cardBuilder: (context, item) { - return AppFlowyColumnItemCard( - key: ObjectKey(item), - child: _buildCard(item), - ); - }, - columnConstraints: const BoxConstraints.tightFor(width: 240), - config: BoardConfig( - columnBackgroundColor: HexColor.fromHex('#F7F8FC'), + return BlocBuilder( + builder: (context, state) { + return Container( + color: Colors.white, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 30, horizontal: 20), + child: Board( + dataController: context.read().boardDataController, + headerBuilder: _buildHeader, + footBuilder: _buildFooter, + cardBuilder: (context, item) { + return AppFlowyColumnItemCard( + key: ObjectKey(item), + child: _buildCard(item), + ); + }, + columnConstraints: const BoxConstraints.tightFor(width: 240), + config: BoardConfig( + columnBackgroundColor: HexColor.fromHex('#F7F8FC'), + ), + ), ), - ), - ), + ); + }, + ); + } + + Widget _buildHeader(BuildContext context, BoardColumnData columnData) { + return AppFlowyColumnHeader( + icon: const Icon(Icons.lightbulb_circle), + title: Text(columnData.desc), + addIcon: const Icon(Icons.add, size: 20), + moreIcon: const Icon(Icons.more_horiz, size: 20), + height: 50, + margin: config.columnItemPadding, + ); + } + + Widget _buildFooter(BuildContext context, BoardColumnData columnData) { + return AppFlowyColumnFooter( + icon: const Icon(Icons.add, size: 20), + title: const Text('New'), + height: 50, + margin: config.columnItemPadding, ); } @@ -171,25 +130,6 @@ class _BoardPageState extends State { } } -class TextItem extends ColumnItem { - final String s; - - TextItem(this.s); - - @override - String get id => s; -} - -class RichTextItem extends ColumnItem { - final String title; - final String subtitle; - - RichTextItem({required this.title, required this.subtitle}); - - @override - String get id => title; -} - extension HexColor on Color { static Color fromHex(String hexString) { final buffer = StringBuffer(); diff --git a/frontend/app_flowy/lib/plugins/grid/application/field/field_cache.dart b/frontend/app_flowy/lib/plugins/grid/application/field/field_cache.dart new file mode 100644 index 0000000000..9597c871c1 --- /dev/null +++ b/frontend/app_flowy/lib/plugins/grid/application/field/field_cache.dart @@ -0,0 +1,192 @@ +import 'dart:collection'; + +import 'package:app_flowy/plugins/grid/application/field/grid_listener.dart'; +import 'package:flowy_sdk/log.dart'; +import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart'; +import 'package:flutter/foundation.dart'; + +import '../row/row_cache.dart'; + +class FieldsNotifier extends ChangeNotifier { + List _fields = []; + + set fields(List fields) { + _fields = fields; + notifyListeners(); + } + + List get fields => _fields; +} + +typedef FieldChangesetCallback = void Function(GridFieldChangesetPB); +typedef FieldsCallback = void Function(List); + +class GridFieldCache { + final String gridId; + final GridFieldsListener _fieldListener; + FieldsNotifier? _fieldNotifier = FieldsNotifier(); + final Map _fieldsCallbackMap = {}; + final Map + _changesetCallbackMap = {}; + + GridFieldCache({required this.gridId}) + : _fieldListener = GridFieldsListener(gridId: gridId) { + _fieldListener.start(onFieldsChanged: (result) { + result.fold( + (changeset) { + _deleteFields(changeset.deletedFields); + _insertFields(changeset.insertedFields); + _updateFields(changeset.updatedFields); + for (final listener in _changesetCallbackMap.values) { + listener(changeset); + } + }, + (err) => Log.error(err), + ); + }); + } + + Future dispose() async { + await _fieldListener.stop(); + _fieldNotifier?.dispose(); + _fieldNotifier = null; + } + + UnmodifiableListView get unmodifiableFields => + UnmodifiableListView(_fieldNotifier?.fields ?? []); + + List get fields => [..._fieldNotifier?.fields ?? []]; + + set fields(List fields) { + _fieldNotifier?.fields = [...fields]; + } + + void addListener({ + FieldsCallback? onFields, + FieldChangesetCallback? onChangeset, + bool Function()? listenWhen, + }) { + if (onChangeset != null) { + fn(c) { + if (listenWhen != null && listenWhen() == false) { + return; + } + onChangeset(c); + } + + _changesetCallbackMap[onChangeset] = fn; + } + + if (onFields != null) { + fn() { + if (listenWhen != null && listenWhen() == false) { + return; + } + onFields(fields); + } + + _fieldsCallbackMap[onFields] = fn; + _fieldNotifier?.addListener(fn); + } + } + + void removeListener({ + FieldsCallback? onFieldsListener, + FieldChangesetCallback? onChangesetListener, + }) { + if (onFieldsListener != null) { + final fn = _fieldsCallbackMap.remove(onFieldsListener); + if (fn != null) { + _fieldNotifier?.removeListener(fn); + } + } + + if (onChangesetListener != null) { + _changesetCallbackMap.remove(onChangesetListener); + } + } + + void _deleteFields(List deletedFields) { + if (deletedFields.isEmpty) { + return; + } + final List newFields = fields; + final Map deletedFieldMap = { + for (var fieldOrder in deletedFields) fieldOrder.fieldId: fieldOrder + }; + + newFields.retainWhere((field) => (deletedFieldMap[field.id] == null)); + _fieldNotifier?.fields = newFields; + } + + void _insertFields(List insertedFields) { + if (insertedFields.isEmpty) { + return; + } + final List newFields = fields; + for (final indexField in insertedFields) { + if (newFields.length > indexField.index) { + newFields.insert(indexField.index, indexField.field_1); + } else { + newFields.add(indexField.field_1); + } + } + _fieldNotifier?.fields = newFields; + } + + void _updateFields(List updatedFields) { + if (updatedFields.isEmpty) { + return; + } + final List newFields = fields; + for (final updatedField in updatedFields) { + final index = + newFields.indexWhere((field) => field.id == updatedField.id); + if (index != -1) { + newFields.removeAt(index); + newFields.insert(index, updatedField); + } + } + _fieldNotifier?.fields = newFields; + } +} + +class GridRowFieldNotifierImpl extends IGridRowFieldNotifier { + final GridFieldCache _cache; + FieldChangesetCallback? _onChangesetFn; + FieldsCallback? _onFieldFn; + GridRowFieldNotifierImpl(GridFieldCache cache) : _cache = cache; + + @override + UnmodifiableListView get fields => _cache.unmodifiableFields; + + @override + void onRowFieldsChanged(VoidCallback callback) { + _onFieldFn = (_) => callback(); + _cache.addListener(onFields: _onFieldFn); + } + + @override + void onRowFieldChanged(void Function(GridFieldPB) callback) { + _onChangesetFn = (GridFieldChangesetPB changeset) { + for (final updatedField in changeset.updatedFields) { + callback(updatedField); + } + }; + + _cache.addListener(onChangeset: _onChangesetFn); + } + + @override + void onRowDispose() { + if (_onFieldFn != null) { + _cache.removeListener(onFieldsListener: _onFieldFn!); + _onFieldFn = null; + } + + if (_onChangesetFn != null) { + _cache.removeListener(onChangesetListener: _onChangesetFn!); + _onChangesetFn = null; + } + } +} diff --git a/frontend/app_flowy/lib/plugins/grid/application/field/field_editor_bloc.dart b/frontend/app_flowy/lib/plugins/grid/application/field/field_editor_bloc.dart index 8d44edf1ff..57e04cbaf4 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/field/field_editor_bloc.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/field/field_editor_bloc.dart @@ -13,7 +13,8 @@ class FieldEditorBloc extends Bloc { required String gridId, required String fieldName, required IFieldTypeOptionLoader loader, - }) : dataController = TypeOptionDataController(gridId: gridId, loader: loader), + }) : dataController = + TypeOptionDataController(gridId: gridId, loader: loader), super(FieldEditorState.initial(gridId, fieldName)) { on( (event, emit) async { @@ -24,7 +25,7 @@ class FieldEditorBloc extends Bloc { add(FieldEditorEvent.didReceiveFieldChanged(field)); } }); - await dataController.loadData(); + await dataController.loadTypeOptionData(); }, updateName: (name) { dataController.fieldName = name; @@ -48,7 +49,8 @@ class FieldEditorBloc extends Bloc { class FieldEditorEvent with _$FieldEditorEvent { const factory FieldEditorEvent.initial() = _InitialField; const factory FieldEditorEvent.updateName(String name) = _UpdateName; - const factory FieldEditorEvent.didReceiveFieldChanged(GridFieldPB field) = _DidReceiveFieldChanged; + const factory FieldEditorEvent.didReceiveFieldChanged(GridFieldPB field) = + _DidReceiveFieldChanged; } @freezed diff --git a/frontend/app_flowy/lib/plugins/grid/application/field/field_service.dart b/frontend/app_flowy/lib/plugins/grid/application/field/field_service.dart index 9274770b21..cf4f374a09 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/field/field_service.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/field/field_service.dart @@ -146,7 +146,8 @@ abstract class IFieldTypeOptionLoader { String get gridId; Future> load(); - Future> switchToField(String fieldId, FieldType fieldType) { + Future> switchToField( + String fieldId, FieldType fieldType) { final payload = EditFieldPayloadPB.create() ..gridId = gridId ..fieldId = fieldId @@ -206,7 +207,7 @@ class TypeOptionDataController { required IFieldTypeOptionLoader loader, }) : _loader = loader; - Future> loadData() async { + Future> loadTypeOptionData() async { final result = await _loader.load(); return result.fold( (data) { @@ -238,7 +239,8 @@ class TypeOptionDataController { _updateData(newTypeOptionData: typeOptionData); } - void _updateData({String? newName, GridFieldPB? newField, List? newTypeOptionData}) { + void _updateData( + {String? newName, GridFieldPB? newField, List? newTypeOptionData}) { _data = _data.rebuild((rebuildData) { if (newName != null) { rebuildData.field_2 = rebuildData.field_2.rebuild((rebuildField) { diff --git a/frontend/app_flowy/lib/plugins/grid/application/field/type_option/multi_select_type_option.dart b/frontend/app_flowy/lib/plugins/grid/application/field/type_option/multi_select_type_option.dart index ebc88aaf95..2f7d2dbc5a 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/field/type_option/multi_select_type_option.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/field/type_option/multi_select_type_option.dart @@ -13,13 +13,13 @@ class MultiSelectTypeOptionContext final TypeOptionService service; MultiSelectTypeOptionContext({ - required MultiSelectTypeOptionWidgetDataParser dataBuilder, + required MultiSelectTypeOptionWidgetDataParser dataParser, required TypeOptionDataController dataController, }) : service = TypeOptionService( gridId: dataController.gridId, fieldId: dataController.field.id, ), - super(dataParser: dataBuilder, dataController: dataController); + super(dataParser: dataParser, dataController: dataController); @override List Function(SelectOptionPB) get deleteOption { diff --git a/frontend/app_flowy/lib/plugins/grid/application/field/type_option/single_select_type_option.dart b/frontend/app_flowy/lib/plugins/grid/application/field/type_option/single_select_type_option.dart index bdf89b5b78..bc995a4237 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/field/type_option/single_select_type_option.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/field/type_option/single_select_type_option.dart @@ -14,12 +14,12 @@ class SingleSelectTypeOptionContext SingleSelectTypeOptionContext({ required SingleSelectTypeOptionWidgetDataParser dataBuilder, - required TypeOptionDataController fieldContext, + required TypeOptionDataController dataController, }) : service = TypeOptionService( - gridId: fieldContext.gridId, - fieldId: fieldContext.field.id, + gridId: dataController.gridId, + fieldId: dataController.field.id, ), - super(dataParser: dataBuilder, dataController: fieldContext); + super(dataParser: dataBuilder, dataController: dataController); @override List Function(SelectOptionPB) get deleteOption { diff --git a/frontend/app_flowy/lib/plugins/grid/application/row/row_cache.dart b/frontend/app_flowy/lib/plugins/grid/application/row/row_cache.dart new file mode 100644 index 0000000000..4f63794afc --- /dev/null +++ b/frontend/app_flowy/lib/plugins/grid/application/row/row_cache.dart @@ -0,0 +1,329 @@ +import 'dart:collection'; +import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart'; +import 'package:flowy_sdk/dispatch/dispatch.dart'; +import 'package:flowy_sdk/log.dart'; +import 'package:flowy_sdk/protobuf/flowy-grid/block_entities.pb.dart'; +import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart'; +import 'package:flowy_sdk/protobuf/flowy-grid/row_entities.pb.dart'; +import 'package:flutter/foundation.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +part 'row_cache.freezed.dart'; + +typedef RowUpdateCallback = void Function(); + +abstract class IGridRowFieldNotifier { + UnmodifiableListView get fields; + void onRowFieldsChanged(VoidCallback callback); + void onRowFieldChanged(void Function(GridFieldPB) callback); + void onRowDispose(); +} + +/// Cache the rows in memory +/// Insert / delete / update row +/// +/// Read https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/architecture/frontend/grid for more information. + +class GridRowCache { + final String gridId; + final GridBlockPB block; + + /// _rows containers the current block's rows + /// Use List to reverse the order of the GridRow. + List _rowInfos = []; + + /// Use Map for faster access the raw row data. + final HashMap _rowByRowId; + + final GridCellCache _cellCache; + final IGridRowFieldNotifier _fieldNotifier; + final _GridRowChangesetNotifier _rowChangeReasonNotifier; + + UnmodifiableListView get rows => UnmodifiableListView(_rowInfos); + GridCellCache get cellCache => _cellCache; + + GridRowCache({ + required this.gridId, + required this.block, + required IGridRowFieldNotifier notifier, + }) : _cellCache = GridCellCache(gridId: gridId), + _rowByRowId = HashMap(), + _rowChangeReasonNotifier = _GridRowChangesetNotifier(), + _fieldNotifier = notifier { + // + notifier.onRowFieldsChanged(() => _rowChangeReasonNotifier + .receive(const GridRowChangeReason.fieldDidChange())); + notifier.onRowFieldChanged((field) => _cellCache.remove(field.id)); + _rowInfos = block.rows + .map((rowInfo) => buildGridRow(rowInfo.id, rowInfo.height.toDouble())) + .toList(); + } + + Future dispose() async { + _fieldNotifier.onRowDispose(); + _rowChangeReasonNotifier.dispose(); + await _cellCache.dispose(); + } + + void applyChangesets(List changesets) { + for (final changeset in changesets) { + _deleteRows(changeset.deletedRows); + _insertRows(changeset.insertedRows); + _updateRows(changeset.updatedRows); + _hideRows(changeset.hideRows); + _showRows(changeset.visibleRows); + } + } + + void _deleteRows(List deletedRows) { + if (deletedRows.isEmpty) { + return; + } + + final List newRows = []; + final DeletedIndexs deletedIndex = []; + final Map deletedRowByRowId = { + for (var rowId in deletedRows) rowId: rowId + }; + + _rowInfos.asMap().forEach((index, row) { + if (deletedRowByRowId[row.id] == null) { + newRows.add(row); + } else { + _rowByRowId.remove(row.id); + deletedIndex.add(DeletedIndex(index: index, row: row)); + } + }); + _rowInfos = newRows; + _rowChangeReasonNotifier.receive(GridRowChangeReason.delete(deletedIndex)); + } + + void _insertRows(List insertRows) { + if (insertRows.isEmpty) { + return; + } + + InsertedIndexs insertIndexs = []; + for (final insertRow in insertRows) { + final insertIndex = InsertedIndex( + index: insertRow.index, + rowId: insertRow.rowId, + ); + insertIndexs.add(insertIndex); + _rowInfos.insert(insertRow.index, + (buildGridRow(insertRow.rowId, insertRow.height.toDouble()))); + } + + _rowChangeReasonNotifier.receive(GridRowChangeReason.insert(insertIndexs)); + } + + void _updateRows(List updatedRows) { + if (updatedRows.isEmpty) { + return; + } + + final UpdatedIndexs updatedIndexs = UpdatedIndexs(); + for (final updatedRow in updatedRows) { + final rowId = updatedRow.rowId; + final index = _rowInfos.indexWhere((row) => row.id == rowId); + if (index != -1) { + _rowByRowId[rowId] = updatedRow.row; + + _rowInfos.removeAt(index); + _rowInfos.insert( + index, buildGridRow(rowId, updatedRow.row.height.toDouble())); + updatedIndexs[rowId] = UpdatedIndex(index: index, rowId: rowId); + } + } + + _rowChangeReasonNotifier.receive(GridRowChangeReason.update(updatedIndexs)); + } + + void _hideRows(List hideRows) {} + + void _showRows(List visibleRows) {} + + void onRowsChanged( + void Function(GridRowChangeReason) onRowChanged, + ) { + _rowChangeReasonNotifier.addListener(() { + onRowChanged(_rowChangeReasonNotifier.reason); + }); + } + + RowUpdateCallback addListener({ + required String rowId, + void Function(GridCellMap, GridRowChangeReason)? onCellUpdated, + bool Function()? listenWhen, + }) { + listenerHandler() async { + if (listenWhen != null && listenWhen() == false) { + return; + } + + notifyUpdate() { + if (onCellUpdated != null) { + final row = _rowByRowId[rowId]; + if (row != null) { + final GridCellMap cellDataMap = _makeGridCells(rowId, row); + onCellUpdated(cellDataMap, _rowChangeReasonNotifier.reason); + } + } + } + + _rowChangeReasonNotifier.reason.whenOrNull( + update: (indexs) { + if (indexs[rowId] != null) notifyUpdate(); + }, + fieldDidChange: () => notifyUpdate(), + ); + } + + _rowChangeReasonNotifier.addListener(listenerHandler); + return listenerHandler; + } + + void removeRowListener(VoidCallback callback) { + _rowChangeReasonNotifier.removeListener(callback); + } + + GridCellMap loadGridCells(String rowId) { + final GridRowPB? data = _rowByRowId[rowId]; + if (data == null) { + _loadRow(rowId); + } + return _makeGridCells(rowId, data); + } + + Future _loadRow(String rowId) async { + final payload = GridRowIdPB.create() + ..gridId = gridId + ..blockId = block.id + ..rowId = rowId; + + final result = await GridEventGetRow(payload).send(); + result.fold( + (optionRow) => _refreshRow(optionRow), + (err) => Log.error(err), + ); + } + + GridCellMap _makeGridCells(String rowId, GridRowPB? row) { + var cellDataMap = GridCellMap.new(); + for (final field in _fieldNotifier.fields) { + if (field.visibility) { + cellDataMap[field.id] = GridCellIdentifier( + rowId: rowId, + gridId: gridId, + field: field, + ); + } + } + return cellDataMap; + } + + void _refreshRow(OptionalRowPB optionRow) { + if (!optionRow.hasRow()) { + return; + } + final updatedRow = optionRow.row; + updatedRow.freeze(); + + _rowByRowId[updatedRow.id] = updatedRow; + final index = + _rowInfos.indexWhere((gridRow) => gridRow.id == updatedRow.id); + if (index != -1) { + // update the corresponding row in _rows if they are not the same + if (_rowInfos[index].rawRow != updatedRow) { + final row = _rowInfos.removeAt(index).copyWith(rawRow: updatedRow); + _rowInfos.insert(index, row); + + // Calculate the update index + final UpdatedIndexs updatedIndexs = UpdatedIndexs(); + updatedIndexs[row.id] = UpdatedIndex(index: index, rowId: row.id); + + // + _rowChangeReasonNotifier + .receive(GridRowChangeReason.update(updatedIndexs)); + } + } + } + + GridRowInfo buildGridRow(String rowId, double rowHeight) { + return GridRowInfo( + gridId: gridId, + blockId: block.id, + fields: _fieldNotifier.fields, + id: rowId, + height: rowHeight, + ); + } +} + +class _GridRowChangesetNotifier extends ChangeNotifier { + GridRowChangeReason reason = const InitialListState(); + + _GridRowChangesetNotifier(); + + void receive(GridRowChangeReason newReason) { + reason = newReason; + reason.map( + insert: (_) => notifyListeners(), + delete: (_) => notifyListeners(), + update: (_) => notifyListeners(), + fieldDidChange: (_) => notifyListeners(), + initial: (_) {}, + ); + } +} + +@freezed +class GridRowInfo with _$GridRowInfo { + const factory GridRowInfo({ + required String gridId, + required String blockId, + required String id, + required UnmodifiableListView fields, + required double height, + GridRowPB? rawRow, + }) = _GridRowInfo; +} + +typedef InsertedIndexs = List; +typedef DeletedIndexs = List; +typedef UpdatedIndexs = LinkedHashMap; + +@freezed +class GridRowChangeReason with _$GridRowChangeReason { + const factory GridRowChangeReason.insert(InsertedIndexs items) = _Insert; + const factory GridRowChangeReason.delete(DeletedIndexs items) = _Delete; + const factory GridRowChangeReason.update(UpdatedIndexs indexs) = _Update; + const factory GridRowChangeReason.fieldDidChange() = _FieldDidChange; + const factory GridRowChangeReason.initial() = InitialListState; +} + +class InsertedIndex { + final int index; + final String rowId; + InsertedIndex({ + required this.index, + required this.rowId, + }); +} + +class DeletedIndex { + final int index; + final GridRowInfo row; + DeletedIndex({ + required this.index, + required this.row, + }); +} + +class UpdatedIndex { + final int index; + final String rowId; + UpdatedIndex({ + required this.index, + required this.rowId, + }); +} diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/builder.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/builder.dart index 4471e672a3..787f2091a9 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/builder.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/builder.dart @@ -65,7 +65,7 @@ TypeOptionWidgetBuilder makeTypeOptionWidgetBuilder( ); case FieldType.SingleSelect: final context = SingleSelectTypeOptionContext( - fieldContext: dataController, + dataController: dataController, dataBuilder: SingleSelectTypeOptionWidgetDataParser(), ); return SingleSelectTypeOptionWidgetBuilder( @@ -75,7 +75,7 @@ TypeOptionWidgetBuilder makeTypeOptionWidgetBuilder( case FieldType.MultiSelect: final context = MultiSelectTypeOptionContext( dataController: dataController, - dataBuilder: MultiSelectTypeOptionWidgetDataParser(), + dataParser: MultiSelectTypeOptionWidgetDataParser(), ); return MultiSelectTypeOptionWidgetBuilder( context, diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/utils/log.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/utils/log.dart index b9f766f961..d11b5fd263 100644 --- a/frontend/app_flowy/packages/appflowy_board/lib/src/utils/log.dart +++ b/frontend/app_flowy/packages/appflowy_board/lib/src/utils/log.dart @@ -20,6 +20,12 @@ class Log { } } + static void warn(String? message) { + if (enableLog) { + debugPrint('🐛[Warn]=> $message'); + } + } + static void trace(String? message) { if (enableLog) { // debugPrint('❗️[Trace]=> $message'); diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column_data.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column_data.dart index 2ce739220e..97551ef350 100644 --- a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column_data.dart +++ b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column_data.dart @@ -56,25 +56,26 @@ class BoardColumnDataController extends ChangeNotifier with EquatableMixin { /// Move the item from [fromIndex] to [toIndex]. It will do nothing if the /// [fromIndex] equal to the [toIndex]. - void move(int fromIndex, int toIndex) { + bool move(int fromIndex, int toIndex) { assert(fromIndex >= 0); assert(toIndex >= 0); if (fromIndex == toIndex) { - return; + return false; } Log.debug( '[$BoardColumnDataController] $columnData move item from $fromIndex to $toIndex'); final item = columnData._items.removeAt(fromIndex); columnData._items.insert(toIndex, item); notifyListeners(); + return true; } /// Insert an item to [index] and notify the listen if the value of [notify] /// is true. /// /// The default value of [notify] is true. - void insert(int index, ColumnItem item, {bool notify = true}) { + bool insert(int index, ColumnItem item, {bool notify = true}) { assert(index >= 0); Log.debug( '[$BoardColumnDataController] $columnData insert $item at $index'); @@ -85,9 +86,14 @@ class BoardColumnDataController extends ChangeNotifier with EquatableMixin { columnData._items.add(item); } - if (notify) { - notifyListeners(); - } + if (notify) notifyListeners(); + return true; + } + + bool add(ColumnItem item, {bool notify = true}) { + columnData._items.add(item); + if (notify) notifyListeners(); + return true; } /// Replace the item at index with the [newItem]. @@ -107,14 +113,18 @@ class BoardColumnDataController extends ChangeNotifier with EquatableMixin { } /// [BoardColumnData] represents the data of each Column of the Board. -class BoardColumnData extends ReoderFlexItem with EquatableMixin { +class BoardColumnData extends ReoderFlexItem with EquatableMixin { @override final String id; + final String desc; final List _items; + final CustomData? customData; BoardColumnData({ + this.customData, required this.id, - required List items, + this.desc = "", + List items = const [], }) : _items = items; /// Returns the readonly List diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_data.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_data.dart index 06e8ff1a57..37bae68121 100644 --- a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_data.dart +++ b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_data.dart @@ -44,32 +44,84 @@ class BoardDataController extends ChangeNotifier this.onMoveColumnItemToColumn, }); - void addColumn(BoardColumnData columnData) { + void addColumn(BoardColumnData columnData, {bool notify = true}) { + if (_columnControllers[columnData.id] != null) return; + final controller = BoardColumnDataController(columnData: columnData); _columnDatas.add(columnData); _columnControllers[columnData.id] = controller; + if (notify) notifyListeners(); + } + + void addColumns(List columns, {bool notify = true}) { + for (final column in columns) { + addColumn(column, notify: false); + } + + if (columns.isNotEmpty && notify) notifyListeners(); + } + + void removeColumn(String columnId, {bool notify = true}) { + final index = _columnDatas.indexWhere((column) => column.id == columnId); + if (index == -1) { + Log.warn( + 'Try to remove Column:[$columnId] failed. Column:[$columnId] not exist'); + } + + if (index != -1) { + _columnDatas.removeAt(index); + _columnControllers.remove(columnId); + + if (notify) notifyListeners(); + } + } + + void removeColumns(List columnIds, {bool notify = true}) { + for (final columnId in columnIds) { + removeColumn(columnId, notify: false); + } + + if (columnIds.isNotEmpty && notify) notifyListeners(); } BoardColumnDataController columnController(String columnId) { return _columnControllers[columnId]!; } - void moveColumn(int fromIndex, int toIndex) { + BoardColumnDataController? getColumnController(String columnId) { + final columnController = _columnControllers[columnId]; + if (columnController == null) { + Log.warn('Column:[$columnId] \'s controller is not exist'); + } + + return columnController; + } + + void moveColumn(int fromIndex, int toIndex, {bool notify = true}) { final columnData = _columnDatas.removeAt(fromIndex); _columnDatas.insert(toIndex, columnData); onMoveColumn?.call(fromIndex, toIndex); - notifyListeners(); + if (notify) notifyListeners(); } void moveColumnItem(String columnId, int fromIndex, int toIndex) { - final columnController = _columnControllers[columnId]; - assert(columnController != null); - if (columnController != null) { - columnController.move(fromIndex, toIndex); + if (getColumnController(columnId)?.move(fromIndex, toIndex) ?? false) { onMoveColumnItem?.call(columnId, fromIndex, toIndex); } } + void addColumnItem(String columnId, ColumnItem item) { + getColumnController(columnId)?.add(item); + } + + void insertColumnItem(String columnId, int index, ColumnItem item) { + getColumnController(columnId)?.insert(index, item); + } + + void removeColumnItem(String columnId, String itemId) { + getColumnController(columnId)?.removeWhere((item) => item.id == itemId); + } + @override @protected void swapColumnItem(