chore: load field typeOption

This commit is contained in:
appflowy 2022-08-10 12:58:07 +08:00
parent 25eb5bf1b0
commit 0d6c04ae81
12 changed files with 759 additions and 158 deletions

View File

@ -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<BoardEvent, BoardState> {
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<BoardEvent>(
(event, emit) async {
await event.when(
@ -27,21 +49,19 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
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<GridFieldPB> fields) {
emit(state.copyWith(fields: GridFieldEquatable(fields)));
},
didReceiveRowUpdate: (newRowInfos, reason) {
emit(state.copyWith(
rowInfos: newRowInfos,
reason: reason,
));
didReceiveRowUpdate: (
List<GridRowInfo> newRowInfos,
GridRowChangeReason reason,
) {
emit(state.copyWith(rowInfos: newRowInfos, reason: reason));
},
);
},
@ -50,17 +70,17 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
@override
Future<void> 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<BoardEvent, BoardState> {
},
onFieldsChanged: (fields) {
if (!isClosed) {
_buildColumns(fields);
add(BoardEvent.didReceiveFieldUpdate(fields));
}
},
);
}
void _buildColumns(UnmodifiableListView<GridFieldPB> fields) {
List<BoardColumnData> 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<void> _loadGrid(Emitter<BoardState> 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<GridFieldPB> 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;
}

View File

@ -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<BoardBloc, BoardState>(
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,88 +36,24 @@ 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<BoardPage> createState() => _BoardPageState();
}
class _BoardPageState extends State<BoardPage> {
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');
},
);
@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();
}
@override
Widget build(BuildContext context) {
final config = BoardConfig(
columnBackgroundColor: HexColor.fromHex('#F7F8FC'),
);
BoardContent({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return BlocBuilder<BoardBloc, BoardState>(
builder: (context, state) {
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,
);
},
dataController: context.read<BoardBloc>().boardDataController,
headerBuilder: _buildHeader,
footBuilder: _buildFooter,
cardBuilder: (context, item) {
return AppFlowyColumnItemCard(
key: ObjectKey(item),
@ -130,6 +67,28 @@ class _BoardPageState extends State<BoardPage> {
),
),
);
},
);
}
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,
);
}
Widget _buildCard(ColumnItem item) {
@ -171,25 +130,6 @@ class _BoardPageState extends State<BoardPage> {
}
}
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();

View File

@ -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<GridFieldPB> _fields = [];
set fields(List<GridFieldPB> fields) {
_fields = fields;
notifyListeners();
}
List<GridFieldPB> get fields => _fields;
}
typedef FieldChangesetCallback = void Function(GridFieldChangesetPB);
typedef FieldsCallback = void Function(List<GridFieldPB>);
class GridFieldCache {
final String gridId;
final GridFieldsListener _fieldListener;
FieldsNotifier? _fieldNotifier = FieldsNotifier();
final Map<FieldsCallback, VoidCallback> _fieldsCallbackMap = {};
final Map<FieldChangesetCallback, FieldChangesetCallback>
_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<void> dispose() async {
await _fieldListener.stop();
_fieldNotifier?.dispose();
_fieldNotifier = null;
}
UnmodifiableListView<GridFieldPB> get unmodifiableFields =>
UnmodifiableListView(_fieldNotifier?.fields ?? []);
List<GridFieldPB> get fields => [..._fieldNotifier?.fields ?? []];
set fields(List<GridFieldPB> 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<GridFieldIdPB> deletedFields) {
if (deletedFields.isEmpty) {
return;
}
final List<GridFieldPB> newFields = fields;
final Map<String, GridFieldIdPB> deletedFieldMap = {
for (var fieldOrder in deletedFields) fieldOrder.fieldId: fieldOrder
};
newFields.retainWhere((field) => (deletedFieldMap[field.id] == null));
_fieldNotifier?.fields = newFields;
}
void _insertFields(List<IndexFieldPB> insertedFields) {
if (insertedFields.isEmpty) {
return;
}
final List<GridFieldPB> 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<GridFieldPB> updatedFields) {
if (updatedFields.isEmpty) {
return;
}
final List<GridFieldPB> 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<GridFieldPB> 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;
}
}
}

View File

@ -13,7 +13,8 @@ class FieldEditorBloc extends Bloc<FieldEditorEvent, FieldEditorState> {
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<FieldEditorEvent>(
(event, emit) async {
@ -24,7 +25,7 @@ class FieldEditorBloc extends Bloc<FieldEditorEvent, FieldEditorState> {
add(FieldEditorEvent.didReceiveFieldChanged(field));
}
});
await dataController.loadData();
await dataController.loadTypeOptionData();
},
updateName: (name) {
dataController.fieldName = name;
@ -48,7 +49,8 @@ class FieldEditorBloc extends Bloc<FieldEditorEvent, FieldEditorState> {
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

View File

@ -146,7 +146,8 @@ abstract class IFieldTypeOptionLoader {
String get gridId;
Future<Either<FieldTypeOptionDataPB, FlowyError>> load();
Future<Either<FieldTypeOptionDataPB, FlowyError>> switchToField(String fieldId, FieldType fieldType) {
Future<Either<FieldTypeOptionDataPB, FlowyError>> 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<Either<Unit, FlowyError>> loadData() async {
Future<Either<Unit, FlowyError>> 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<int>? newTypeOptionData}) {
void _updateData(
{String? newName, GridFieldPB? newField, List<int>? newTypeOptionData}) {
_data = _data.rebuild((rebuildData) {
if (newName != null) {
rebuildData.field_2 = rebuildData.field_2.rebuild((rebuildField) {

View File

@ -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<SelectOptionPB> Function(SelectOptionPB) get deleteOption {

View File

@ -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<SelectOptionPB> Function(SelectOptionPB) get deleteOption {

View File

@ -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<GridFieldPB> 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<GridRowInfo> _rowInfos = [];
/// Use Map for faster access the raw row data.
final HashMap<String, GridRowPB> _rowByRowId;
final GridCellCache _cellCache;
final IGridRowFieldNotifier _fieldNotifier;
final _GridRowChangesetNotifier _rowChangeReasonNotifier;
UnmodifiableListView<GridRowInfo> 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<void> dispose() async {
_fieldNotifier.onRowDispose();
_rowChangeReasonNotifier.dispose();
await _cellCache.dispose();
}
void applyChangesets(List<GridBlockChangesetPB> changesets) {
for (final changeset in changesets) {
_deleteRows(changeset.deletedRows);
_insertRows(changeset.insertedRows);
_updateRows(changeset.updatedRows);
_hideRows(changeset.hideRows);
_showRows(changeset.visibleRows);
}
}
void _deleteRows(List<String> deletedRows) {
if (deletedRows.isEmpty) {
return;
}
final List<GridRowInfo> newRows = [];
final DeletedIndexs deletedIndex = [];
final Map<String, String> 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<InsertedRowPB> 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<UpdatedRowPB> 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<String> hideRows) {}
void _showRows(List<String> 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<void> _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<GridFieldPB> fields,
required double height,
GridRowPB? rawRow,
}) = _GridRowInfo;
}
typedef InsertedIndexs = List<InsertedIndex>;
typedef DeletedIndexs = List<DeletedIndex>;
typedef UpdatedIndexs = LinkedHashMap<String, UpdatedIndex>;
@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,
});
}

View File

@ -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,

View File

@ -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');

View File

@ -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<CustomData> extends ReoderFlexItem with EquatableMixin {
@override
final String id;
final String desc;
final List<ColumnItem> _items;
final CustomData? customData;
BoardColumnData({
this.customData,
required this.id,
required List<ColumnItem> items,
this.desc = "",
List<ColumnItem> items = const [],
}) : _items = items;
/// Returns the readonly List<ColumnItem>

View File

@ -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<BoardColumnData> 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<String> 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(