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 df79e3154c..09f85df732 100644 --- a/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart +++ b/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'package:app_flowy/plugins/grid/application/block/block_cache.dart'; +import 'package:app_flowy/plugins/grid/application/field/field_cache.dart'; import 'package:app_flowy/plugins/grid/application/row/row_cache.dart'; import 'package:appflowy_board/appflowy_board.dart'; import 'package:dartz/dartz.dart'; @@ -20,6 +21,9 @@ class BoardBloc extends Bloc { final BoardDataController _dataController; late final AFBoardDataController boardDataController; + GridFieldCache get fieldCache => _dataController.fieldCache; + String get gridId => _dataController.gridId; + BoardBloc({required ViewPB view}) : _dataController = BoardDataController(view: view), super(BoardState.initial(view.id)) { @@ -57,6 +61,9 @@ class BoardBloc extends Bloc { didReceiveGroups: (List groups) { emit(state.copyWith(groups: groups)); }, + didReceiveRows: (List rowInfos) { + emit(state.copyWith(rowInfos: rowInfos)); + }, ); }, ); @@ -68,7 +75,7 @@ class BoardBloc extends Bloc { return super.close(); } - GridRowCache? getRowCache(String blockId, String rowId) { + GridRowCache? getRowCache(String blockId) { final GridBlockCache? blockCache = _dataController.blocks[blockId]; return blockCache?.rowCache; } @@ -92,24 +99,29 @@ class BoardBloc extends Bloc { boardDataController.addColumns(columns); }, + onRowsChanged: (List rowInfos, RowChangeReason reason) { + add(BoardEvent.didReceiveRows(rowInfos)); + }, onError: (err) { Log.error(err); }, ); } - List _buildRows(List rows) { - return rows.map((row) { - final rowInfo = RowInfo( - gridId: _dataController.gridId, - blockId: row.blockId, - id: row.id, - fields: _dataController.fieldCache.unmodifiableFields, - height: row.height.toDouble(), - rawRow: row, - ); - return BoardColumnItem(row: rowInfo); + List _buildRows(List rows) { + final items = rows.map((row) { + // final rowInfo = RowInfo( + // gridId: _dataController.gridId, + // blockId: row.blockId, + // id: row.id, + // fields: _dataController.fieldCache.unmodifiableFields, + // height: row.height.toDouble(), + // rawRow: row, + // ); + return BoardColumnItem(row: row); }).toList(); + + return [...items]; } Future _loadGrid(Emitter emit) async { @@ -131,6 +143,8 @@ class BoardEvent with _$BoardEvent { const factory BoardEvent.createRow() = _CreateRow; const factory BoardEvent.didReceiveGroups(List groups) = _DidReceiveGroup; + const factory BoardEvent.didReceiveRows(List rowInfos) = + _DidReceiveRows; const factory BoardEvent.didReceiveGridUpdate( GridPB grid, ) = _DidReceiveGridUpdate; @@ -186,10 +200,15 @@ class GridFieldEquatable extends Equatable { } class BoardColumnItem extends AFColumnItem { - final RowInfo row; + final RowPB row; BoardColumnItem({required this.row}); @override String get id => row.id; } + +class CreateCardItem extends AFColumnItem { + @override + String get id => '$CreateCardItem'; +} diff --git a/frontend/app_flowy/lib/plugins/board/application/board_data_controller.dart b/frontend/app_flowy/lib/plugins/board/application/board_data_controller.dart index c8e20b6172..da4cc54132 100644 --- a/frontend/app_flowy/lib/plugins/board/application/board_data_controller.dart +++ b/frontend/app_flowy/lib/plugins/board/application/board_data_controller.dart @@ -3,6 +3,8 @@ import 'dart:collection'; import 'package:app_flowy/plugins/grid/application/block/block_cache.dart'; import 'package:app_flowy/plugins/grid/application/field/field_cache.dart'; import 'package:app_flowy/plugins/grid/application/grid_service.dart'; +import 'package:app_flowy/plugins/grid/application/row/row_cache.dart'; +import 'package:flowy_sdk/log.dart'; import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-folder/view.pb.dart'; import 'dart:async'; @@ -12,6 +14,10 @@ import 'package:flowy_sdk/protobuf/flowy-grid/protobuf.dart'; typedef OnFieldsChanged = void Function(UnmodifiableListView); typedef OnGridChanged = void Function(GridPB); typedef OnGroupChanged = void Function(List); +typedef OnRowsChanged = void Function( + List rowInfos, + RowChangeReason, +); typedef OnError = void Function(FlowyError); class BoardDataController { @@ -21,17 +27,25 @@ class BoardDataController { // key: the block id final LinkedHashMap _blocks; - UnmodifiableMapView get blocks => - UnmodifiableMapView(_blocks); + LinkedHashMap get blocks => _blocks; OnFieldsChanged? _onFieldsChanged; OnGridChanged? _onGridChanged; OnGroupChanged? _onGroupChanged; + OnRowsChanged? _onRowsChanged; OnError? _onError; + List get rowInfos { + final List rows = []; + for (var block in _blocks.values) { + rows.addAll(block.rows); + } + return rows; + } + BoardDataController({required ViewPB view}) : gridId = view.id, - _blocks = LinkedHashMap.identity(), + _blocks = LinkedHashMap.new(), _gridFFIService = GridService(gridId: view.id), fieldCache = GridFieldCache(gridId: view.id); @@ -39,11 +53,13 @@ class BoardDataController { OnGridChanged? onGridChanged, OnFieldsChanged? onFieldsChanged, OnGroupChanged? onGroupChanged, + OnRowsChanged? onRowsChanged, OnError? onError, }) { _onGridChanged = onGridChanged; _onFieldsChanged = onFieldsChanged; _onGroupChanged = onGroupChanged; + _onRowsChanged = onRowsChanged; _onError = onError; fieldCache.addListener(onFields: (fields) { @@ -57,6 +73,7 @@ class BoardDataController { () => result.fold( (grid) async { _onGridChanged?.call(grid); + _initialBlocks(grid.blocks); return await _loadFields(grid).then((result) { return result.fold( (l) { @@ -72,8 +89,8 @@ class BoardDataController { ); } - void createRow() { - _gridFFIService.createRow(); + Future> createRow() { + return _gridFFIService.createRow(); } Future dispose() async { @@ -85,6 +102,29 @@ class BoardDataController { } } + void _initialBlocks(List blocks) { + for (final block in blocks) { + if (_blocks[block.id] != null) { + Log.warn("Initial duplicate block's cache: ${block.id}"); + return; + } + + final cache = GridBlockCache( + gridId: gridId, + block: block, + fieldCache: fieldCache, + ); + + cache.addListener( + onChangeReason: (reason) { + _onRowsChanged?.call(rowInfos, reason); + }, + ); + + _blocks[block.id] = cache; + } + } + Future> _loadFields(GridPB grid) async { final result = await _gridFFIService.getFields(fieldIds: grid.fields); return Future( diff --git a/frontend/app_flowy/lib/plugins/board/application/card/board_checkbox_cell_bloc.dart b/frontend/app_flowy/lib/plugins/board/application/card/board_checkbox_cell_bloc.dart new file mode 100644 index 0000000000..3834db112c --- /dev/null +++ b/frontend/app_flowy/lib/plugins/board/application/card/board_checkbox_cell_bloc.dart @@ -0,0 +1,71 @@ +import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'dart:async'; + +part 'board_checkbox_cell_bloc.freezed.dart'; + +class BoardCheckboxCellBloc + extends Bloc { + final GridCheckboxCellController cellController; + void Function()? _onCellChangedFn; + BoardCheckboxCellBloc({ + required this.cellController, + }) : super(BoardCheckboxCellState.initial(cellController)) { + on( + (event, emit) async { + await event.when( + initial: () async { + _startListening(); + }, + didReceiveCellUpdate: (cellData) { + emit(state.copyWith(isSelected: _isSelected(cellData))); + }, + ); + }, + ); + } + + @override + Future close() async { + if (_onCellChangedFn != null) { + cellController.removeListener(_onCellChangedFn!); + _onCellChangedFn = null; + } + cellController.dispose(); + return super.close(); + } + + void _startListening() { + _onCellChangedFn = cellController.startListening( + onCellChanged: ((cellContent) { + if (!isClosed) { + add(BoardCheckboxCellEvent.didReceiveCellUpdate(cellContent ?? "")); + } + }), + ); + } +} + +@freezed +class BoardCheckboxCellEvent with _$BoardCheckboxCellEvent { + const factory BoardCheckboxCellEvent.initial() = _InitialCell; + const factory BoardCheckboxCellEvent.didReceiveCellUpdate( + String cellContent) = _DidReceiveCellUpdate; +} + +@freezed +class BoardCheckboxCellState with _$BoardCheckboxCellState { + const factory BoardCheckboxCellState({ + required bool isSelected, + }) = _CheckboxCellState; + + factory BoardCheckboxCellState.initial(GridCellController context) { + return BoardCheckboxCellState( + isSelected: _isSelected(context.getCellData())); + } +} + +bool _isSelected(String? cellData) { + return cellData == "Yes"; +} diff --git a/frontend/app_flowy/lib/plugins/board/application/card/board_date_cell_bloc.dart b/frontend/app_flowy/lib/plugins/board/application/card/board_date_cell_bloc.dart new file mode 100644 index 0000000000..76267ededb --- /dev/null +++ b/frontend/app_flowy/lib/plugins/board/application/card/board_date_cell_bloc.dart @@ -0,0 +1,85 @@ +import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart'; +import 'package:flowy_sdk/protobuf/flowy-grid/date_type_option_entities.pb.dart'; +import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'dart:async'; +part 'board_date_cell_bloc.freezed.dart'; + +class BoardDateCellBloc extends Bloc { + final GridDateCellController cellController; + void Function()? _onCellChangedFn; + + BoardDateCellBloc({required this.cellController}) + : super(BoardDateCellState.initial(cellController)) { + on( + (event, emit) async { + event.when( + initial: () => _startListening(), + didReceiveCellUpdate: (DateCellDataPB? cellData) { + emit(state.copyWith( + data: cellData, dateStr: _dateStrFromCellData(cellData))); + }, + didReceiveFieldUpdate: (FieldPB value) => + emit(state.copyWith(field: value)), + ); + }, + ); + } + + @override + Future close() async { + if (_onCellChangedFn != null) { + cellController.removeListener(_onCellChangedFn!); + _onCellChangedFn = null; + } + cellController.dispose(); + return super.close(); + } + + void _startListening() { + _onCellChangedFn = cellController.startListening( + onCellChanged: ((data) { + if (!isClosed) { + add(BoardDateCellEvent.didReceiveCellUpdate(data)); + } + }), + ); + } +} + +@freezed +class BoardDateCellEvent with _$BoardDateCellEvent { + const factory BoardDateCellEvent.initial() = _InitialCell; + const factory BoardDateCellEvent.didReceiveCellUpdate(DateCellDataPB? data) = + _DidReceiveCellUpdate; + const factory BoardDateCellEvent.didReceiveFieldUpdate(FieldPB field) = + _DidReceiveFieldUpdate; +} + +@freezed +class BoardDateCellState with _$BoardDateCellState { + const factory BoardDateCellState({ + required DateCellDataPB? data, + required String dateStr, + required FieldPB field, + }) = _BoardDateCellState; + + factory BoardDateCellState.initial(GridDateCellController context) { + final cellData = context.getCellData(); + + return BoardDateCellState( + field: context.field, + data: cellData, + dateStr: _dateStrFromCellData(cellData), + ); + } +} + +String _dateStrFromCellData(DateCellDataPB? cellData) { + String dateStr = ""; + if (cellData != null) { + dateStr = cellData.date + " " + cellData.time; + } + return dateStr; +} diff --git a/frontend/app_flowy/lib/plugins/board/application/card/board_number_cell_bloc.dart b/frontend/app_flowy/lib/plugins/board/application/card/board_number_cell_bloc.dart new file mode 100644 index 0000000000..2cc4882357 --- /dev/null +++ b/frontend/app_flowy/lib/plugins/board/application/card/board_number_cell_bloc.dart @@ -0,0 +1,67 @@ +import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'dart:async'; + +part 'board_number_cell_bloc.freezed.dart'; + +class BoardNumberCellBloc + extends Bloc { + final GridNumberCellController cellController; + void Function()? _onCellChangedFn; + BoardNumberCellBloc({ + required this.cellController, + }) : super(BoardNumberCellState.initial(cellController)) { + on( + (event, emit) async { + await event.when( + initial: () async { + _startListening(); + }, + didReceiveCellUpdate: (content) { + emit(state.copyWith(content: content)); + }, + ); + }, + ); + } + + @override + Future close() async { + if (_onCellChangedFn != null) { + cellController.removeListener(_onCellChangedFn!); + _onCellChangedFn = null; + } + cellController.dispose(); + return super.close(); + } + + void _startListening() { + _onCellChangedFn = cellController.startListening( + onCellChanged: ((cellContent) { + if (!isClosed) { + add(BoardNumberCellEvent.didReceiveCellUpdate(cellContent ?? "")); + } + }), + ); + } +} + +@freezed +class BoardNumberCellEvent with _$BoardNumberCellEvent { + const factory BoardNumberCellEvent.initial() = _InitialCell; + const factory BoardNumberCellEvent.didReceiveCellUpdate(String cellContent) = + _DidReceiveCellUpdate; +} + +@freezed +class BoardNumberCellState with _$BoardNumberCellState { + const factory BoardNumberCellState({ + required String content, + }) = _BoardNumberCellState; + + factory BoardNumberCellState.initial(GridCellController context) => + BoardNumberCellState( + content: context.getCellData() ?? "", + ); +} diff --git a/frontend/app_flowy/lib/plugins/board/application/card/board_url_cell_bloc.dart b/frontend/app_flowy/lib/plugins/board/application/card/board_url_cell_bloc.dart new file mode 100644 index 0000000000..045a1633fa --- /dev/null +++ b/frontend/app_flowy/lib/plugins/board/application/card/board_url_cell_bloc.dart @@ -0,0 +1,78 @@ +import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart'; +import 'package:flowy_sdk/protobuf/flowy-grid/url_type_option_entities.pb.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'dart:async'; + +part 'board_url_cell_bloc.freezed.dart'; + +class BoardURLCellBloc extends Bloc { + final GridURLCellController cellController; + void Function()? _onCellChangedFn; + BoardURLCellBloc({ + required this.cellController, + }) : super(BoardURLCellState.initial(cellController)) { + on( + (event, emit) async { + event.when( + initial: () { + _startListening(); + }, + didReceiveCellUpdate: (cellData) { + emit(state.copyWith( + content: cellData?.content ?? "", + url: cellData?.url ?? "", + )); + }, + updateURL: (String url) { + cellController.saveCellData(url, deduplicate: true); + }, + ); + }, + ); + } + + @override + Future close() async { + if (_onCellChangedFn != null) { + cellController.removeListener(_onCellChangedFn!); + _onCellChangedFn = null; + } + cellController.dispose(); + return super.close(); + } + + void _startListening() { + _onCellChangedFn = cellController.startListening( + onCellChanged: ((cellData) { + if (!isClosed) { + add(BoardURLCellEvent.didReceiveCellUpdate(cellData)); + } + }), + ); + } +} + +@freezed +class BoardURLCellEvent with _$BoardURLCellEvent { + const factory BoardURLCellEvent.initial() = _InitialCell; + const factory BoardURLCellEvent.updateURL(String url) = _UpdateURL; + const factory BoardURLCellEvent.didReceiveCellUpdate(URLCellDataPB? cell) = + _DidReceiveCellUpdate; +} + +@freezed +class BoardURLCellState with _$BoardURLCellState { + const factory BoardURLCellState({ + required String content, + required String url, + }) = _BoardURLCellState; + + factory BoardURLCellState.initial(GridURLCellController context) { + final cellData = context.getCellData(); + return BoardURLCellState( + content: cellData?.content ?? "", + url: cellData?.url ?? "", + ); + } +} diff --git a/frontend/app_flowy/lib/plugins/board/application/card/card_bloc.dart b/frontend/app_flowy/lib/plugins/board/application/card/card_bloc.dart new file mode 100644 index 0000000000..9ba66c2aab --- /dev/null +++ b/frontend/app_flowy/lib/plugins/board/application/card/card_bloc.dart @@ -0,0 +1,111 @@ +import 'dart:collection'; +import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart'; +import 'package:app_flowy/plugins/grid/application/row/row_cache.dart'; +import 'package:app_flowy/plugins/grid/application/row/row_service.dart'; +import 'package:equatable/equatable.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:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'dart:async'; + +import 'card_data_controller.dart'; + +part 'card_bloc.freezed.dart'; + +class BoardCardBloc extends Bloc { + final RowFFIService _rowService; + final CardDataController _dataController; + + BoardCardBloc({ + required String gridId, + required CardDataController dataController, + }) : _rowService = RowFFIService( + gridId: gridId, + blockId: dataController.rowPB.blockId, + rowId: dataController.rowPB.id, + ), + _dataController = dataController, + super(BoardCardState.initial( + dataController.rowPB, dataController.loadData())) { + on( + (event, emit) async { + await event.map( + initial: (_InitialRow value) async { + await _startListening(); + }, + createRow: (_CreateRow value) { + _rowService.createRow(); + }, + didReceiveCells: (_DidReceiveCells value) async { + final cells = value.gridCellMap.values + .map((e) => GridCellEquatable(e.field)) + .toList(); + emit(state.copyWith( + gridCellMap: value.gridCellMap, + cells: UnmodifiableListView(cells), + changeReason: value.reason, + )); + }, + ); + }, + ); + } + + @override + Future close() async { + _dataController.dispose(); + return super.close(); + } + + Future _startListening() async { + _dataController.addListener( + onRowChanged: (cells, reason) { + if (!isClosed) { + add(BoardCardEvent.didReceiveCells(cells, reason)); + } + }, + ); + } +} + +@freezed +class BoardCardEvent with _$BoardCardEvent { + const factory BoardCardEvent.initial() = _InitialRow; + const factory BoardCardEvent.createRow() = _CreateRow; + const factory BoardCardEvent.didReceiveCells( + GridCellMap gridCellMap, RowChangeReason reason) = _DidReceiveCells; +} + +@freezed +class BoardCardState with _$BoardCardState { + const factory BoardCardState({ + required RowPB rowPB, + required GridCellMap gridCellMap, + required UnmodifiableListView cells, + RowChangeReason? changeReason, + }) = _BoardCardState; + + factory BoardCardState.initial(RowPB rowPB, GridCellMap cellDataMap) => + BoardCardState( + rowPB: rowPB, + gridCellMap: cellDataMap, + cells: UnmodifiableListView( + cellDataMap.values.map((e) => GridCellEquatable(e.field)).toList(), + ), + ); +} + +class GridCellEquatable extends Equatable { + final FieldPB _field; + + const GridCellEquatable(FieldPB field) : _field = field; + + @override + List get props => [ + _field.id, + _field.fieldType, + _field.visibility, + _field.width, + ]; +} diff --git a/frontend/app_flowy/lib/plugins/board/application/card/card_data_controller.dart b/frontend/app_flowy/lib/plugins/board/application/card/card_data_controller.dart new file mode 100644 index 0000000000..d9ac41f10b --- /dev/null +++ b/frontend/app_flowy/lib/plugins/board/application/card/card_data_controller.dart @@ -0,0 +1,49 @@ +import 'package:app_flowy/plugins/board/presentation/card/card_cell_builder.dart'; +import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart'; +import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_field_notifier.dart'; +import 'package:app_flowy/plugins/grid/application/field/field_cache.dart'; +import 'package:app_flowy/plugins/grid/application/row/row_cache.dart'; +import 'package:flowy_sdk/protobuf/flowy-grid/block_entities.pb.dart'; +import 'package:flutter/foundation.dart'; + +typedef OnCardChanged = void Function(GridCellMap, RowChangeReason); + +class CardDataController extends BoardCellBuilderDelegate { + final RowPB rowPB; + final GridFieldCache _fieldCache; + final GridRowCache _rowCache; + final List _onCardChangedListeners = []; + + CardDataController({ + required this.rowPB, + required GridFieldCache fieldCache, + required GridRowCache rowCache, + }) : _fieldCache = fieldCache, + _rowCache = rowCache; + + GridCellMap loadData() { + return _rowCache.loadGridCells(rowPB.id); + } + + void addListener({OnCardChanged? onRowChanged}) { + _onCardChangedListeners.add(_rowCache.addListener( + rowId: rowPB.id, + onCellUpdated: onRowChanged, + )); + } + + void dispose() { + for (final fn in _onCardChangedListeners) { + _rowCache.removeRowListener(fn); + } + } + + @override + GridCellFieldNotifier buildFieldNotifier() { + return GridCellFieldNotifier( + notifier: GridCellFieldNotifierImpl(_fieldCache)); + } + + @override + GridCellCache get cellCache => _rowCache.cellCache; +} 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 f8367c1392..6dcfe8267c 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/board_page.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/board_page.dart @@ -1,5 +1,6 @@ // ignore_for_file: unused_field +import 'package:app_flowy/plugins/board/application/card/card_data_controller.dart'; 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'; @@ -7,6 +8,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../application/board_bloc.dart'; import 'card/card.dart'; +import 'card/card_cell_builder.dart'; class BoardPage extends StatelessWidget { final ViewPB view; @@ -49,12 +51,14 @@ class BoardContent extends StatelessWidget { return Container( color: Colors.white, child: Padding( - padding: const EdgeInsets.symmetric(vertical: 30, horizontal: 20), + padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 20), child: AFBoard( + key: UniqueKey(), + scrollController: ScrollController(), dataController: context.read().boardDataController, headerBuilder: _buildHeader, footBuilder: _buildFooter, - cardBuilder: _buildCard, + cardBuilder: (_, data) => _buildCard(context, data), columnConstraints: const BoxConstraints.tightFor(width: 240), config: AFBoardConfig( columnBackgroundColor: HexColor.fromHex('#F7F8FC'), @@ -87,10 +91,29 @@ class BoardContent extends StatelessWidget { } Widget _buildCard(BuildContext context, AFColumnItem item) { - final rowInfo = (item as BoardColumnItem).row; + final rowPB = (item as BoardColumnItem).row; + final rowCache = context.read().getRowCache(rowPB.blockId); + + /// Return placeholder widget if the rowCache is null. + if (rowCache == null) return SizedBox(key: ObjectKey(item)); + + final fieldCache = context.read().fieldCache; + final gridId = context.read().gridId; + final cardController = CardDataController( + fieldCache: fieldCache, + rowCache: rowCache, + rowPB: rowPB, + ); + + final cellBuilder = BoardCellBuilder(cardController); + return AppFlowyColumnItemCard( key: ObjectKey(item), - child: BoardCard(rowInfo: rowInfo), + child: BoardCard( + cellBuilder: cellBuilder, + dataController: cardController, + gridId: gridId, + ), ); } } diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/board_checkbox_cell.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/board_checkbox_cell.dart new file mode 100644 index 0000000000..c816964d3c --- /dev/null +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/board_checkbox_cell.dart @@ -0,0 +1,59 @@ +import 'package:app_flowy/plugins/board/application/card/board_checkbox_cell_bloc.dart'; +import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart'; +import 'package:flowy_infra/image.dart'; +import 'package:flowy_infra_ui/style_widget/icon_button.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class BoardCheckboxCell extends StatefulWidget { + final GridCellControllerBuilder cellControllerBuilder; + + const BoardCheckboxCell({ + required this.cellControllerBuilder, + Key? key, + }) : super(key: key); + + @override + State createState() => _BoardCheckboxCellState(); +} + +class _BoardCheckboxCellState extends State { + late BoardCheckboxCellBloc _cellBloc; + + @override + void initState() { + final cellController = + widget.cellControllerBuilder.build() as GridCheckboxCellController; + _cellBloc = BoardCheckboxCellBloc(cellController: cellController); + _cellBloc.add(const BoardCheckboxCellEvent.initial()); + super.initState(); + } + + @override + Widget build(BuildContext context) { + return BlocProvider.value( + value: _cellBloc, + child: BlocBuilder( + builder: (context, state) { + final icon = state.isSelected + ? svgWidget('editor/editor_check') + : svgWidget('editor/editor_uncheck'); + return Align( + alignment: Alignment.centerLeft, + child: FlowyIconButton( + iconPadding: EdgeInsets.zero, + icon: icon, + width: 20, + ), + ); + }, + ), + ); + } + + @override + Future dispose() async { + _cellBloc.close(); + super.dispose(); + } +} diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/board_date_cell.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/board_date_cell.dart new file mode 100644 index 0000000000..4a52d82116 --- /dev/null +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/board_date_cell.dart @@ -0,0 +1,59 @@ +import 'package:app_flowy/plugins/board/application/card/board_date_cell_bloc.dart'; +import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart'; +import 'package:flowy_infra_ui/style_widget/text.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class BoardDateCell extends StatefulWidget { + final GridCellControllerBuilder cellControllerBuilder; + + const BoardDateCell({ + required this.cellControllerBuilder, + Key? key, + }) : super(key: key); + + @override + State createState() => _BoardDateCellState(); +} + +class _BoardDateCellState extends State { + late BoardDateCellBloc _cellBloc; + + @override + void initState() { + final cellController = + widget.cellControllerBuilder.build() as GridDateCellController; + + _cellBloc = BoardDateCellBloc(cellController: cellController) + ..add(const BoardDateCellEvent.initial()); + super.initState(); + } + + @override + Widget build(BuildContext context) { + return BlocProvider.value( + value: _cellBloc, + child: BlocBuilder( + builder: (context, state) { + if (state.dateStr.isEmpty) { + return const SizedBox(); + } else { + return Align( + alignment: Alignment.centerLeft, + child: FlowyText.regular( + state.dateStr, + fontSize: 14, + ), + ); + } + }, + ), + ); + } + + @override + Future dispose() async { + _cellBloc.close(); + super.dispose(); + } +} diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/board_number_cell.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/board_number_cell.dart new file mode 100644 index 0000000000..096592583e --- /dev/null +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/board_number_cell.dart @@ -0,0 +1,59 @@ +import 'package:app_flowy/plugins/board/application/card/board_number_cell_bloc.dart'; +import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart'; +import 'package:flowy_infra_ui/style_widget/text.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class BoardNumberCell extends StatefulWidget { + final GridCellControllerBuilder cellControllerBuilder; + + const BoardNumberCell({ + required this.cellControllerBuilder, + Key? key, + }) : super(key: key); + + @override + State createState() => _BoardNumberCellState(); +} + +class _BoardNumberCellState extends State { + late BoardNumberCellBloc _cellBloc; + + @override + void initState() { + final cellController = + widget.cellControllerBuilder.build() as GridNumberCellController; + + _cellBloc = BoardNumberCellBloc(cellController: cellController) + ..add(const BoardNumberCellEvent.initial()); + super.initState(); + } + + @override + Widget build(BuildContext context) { + return BlocProvider.value( + value: _cellBloc, + child: BlocBuilder( + builder: (context, state) { + if (state.content.isEmpty) { + return const SizedBox(); + } else { + return Align( + alignment: Alignment.centerLeft, + child: FlowyText.regular( + state.content, + fontSize: 14, + ), + ); + } + }, + ), + ); + } + + @override + Future dispose() async { + _cellBloc.close(); + super.dispose(); + } +} diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/board_select_option_cell.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/board_select_option_cell.dart index 64a7a124c6..d430f869c1 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/card/board_select_option_cell.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/board_select_option_cell.dart @@ -1,6 +1,6 @@ import 'package:app_flowy/plugins/board/application/card/board_select_option_cell_bloc.dart'; import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart'; -import 'package:app_flowy/plugins/grid/presentation/widgets/cell/select_option_cell/select_option_cell.dart'; +import 'package:app_flowy/plugins/grid/presentation/widgets/cell/select_option_cell/extension.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -34,9 +34,15 @@ class _BoardSelectOptionCellState extends State { value: _cellBloc, child: BlocBuilder( builder: (context, state) { - return SelectOptionWrap( - selectOptions: state.selectedOptions, - cellControllerBuilder: widget.cellControllerBuilder, + final children = state.selectedOptions + .map((option) => SelectOptionTag.fromOption( + context: context, + option: option, + )) + .toList(); + return Align( + alignment: Alignment.centerLeft, + child: Wrap(children: children, spacing: 4, runSpacing: 2), ); }, ), diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/board_text_cell.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/board_text_cell.dart index fd3b89a9ae..8cb5c4987e 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/card/board_text_cell.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/board_text_cell.dart @@ -32,10 +32,17 @@ class _BoardTextCellState extends State { value: _cellBloc, child: BlocBuilder( builder: (context, state) { - return SizedBox( - height: 30, - child: FlowyText.medium(state.content), - ); + if (state.content.isEmpty) { + return const SizedBox(); + } else { + return Align( + alignment: Alignment.centerLeft, + child: FlowyText.regular( + state.content, + fontSize: 14, + ), + ); + } }, ), ); diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/board_url_cell.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/board_url_cell.dart new file mode 100644 index 0000000000..5493b0d45b --- /dev/null +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/board_url_cell.dart @@ -0,0 +1,64 @@ +import 'package:app_flowy/plugins/board/application/card/board_url_cell_bloc.dart'; +import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart'; +import 'package:flowy_infra/theme.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class BoardUrlCell extends StatefulWidget { + final GridCellControllerBuilder cellControllerBuilder; + + const BoardUrlCell({ + required this.cellControllerBuilder, + Key? key, + }) : super(key: key); + + @override + State createState() => _BoardUrlCellState(); +} + +class _BoardUrlCellState extends State { + late BoardURLCellBloc _cellBloc; + + @override + void initState() { + final cellController = + widget.cellControllerBuilder.build() as GridURLCellController; + _cellBloc = BoardURLCellBloc(cellController: cellController); + _cellBloc.add(const BoardURLCellEvent.initial()); + super.initState(); + } + + @override + Widget build(BuildContext context) { + final theme = context.watch(); + return BlocProvider.value( + value: _cellBloc, + child: BlocBuilder( + builder: (context, state) { + final richText = RichText( + textAlign: TextAlign.left, + text: TextSpan( + text: state.content, + style: TextStyle( + color: theme.main2, + fontSize: 14, + decoration: TextDecoration.underline, + ), + ), + ); + + return Align( + alignment: Alignment.centerLeft, + child: richText, + ); + }, + ), + ); + } + + @override + Future dispose() async { + _cellBloc.close(); + super.dispose(); + } +} diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/card.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/card.dart index 020cc49db3..dfb9ab42d7 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/card/card.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/card.dart @@ -1,13 +1,85 @@ -import 'package:app_flowy/plugins/grid/application/row/row_cache.dart'; +import 'package:app_flowy/plugins/board/application/card/card_bloc.dart'; +import 'package:app_flowy/plugins/board/application/card/card_data_controller.dart'; +import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart'; +import 'package:flowy_infra/image.dart'; +import 'package:flowy_infra/theme.dart'; +import 'package:flowy_sdk/log.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'card_cell_builder.dart'; +import 'card_container.dart'; -class BoardCard extends StatelessWidget { - final RowInfo rowInfo; +class BoardCard extends StatefulWidget { + final String gridId; + final CardDataController dataController; + final BoardCellBuilder cellBuilder; - const BoardCard({required this.rowInfo, Key? key}) : super(key: key); + const BoardCard({ + required this.gridId, + required this.dataController, + required this.cellBuilder, + Key? key, + }) : super(key: key); + + @override + State createState() => _BoardCardState(); +} + +class _BoardCardState extends State { + late BoardCardBloc _cardBloc; + + @override + void initState() { + _cardBloc = BoardCardBloc( + gridId: widget.gridId, + dataController: widget.dataController, + ); + super.initState(); + } @override Widget build(BuildContext context) { - return const SizedBox(height: 20, child: Text('1234')); + return BlocProvider.value( + value: _cardBloc, + child: BlocBuilder( + builder: (context, state) { + return BoardCardContainer( + accessoryBuilder: (context) { + return [const _CardMoreOption()]; + }, + child: Column( + children: _makeCells(context, state.gridCellMap), + ), + ); + }, + ), + ); + } + + List _makeCells(BuildContext context, GridCellMap cellMap) { + return cellMap.values.map( + (cellId) { + final child = widget.cellBuilder.buildCell(cellId); + + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), + child: child, + ); + }, + ).toList(); + } +} + +class _CardMoreOption extends StatelessWidget with CardAccessory { + const _CardMoreOption({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return svgWidget('home/details', color: context.read().iconColor); + } + + @override + void onTap(BuildContext context) { + Log.debug('show options'); } } diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/card_cell_builder.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/card_cell_builder.dart index e69de29bb2..10ae0db680 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/card/card_cell_builder.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/card_cell_builder.dart @@ -0,0 +1,69 @@ +import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart'; +import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart'; +import 'package:flutter/material.dart'; + +import 'board_checkbox_cell.dart'; +import 'board_date_cell.dart'; +import 'board_number_cell.dart'; +import 'board_select_option_cell.dart'; +import 'board_text_cell.dart'; +import 'board_url_cell.dart'; + +abstract class BoardCellBuilderDelegate + extends GridCellControllerBuilderDelegate { + GridCellCache get cellCache; +} + +class BoardCellBuilder { + final BoardCellBuilderDelegate delegate; + + BoardCellBuilder(this.delegate); + + Widget buildCell(GridCellIdentifier cellId) { + final cellControllerBuilder = GridCellControllerBuilder( + delegate: delegate, + cellId: cellId, + cellCache: delegate.cellCache, + ); + + final key = cellId.key(); + switch (cellId.fieldType) { + case FieldType.Checkbox: + return BoardCheckboxCell( + cellControllerBuilder: cellControllerBuilder, + key: key, + ); + case FieldType.DateTime: + return BoardDateCell( + cellControllerBuilder: cellControllerBuilder, + key: key, + ); + case FieldType.SingleSelect: + return BoardSelectOptionCell( + cellControllerBuilder: cellControllerBuilder, + key: key, + ); + case FieldType.MultiSelect: + return BoardSelectOptionCell( + cellControllerBuilder: cellControllerBuilder, + key: key, + ); + case FieldType.Number: + return BoardNumberCell( + cellControllerBuilder: cellControllerBuilder, + key: key, + ); + case FieldType.RichText: + return BoardTextCell( + cellControllerBuilder: cellControllerBuilder, + key: key, + ); + case FieldType.URL: + return BoardUrlCell( + cellControllerBuilder: cellControllerBuilder, + key: key, + ); + } + throw UnimplementedError; + } +} diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/card_container.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/card_container.dart new file mode 100644 index 0000000000..13f3af2195 --- /dev/null +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/card_container.dart @@ -0,0 +1,133 @@ +import 'package:flowy_infra/theme.dart'; +import 'package:flowy_infra_ui/style_widget/hover.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:styled_widget/styled_widget.dart'; + +class BoardCardContainer extends StatelessWidget { + final Widget child; + final CardAccessoryBuilder? accessoryBuilder; + const BoardCardContainer({ + required this.child, + this.accessoryBuilder, + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return ChangeNotifierProvider( + create: (_) => _CardContainerNotifier(), + child: Consumer<_CardContainerNotifier>( + builder: (context, notifier, _) { + Widget container = Center(child: child); + if (accessoryBuilder != null) { + final accessories = accessoryBuilder!(context); + if (accessories.isNotEmpty) { + container = _CardEnterRegion( + child: container, + accessories: accessories, + ); + } + } + return Padding( + padding: const EdgeInsets.all(8), + child: container, + ); + }, + ), + ); + } +} + +abstract class CardAccessory implements Widget { + void onTap(BuildContext context); +} + +typedef CardAccessoryBuilder = List Function( + BuildContext buildContext, +); + +class CardAccessoryContainer extends StatelessWidget { + final List accessories; + const CardAccessoryContainer({required this.accessories, Key? key}) + : super(key: key); + + @override + Widget build(BuildContext context) { + final theme = context.read(); + final children = accessories.map((accessory) { + final hover = FlowyHover( + style: HoverStyle( + hoverColor: theme.hover, + backgroundColor: theme.surface, + ), + builder: (_, onHover) => Container( + width: 26, + height: 26, + padding: const EdgeInsets.all(3), + child: accessory, + ), + ); + return GestureDetector( + child: hover, + behavior: HitTestBehavior.opaque, + onTap: () => accessory.onTap(context), + ); + }).toList(); + + return Wrap(children: children, spacing: 6); + } +} + +class _CardEnterRegion extends StatelessWidget { + final Widget child; + final List accessories; + const _CardEnterRegion( + {required this.child, required this.accessories, Key? key}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return Selector<_CardContainerNotifier, bool>( + selector: (context, notifier) => notifier.onEnter, + builder: (context, onEnter, _) { + List children = [child]; + if (onEnter) { + children.add(CardAccessoryContainer(accessories: accessories) + .positioned(right: 0)); + } + + return MouseRegion( + cursor: SystemMouseCursors.click, + onEnter: (p) => + Provider.of<_CardContainerNotifier>(context, listen: false) + .onEnter = true, + onExit: (p) => + Provider.of<_CardContainerNotifier>(context, listen: false) + .onEnter = false, + child: IntrinsicHeight( + child: Stack( + alignment: AlignmentDirectional.center, + fit: StackFit.expand, + children: children, + )), + ); + }, + ); + } +} + +class _CardContainerNotifier extends ChangeNotifier { + bool _onEnter = false; + + _CardContainerNotifier(); + + set onEnter(bool value) { + if (_onEnter != value) { + _onEnter = value; + notifyListeners(); + } + } + + bool get onEnter => _onEnter; +} diff --git a/frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/context_builder.dart b/frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/context_builder.dart index b7c68a7937..c8e92809d7 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/context_builder.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/context_builder.dart @@ -1,6 +1,8 @@ part of 'cell_service.dart'; typedef GridCellController = IGridCellController; +typedef GridCheckboxCellController = IGridCellController; +typedef GridNumberCellController = IGridCellController; typedef GridSelectOptionCellController = IGridCellController; typedef GridDateCellController @@ -58,7 +60,7 @@ class GridCellControllerBuilder { parser: StringCellDataParser(), reloadOnFieldChanged: true, ); - return GridCellController( + return GridNumberCellController( cellId: _cellId, cellCache: _cellCache, cellDataLoader: cellDataLoader, @@ -127,7 +129,7 @@ class IGridCellController extends Equatable { final GridCellDataLoader _cellDataLoader; final IGridCellDataPersistence _cellDataPersistence; - late final CellListener _cellListener; + CellListener? _cellListener; ValueNotifier? _cellDataNotifier; bool isListening = false; @@ -186,7 +188,7 @@ class IGridCellController extends Equatable { /// For example: /// user input: 12 /// cell display: $12 - _cellListener.start(onCellChanged: (result) { + _cellListener?.start(onCellChanged: (result) { result.fold( (_) => _loadData(), (err) => Log.error(err), @@ -289,7 +291,7 @@ class IGridCellController extends Equatable { return; } _isDispose = true; - _cellListener.stop(); + _cellListener?.stop(); _loadDataOperation?.cancel(); _saveDataOperation?.cancel(); _cellDataNotifier = null; diff --git a/frontend/app_flowy/lib/plugins/grid/application/cell/checkbox_cell_bloc.dart b/frontend/app_flowy/lib/plugins/grid/application/cell/checkbox_cell_bloc.dart index 44b94a3ddf..f5e7a451e2 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/cell/checkbox_cell_bloc.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/cell/checkbox_cell_bloc.dart @@ -6,7 +6,7 @@ import 'cell_service/cell_service.dart'; part 'checkbox_cell_bloc.freezed.dart'; class CheckboxCellBloc extends Bloc { - final GridCellController cellController; + final GridCheckboxCellController cellController; void Function()? _onCellChangedFn; CheckboxCellBloc({ diff --git a/frontend/app_flowy/lib/plugins/grid/application/cell/number_cell_bloc.dart b/frontend/app_flowy/lib/plugins/grid/application/cell/number_cell_bloc.dart index 88b28ea414..2ca989289f 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/cell/number_cell_bloc.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/cell/number_cell_bloc.dart @@ -8,7 +8,7 @@ import 'cell_service/cell_service.dart'; part 'number_cell_bloc.freezed.dart'; class NumberCellBloc extends Bloc { - final GridCellController cellController; + final GridNumberCellController cellController; void Function()? _onCellChangedFn; NumberCellBloc({ diff --git a/frontend/app_flowy/lib/plugins/grid/application/grid_data_controller.dart b/frontend/app_flowy/lib/plugins/grid/application/grid_data_controller.dart index 32488599ea..aae6dc684e 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/grid_data_controller.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/grid_data_controller.dart @@ -46,7 +46,7 @@ class GridDataController { GridDataController({required ViewPB view}) : gridId = view.id, - _blocks = LinkedHashMap.identity(), + _blocks = LinkedHashMap.new(), _gridFFIService = GridService(gridId: view.id), fieldCache = GridFieldCache(gridId: view.id); diff --git a/frontend/app_flowy/lib/plugins/grid/application/row/row_action_sheet_bloc.dart b/frontend/app_flowy/lib/plugins/grid/application/row/row_action_sheet_bloc.dart index 0b4499682f..7e3e9a21bd 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/row/row_action_sheet_bloc.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/row/row_action_sheet_bloc.dart @@ -12,10 +12,10 @@ part 'row_action_sheet_bloc.freezed.dart'; class RowActionSheetBloc extends Bloc { - final RowService _rowService; + final RowFFIService _rowService; RowActionSheetBloc({required RowInfo rowData}) - : _rowService = RowService( + : _rowService = RowFFIService( gridId: rowData.gridId, blockId: rowData.blockId, rowId: rowData.id, diff --git a/frontend/app_flowy/lib/plugins/grid/application/row/row_bloc.dart b/frontend/app_flowy/lib/plugins/grid/application/row/row_bloc.dart index e6a68cd080..372287fd1f 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/row/row_bloc.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/row/row_bloc.dart @@ -12,13 +12,13 @@ import 'row_service.dart'; part 'row_bloc.freezed.dart'; class RowBloc extends Bloc { - final RowService _rowService; + final RowFFIService _rowService; final GridRowDataController _dataController; RowBloc({ required RowInfo rowInfo, required GridRowDataController dataController, - }) : _rowService = RowService( + }) : _rowService = RowFFIService( gridId: rowInfo.gridId, blockId: rowInfo.blockId, rowId: rowInfo.id, @@ -35,13 +35,12 @@ class RowBloc extends Bloc { _rowService.createRow(); }, didReceiveCells: (_DidReceiveCells value) async { - final fields = value.gridCellMap.values + final cells = value.gridCellMap.values .map((e) => GridCellEquatable(e.field)) .toList(); - final snapshots = UnmodifiableListView(fields); emit(state.copyWith( gridCellMap: value.gridCellMap, - snapshots: snapshots, + cells: UnmodifiableListView(cells), changeReason: value.reason, )); }, @@ -80,7 +79,7 @@ class RowState with _$RowState { const factory RowState({ required RowInfo rowInfo, required GridCellMap gridCellMap, - required UnmodifiableListView snapshots, + required UnmodifiableListView cells, RowChangeReason? changeReason, }) = _RowState; @@ -88,8 +87,9 @@ class RowState with _$RowState { RowState( rowInfo: rowInfo, gridCellMap: cellDataMap, - snapshots: UnmodifiableListView( - cellDataMap.values.map((e) => GridCellEquatable(e.field)).toList()), + cells: UnmodifiableListView( + cellDataMap.values.map((e) => GridCellEquatable(e.field)).toList(), + ), ); } diff --git a/frontend/app_flowy/lib/plugins/grid/application/row/row_data_controller.dart b/frontend/app_flowy/lib/plugins/grid/application/row/row_data_controller.dart index 31a54aa29b..78783fc894 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/row/row_data_controller.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/row/row_data_controller.dart @@ -13,10 +13,6 @@ class GridRowDataController extends GridCellBuilderDelegate { final GridFieldCache _fieldCache; final GridRowCache _rowCache; - GridFieldCache get fieldCache => _fieldCache; - - GridRowCache get rowCache => _rowCache; - GridRowDataController({ required this.rowInfo, required GridFieldCache fieldCache, @@ -49,5 +45,5 @@ class GridRowDataController extends GridCellBuilderDelegate { } @override - GridCellCache get cellCache => rowCache.cellCache; + GridCellCache get cellCache => _rowCache.cellCache; } diff --git a/frontend/app_flowy/lib/plugins/grid/application/row/row_service.dart b/frontend/app_flowy/lib/plugins/grid/application/row/row_service.dart index 94e047c1f7..0f056a4006 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/row/row_service.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/row/row_service.dart @@ -5,12 +5,12 @@ import 'package:flowy_sdk/protobuf/flowy-grid/block_entities.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-grid/grid_entities.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-grid/row_entities.pb.dart'; -class RowService { +class RowFFIService { final String gridId; final String blockId; final String rowId; - RowService( + RowFFIService( {required this.gridId, required this.blockId, required this.rowId}); Future> createRow() { diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/cell_accessory.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/cell_accessory.dart index e41f9bc9ac..9b3f281130 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/cell_accessory.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/cell_accessory.dart @@ -8,6 +8,8 @@ import 'package:styled_widget/styled_widget.dart'; import 'package:app_flowy/generated/locale_keys.g.dart'; import 'package:easy_localization/easy_localization.dart'; +import 'cell_builder.dart'; + class GridCellAccessoryBuildContext { final BuildContext anchorContext; final bool isCellEditing; @@ -57,18 +59,6 @@ class PrimaryCellAccessory extends StatelessWidget with GridCellAccessory { bool enable() => !isCellEditing; } -typedef AccessoryBuilder = List Function( - GridCellAccessoryBuildContext buildContext); - -abstract class CellAccessory extends Widget { - const CellAccessory({Key? key}) : super(key: key); - - // The hover will show if the isHover's value is true - ValueNotifier? get onAccessoryHover; - - AccessoryBuilder? get accessoryBuilder; -} - class AccessoryHover extends StatefulWidget { final CellAccessory child; final EdgeInsets contentPadding; diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/cell_builder.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/cell_builder.dart index 6c3fa38bc1..0a7c3a48a7 100755 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/cell_builder.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/cell_builder.dart @@ -94,6 +94,18 @@ abstract class CellEditable { ValueNotifier get onCellEditing; } +typedef AccessoryBuilder = List Function( + GridCellAccessoryBuildContext buildContext); + +abstract class CellAccessory extends Widget { + const CellAccessory({Key? key}) : super(key: key); + + // The hover will show if the isHover's value is true + ValueNotifier? get onAccessoryHover; + + AccessoryBuilder? get accessoryBuilder; +} + abstract class GridCellWidget extends StatefulWidget implements CellAccessory, CellEditable, CellShortcuts { GridCellWidget({Key? key}) : super(key: key) { diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/cell_container.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/cell_container.dart index b2d174e3e2..ed09ec3f36 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/cell_container.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/cell_container.dart @@ -25,24 +25,28 @@ class CellContainer extends StatelessWidget { @override Widget build(BuildContext context) { return ChangeNotifierProxyProvider( - create: (_) => CellContainerNotifier(child), + _CellContainerNotifier>( + create: (_) => _CellContainerNotifier(child), update: (_, rowStateNotifier, cellStateNotifier) => cellStateNotifier!..onEnter = rowStateNotifier.onEnter, - child: Selector( + child: Selector<_CellContainerNotifier, bool>( selector: (context, notifier) => notifier.isFocus, builder: (context, isFocus, _) { Widget container = Center(child: GridCellShortcuts(child: child)); if (accessoryBuilder != null) { - final accessories = accessoryBuilder!(GridCellAccessoryBuildContext( - anchorContext: context, - isCellEditing: isFocus, - )); + final accessories = accessoryBuilder!( + GridCellAccessoryBuildContext( + anchorContext: context, + isCellEditing: isFocus, + ), + ); if (accessories.isNotEmpty) { - container = - CellEnterRegion(child: container, accessories: accessories); + container = _GridCellEnterRegion( + child: container, + accessories: accessories, + ); } } @@ -74,16 +78,16 @@ class CellContainer extends StatelessWidget { } } -class CellEnterRegion extends StatelessWidget { +class _GridCellEnterRegion extends StatelessWidget { final Widget child; final List accessories; - const CellEnterRegion( + const _GridCellEnterRegion( {required this.child, required this.accessories, Key? key}) : super(key: key); @override Widget build(BuildContext context) { - return Selector( + return Selector<_CellContainerNotifier, bool>( selector: (context, notifier) => notifier.onEnter, builder: (context, onEnter, _) { List children = [child]; @@ -95,10 +99,10 @@ class CellEnterRegion extends StatelessWidget { return MouseRegion( cursor: SystemMouseCursors.click, onEnter: (p) => - Provider.of(context, listen: false) + Provider.of<_CellContainerNotifier>(context, listen: false) .onEnter = true, onExit: (p) => - Provider.of(context, listen: false) + Provider.of<_CellContainerNotifier>(context, listen: false) .onEnter = false, child: Stack( alignment: AlignmentDirectional.center, @@ -111,13 +115,13 @@ class CellEnterRegion extends StatelessWidget { } } -class CellContainerNotifier extends ChangeNotifier { +class _CellContainerNotifier extends ChangeNotifier { final CellEditable cellEditable; VoidCallback? _onCellFocusListener; bool _isFocus = false; bool _onEnter = false; - CellContainerNotifier(this.cellEditable) { + _CellContainerNotifier(this.cellEditable) { _onCellFocusListener = () => isFocus = cellEditable.onCellFocus.value; cellEditable.onCellFocus.addListener(_onCellFocusListener!); } diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/checkbox_cell.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/checkbox_cell.dart index ac80303f0d..adffa3ef16 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/checkbox_cell.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/checkbox_cell.dart @@ -22,7 +22,8 @@ class _CheckboxCellState extends GridCellState { @override void initState() { - final cellController = widget.cellControllerBuilder.build(); + final cellController = + widget.cellControllerBuilder.build() as GridCheckboxCellController; _cellBloc = getIt(param1: cellController) ..add(const CheckboxCellEvent.initial()); super.initState(); diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/select_option_cell/extension.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/select_option_cell/extension.dart index 6946993bae..6fdd8bf6f8 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/select_option_cell/extension.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/select_option_cell/extension.dart @@ -73,7 +73,7 @@ class SelectOptionTag extends StatelessWidget { Key? key, }) : super(key: key); - factory SelectOptionTag.fromSelectOption({ + factory SelectOptionTag.fromOption({ required BuildContext context, required SelectOptionPB option, VoidCallback? onSelected, @@ -91,7 +91,8 @@ class SelectOptionTag extends StatelessWidget { Widget build(BuildContext context) { return ChoiceChip( pressElevation: 1, - label: FlowyText.medium(name, fontSize: 12, overflow: TextOverflow.ellipsis), + label: + FlowyText.medium(name, fontSize: 12, overflow: TextOverflow.ellipsis), selectedColor: color, backgroundColor: color, labelPadding: const EdgeInsets.symmetric(horizontal: 6), @@ -133,7 +134,7 @@ class SelectOptionTagCell extends StatelessWidget { Flexible( fit: FlexFit.loose, flex: 2, - child: SelectOptionTag.fromSelectOption( + child: SelectOptionTag.fromOption( context: context, option: option, onSelected: () => onSelected(option), diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/select_option_cell/select_option_cell.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/select_option_cell/select_option_cell.dart index 1bd54e7514..a8d3993a2f 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/select_option_cell/select_option_cell.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/select_option_cell/select_option_cell.dart @@ -153,21 +153,25 @@ class SelectOptionWrap extends StatelessWidget { if (selectOptions.isEmpty && cellStyle != null) { child = Align( alignment: Alignment.centerLeft, - child: FlowyText.medium(cellStyle!.placeholder, - fontSize: 14, color: theme.shader3), + child: FlowyText.medium( + cellStyle!.placeholder, + fontSize: 14, + color: theme.shader3, + ), ); } else { - final tags = selectOptions - .map( - (option) => SelectOptionTag.fromSelectOption( - context: context, - option: option, - ), - ) - .toList(); child = Align( alignment: Alignment.centerLeft, - child: Wrap(children: tags, spacing: 4, runSpacing: 2), + child: Wrap( + children: selectOptions + .map((option) => SelectOptionTag.fromOption( + context: context, + option: option, + )) + .toList(), + spacing: 4, + runSpacing: 2, + ), ); } @@ -176,15 +180,14 @@ class SelectOptionWrap extends StatelessWidget { fit: StackFit.expand, children: [ child, - InkWell( - onTap: () { - onFocus?.call(true); - final cellContext = - cellControllerBuilder.build() as GridSelectOptionCellController; - SelectOptionCellEditor.show( - context, cellContext, () => onFocus?.call(false)); - }, - ), + InkWell(onTap: () { + onFocus?.call(true); + SelectOptionCellEditor.show( + context, + cellControllerBuilder.build() as GridSelectOptionCellController, + () => onFocus?.call(false), + ); + }), ], ); } diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/select_option_cell/text_field.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/select_option_cell/text_field.dart index 10b04cfb58..5482a403cc 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/select_option_cell/text_field.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/select_option_cell/text_field.dart @@ -49,7 +49,8 @@ class SelectOptionTextField extends StatelessWidget { initialTags: selectedOptionMap.keys.toList(), focusNode: _focusNode, textSeparators: const [' ', ','], - inputfieldBuilder: (BuildContext context, editController, focusNode, error, onChanged, onSubmitted) { + inputfieldBuilder: (BuildContext context, editController, focusNode, + error, onChanged, onSubmitted) { return ((context, sc, tags, onTagDelegate) { return TextField( autofocus: true, @@ -99,7 +100,8 @@ class SelectOptionTextField extends StatelessWidget { } final children = selectedOptionMap.values - .map((option) => SelectOptionTag.fromSelectOption(context: context, option: option)) + .map((option) => + SelectOptionTag.fromOption(context: context, option: option)) .toList(); return Padding( padding: const EdgeInsets.all(8.0), diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/url_cell/url_cell.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/url_cell/url_cell.dart index 585ef1ce85..102595e166 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/url_cell/url_cell.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/url_cell/url_cell.dart @@ -90,9 +90,9 @@ class _GridURLCellState extends GridCellState { @override void initState() { - final cellContext = + final cellController = widget.cellControllerBuilder.build() as GridURLCellController; - _cellBloc = URLCellBloc(cellController: cellContext); + _cellBloc = URLCellBloc(cellController: cellController); _cellBloc.add(const URLCellEvent.initial()); super.initState(); } diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/row/grid_row.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/row/grid_row.dart index 6c995d57eb..c96b5e1526 100755 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/row/grid_row.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/row/grid_row.dart @@ -164,7 +164,7 @@ class RowContent extends StatelessWidget { Widget build(BuildContext context) { return BlocBuilder( buildWhen: (previous, current) => - !listEquals(previous.snapshots, current.snapshots), + !listEquals(previous.cells, current.cells), builder: (context, state) { return IntrinsicHeight( child: Row( @@ -181,28 +181,27 @@ class RowContent extends StatelessWidget { return gridCellMap.values.map( (cellId) { final GridCellWidget child = builder.build(cellId); - accessoryBuilder(GridCellAccessoryBuildContext buildContext) { - final builder = child.accessoryBuilder; - List accessories = []; - if (cellId.field.isPrimary) { - accessories.add(PrimaryCellAccessory( - onTapCallback: onExpand, - isCellEditing: buildContext.isCellEditing, - )); - } - - if (builder != null) { - accessories.addAll(builder(buildContext)); - } - return accessories; - } return CellContainer( width: cellId.field.width.toDouble(), child: child, rowStateNotifier: Provider.of(context, listen: false), - accessoryBuilder: accessoryBuilder, + accessoryBuilder: (buildContext) { + final builder = child.accessoryBuilder; + List accessories = []; + if (cellId.field.isPrimary) { + accessories.add(PrimaryCellAccessory( + onTapCallback: onExpand, + isCellEditing: buildContext.isCellEditing, + )); + } + + if (builder != null) { + accessories.addAll(builder(buildContext)); + } + return accessories; + }, ); }, ).toList(); diff --git a/frontend/app_flowy/packages/appflowy_board/example/lib/multi_board_list_example.dart b/frontend/app_flowy/packages/appflowy_board/example/lib/multi_board_list_example.dart index 282e6028c5..83f75d2a0e 100644 --- a/frontend/app_flowy/packages/appflowy_board/example/lib/multi_board_list_example.dart +++ b/frontend/app_flowy/packages/appflowy_board/example/lib/multi_board_list_example.dart @@ -23,18 +23,19 @@ class _MultiBoardListExampleState extends State { @override void initState() { - final column1 = AFBoardColumnData(id: "To Do", items: [ + List a = [ TextItem("Card 1"), TextItem("Card 2"), - RichTextItem(title: "Card 3", subtitle: 'Aug 1, 2020 4:05 PM'), + // RichTextItem(title: "Card 3", subtitle: 'Aug 1, 2020 4:05 PM'), TextItem("Card 4"), - ]); - final column2 = AFBoardColumnData(id: "In Progress", items: [ - RichTextItem(title: "Card 5", subtitle: 'Aug 1, 2020 4:05 PM'), - TextItem("Card 6"), + ]; + final column1 = AFBoardColumnData(id: "To Do", items: a); + final column2 = AFBoardColumnData(id: "In Progress", items: [ + // RichTextItem(title: "Card 5", subtitle: 'Aug 1, 2020 4:05 PM'), + // TextItem("Card 6"), ]); - final column3 = AFBoardColumnData(id: "Done", items: []); + final column3 = AFBoardColumnData(id: "Done", items: []); boardDataController.addColumn(column1); boardDataController.addColumn(column2); diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board.dart index 03d9ca1750..f4ce09fc2c 100644 --- a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board.dart +++ b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board.dart @@ -46,6 +46,8 @@ class AFBoard extends StatelessWidget { /// final BoardPhantomController phantomController; + final ScrollController? scrollController; + final AFBoardConfig config; AFBoard({ @@ -54,6 +56,7 @@ class AFBoard extends StatelessWidget { this.background, this.footBuilder, this.headerBuilder, + this.scrollController, this.columnConstraints = const BoxConstraints(maxWidth: 200), this.config = const AFBoardConfig(), Key? key, @@ -69,6 +72,7 @@ class AFBoard extends StatelessWidget { return BoardContent( config: config, dataController: dataController, + scrollController: scrollController, background: background, delegate: phantomController, columnConstraints: columnConstraints, @@ -202,7 +206,7 @@ class _BoardContentState extends State { return ChangeNotifierProvider.value( key: ValueKey(columnData.id), value: widget.dataController.columnController(columnData.id), - child: Consumer( + child: Consumer( builder: (context, value, child) { return ConstrainedBox( constraints: widget.columnConstraints, 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 cfce93af00..847f07c86e 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 @@ -12,7 +12,7 @@ abstract class AFColumnItem extends ReoderFlexItem { String toString() => id; } -/// [BoardColumnDataController] is used to handle the [AFBoardColumnData]. +/// [AFBoardColumnDataController] is used to handle the [AFBoardColumnData]. /// * Remove an item by calling [removeAt] method. /// * Move item to another position by calling [move] method. /// * Insert item to index by calling [insert] method @@ -20,10 +20,10 @@ abstract class AFColumnItem extends ReoderFlexItem { /// /// All there operations will notify listeners by default. /// -class BoardColumnDataController extends ChangeNotifier with EquatableMixin { +class AFBoardColumnDataController extends ChangeNotifier with EquatableMixin { final AFBoardColumnData columnData; - BoardColumnDataController({ + AFBoardColumnDataController({ required this.columnData, }); @@ -42,7 +42,8 @@ class BoardColumnDataController extends ChangeNotifier with EquatableMixin { AFColumnItem removeAt(int index, {bool notify = true}) { assert(index >= 0); - Log.debug('[$BoardColumnDataController] $columnData remove item at $index'); + Log.debug( + '[$AFBoardColumnDataController] $columnData remove item at $index'); final item = columnData._items.removeAt(index); if (notify) { notifyListeners(); @@ -64,7 +65,7 @@ class BoardColumnDataController extends ChangeNotifier with EquatableMixin { return false; } Log.debug( - '[$BoardColumnDataController] $columnData move item from $fromIndex to $toIndex'); + '[$AFBoardColumnDataController] $columnData move item from $fromIndex to $toIndex'); final item = columnData._items.removeAt(fromIndex); columnData._items.insert(toIndex, item); notifyListeners(); @@ -78,7 +79,7 @@ class BoardColumnDataController extends ChangeNotifier with EquatableMixin { bool insert(int index, AFColumnItem item, {bool notify = true}) { assert(index >= 0); Log.debug( - '[$BoardColumnDataController] $columnData insert $item at $index'); + '[$AFBoardColumnDataController] $columnData insert $item at $index'); if (columnData._items.length > index) { columnData._items.insert(index, item); @@ -100,12 +101,12 @@ class BoardColumnDataController extends ChangeNotifier with EquatableMixin { void replace(int index, AFColumnItem newItem) { if (columnData._items.isEmpty) { columnData._items.add(newItem); - Log.debug('[$BoardColumnDataController] $columnData add $newItem'); + Log.debug('[$AFBoardColumnDataController] $columnData add $newItem'); } else { final removedItem = columnData._items.removeAt(index); columnData._items.insert(index, newItem); Log.debug( - '[$BoardColumnDataController] $columnData replace $removedItem with $newItem at $index'); + '[$AFBoardColumnDataController] $columnData replace $removedItem with $newItem at $index'); } notifyListeners(); 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 00c84daa3c..6e3f45fc7d 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 @@ -35,7 +35,7 @@ class AFBoardDataController extends ChangeNotifier List get columnIds => _columnDatas.map((columnData) => columnData.id).toList(); - final LinkedHashMap _columnControllers = + final LinkedHashMap _columnControllers = LinkedHashMap(); AFBoardDataController({ @@ -47,7 +47,7 @@ class AFBoardDataController extends ChangeNotifier void addColumn(AFBoardColumnData columnData, {bool notify = true}) { if (_columnControllers[columnData.id] != null) return; - final controller = BoardColumnDataController(columnData: columnData); + final controller = AFBoardColumnDataController(columnData: columnData); _columnDatas.add(columnData); _columnControllers[columnData.id] = controller; if (notify) notifyListeners(); @@ -84,11 +84,11 @@ class AFBoardDataController extends ChangeNotifier if (columnIds.isNotEmpty && notify) notifyListeners(); } - BoardColumnDataController columnController(String columnId) { + AFBoardColumnDataController columnController(String columnId) { return _columnControllers[columnId]!; } - BoardColumnDataController? getColumnController(String columnId) { + AFBoardColumnDataController? getColumnController(String columnId) { final columnController = _columnControllers[columnId]; if (columnController == null) { Log.warn('Column:[$columnId] \'s controller is not exist'); @@ -153,7 +153,7 @@ class AFBoardDataController extends ChangeNotifier } @override - BoardColumnDataController? controller(String columnId) { + AFBoardColumnDataController? controller(String columnId) { return _columnControllers[columnId]; } diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_phantom/phantom_controller.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_phantom/phantom_controller.dart index bd770fa820..1ab7b2da23 100644 --- a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_phantom/phantom_controller.dart +++ b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_phantom/phantom_controller.dart @@ -7,7 +7,7 @@ import '../reorder_flex/drag_target_inteceptor.dart'; import 'phantom_state.dart'; abstract class BoardPhantomControllerDelegate { - BoardColumnDataController? controller(String columnId); + AFBoardColumnDataController? controller(String columnId); bool removePhantom(String columnId); diff --git a/frontend/rust-lib/flowy-folder/tests/workspace/script.rs b/frontend/rust-lib/flowy-folder/tests/workspace/script.rs index 5fb0be874c..528ffbcafb 100644 --- a/frontend/rust-lib/flowy-folder/tests/workspace/script.rs +++ b/frontend/rust-lib/flowy-folder/tests/workspace/script.rs @@ -150,7 +150,7 @@ impl FolderTest { // assert_eq!(json, expected_json); // } FolderScript::AssertWorkspace(workspace) => { - assert_eq!(self.workspace, workspace); + assert_eq!(self.workspace, workspace, "Workspace not equal"); } FolderScript::ReadWorkspace(workspace_id) => { let workspace = read_workspace(sdk, workspace_id).await.pop().unwrap(); @@ -166,7 +166,7 @@ impl FolderTest { // assert_eq!(json, expected_json); // } FolderScript::AssertApp(app) => { - assert_eq!(self.app, app); + assert_eq!(self.app, app, "App not equal"); } FolderScript::ReadApp(app_id) => { let app = read_app(sdk, &app_id).await; @@ -184,7 +184,7 @@ impl FolderTest { self.view = view; } FolderScript::AssertView(view) => { - assert_eq!(self.view, view); + assert_eq!(self.view, view, "View not equal"); } FolderScript::ReadView(view_id) => { let view = read_view(sdk, &view_id).await; @@ -215,7 +215,7 @@ impl FolderTest { } FolderScript::AssertRevisionState { rev_id, state } => { let record = cache.get(rev_id).await.unwrap(); - assert_eq!(record.state, state); + assert_eq!(record.state, state, "Revision state is not match"); if let RevisionState::Ack = state { // There is a defer action that writes the revisions to disk, so we wait here. // Make sure everything is written. @@ -235,7 +235,7 @@ impl FolderTest { .unwrap_or_else(|| panic!("Expected Next revision is {}, but receive None", rev_id.unwrap())); let mut notify = rev_manager.ack_notify(); let _ = notify.recv().await; - assert_eq!(next_revision.rev_id, rev_id.unwrap()); + assert_eq!(next_revision.rev_id, rev_id.unwrap(), "Revision id not match"); } } } diff --git a/frontend/rust-lib/flowy-grid/src/services/field/type_options/selection_type_option/multi_select_type_option.rs b/frontend/rust-lib/flowy-grid/src/services/field/type_options/selection_type_option/multi_select_type_option.rs index 0bffef4c52..be83975c91 100644 --- a/frontend/rust-lib/flowy-grid/src/services/field/type_options/selection_type_option/multi_select_type_option.rs +++ b/frontend/rust-lib/flowy-grid/src/services/field/type_options/selection_type_option/multi_select_type_option.rs @@ -97,7 +97,7 @@ pub struct MultiSelectTypeOptionBuilder(MultiSelectTypeOptionPB); impl_into_box_type_option_builder!(MultiSelectTypeOptionBuilder); impl_builder_from_json_str_and_from_bytes!(MultiSelectTypeOptionBuilder, MultiSelectTypeOptionPB); impl MultiSelectTypeOptionBuilder { - pub fn option(mut self, opt: SelectOptionPB) -> Self { + pub fn add_option(mut self, opt: SelectOptionPB) -> Self { self.0.options.push(opt); self } @@ -127,9 +127,9 @@ mod tests { let facebook_option = SelectOptionPB::new("Facebook"); let twitter_option = SelectOptionPB::new("Twitter"); let multi_select = MultiSelectTypeOptionBuilder::default() - .option(google_option.clone()) - .option(facebook_option.clone()) - .option(twitter_option); + .add_option(google_option.clone()) + .add_option(facebook_option.clone()) + .add_option(twitter_option); let field_rev = FieldBuilder::new(multi_select) .name("Platform") diff --git a/frontend/rust-lib/flowy-grid/src/services/field/type_options/text_type_option/text_type_option.rs b/frontend/rust-lib/flowy-grid/src/services/field/type_options/text_type_option/text_type_option.rs index f21ca91f8c..6c50ce8da3 100644 --- a/frontend/rust-lib/flowy-grid/src/services/field/type_options/text_type_option/text_type_option.rs +++ b/frontend/rust-lib/flowy-grid/src/services/field/type_options/text_type_option/text_type_option.rs @@ -156,8 +156,8 @@ mod tests { let ids = vec![google_option.id.clone(), facebook_option.id.clone()].join(SELECTION_IDS_SEPARATOR); let cell_data_changeset = SelectOptionCellChangeset::from_insert(&ids).to_str(); let multi_select = MultiSelectTypeOptionBuilder::default() - .option(google_option.clone()) - .option(facebook_option.clone()); + .add_option(google_option.clone()) + .add_option(facebook_option.clone()); let multi_select_field_rev = FieldBuilder::new(multi_select).build(); let multi_type_option = MultiSelectTypeOptionPB::from(&multi_select_field_rev); let cell_data = multi_type_option diff --git a/frontend/rust-lib/flowy-grid/src/services/row/row_builder.rs b/frontend/rust-lib/flowy-grid/src/services/row/row_builder.rs index ad294e74dc..3db4c0b550 100644 --- a/frontend/rust-lib/flowy-grid/src/services/row/row_builder.rs +++ b/frontend/rust-lib/flowy-grid/src/services/row/row_builder.rs @@ -1,6 +1,5 @@ use crate::services::cell::apply_cell_data_changeset; -use crate::services::field::SelectOptionCellChangeset; -use flowy_error::{FlowyError, FlowyResult}; +use crate::services::field::{DateCellChangesetPB, SelectOptionCellChangeset}; use flowy_grid_data_model::revision::{gen_row_id, CellRevision, FieldRevision, RowRevision, DEFAULT_ROW_HEIGHT}; use indexmap::IndexMap; use std::collections::HashMap; @@ -35,17 +34,33 @@ impl<'a> RowRevisionBuilder<'a> { } } - pub fn insert_cell(&mut self, field_id: &str, data: String) -> FlowyResult<()> { + pub fn insert_cell(&mut self, field_id: &str, data: String) { match self.field_rev_map.get(&field_id.to_owned()) { None => { - let msg = format!("Can't find the field with id: {}", field_id); - Err(FlowyError::internal().context(msg)) + tracing::warn!("Can't find the field with id: {}", field_id); } Some(field_rev) => { - let data = apply_cell_data_changeset(data, None, field_rev)?; + let data = apply_cell_data_changeset(data, None, field_rev).unwrap(); + let cell = CellRevision::new(data); + self.payload.cell_by_field_id.insert(field_id.to_owned(), cell); + } + } + } + + pub fn insert_date_cell(&mut self, field_id: &str, timestamp: i64) { + match self.field_rev_map.get(&field_id.to_owned()) { + None => { + tracing::warn!("Invalid field_id: {}", field_id); + } + Some(field_rev) => { + let cell_data = serde_json::to_string(&DateCellChangesetPB { + date: Some(timestamp.to_string()), + time: None, + }) + .unwrap(); + let data = apply_cell_data_changeset(cell_data, None, field_rev).unwrap(); let cell = CellRevision::new(data); self.payload.cell_by_field_id.insert(field_id.to_owned(), cell); - Ok(()) } } } diff --git a/frontend/rust-lib/flowy-grid/src/util.rs b/frontend/rust-lib/flowy-grid/src/util.rs index 7897dbfdaa..ca1d92a54c 100644 --- a/frontend/rust-lib/flowy-grid/src/util.rs +++ b/frontend/rust-lib/flowy-grid/src/util.rs @@ -3,6 +3,7 @@ use crate::services::field::*; use crate::services::row::RowRevisionBuilder; use flowy_grid_data_model::revision::BuildGridContext; use flowy_sync::client_grid::GridBuilder; +use lib_infra::util::timestamp; pub fn make_default_grid() -> BuildGridContext { let mut grid_builder = GridBuilder::new(); @@ -40,24 +41,94 @@ pub fn make_default_board() -> BuildGridContext { .visibility(true) .primary(true) .build(); + let text_field_id = text_field.id.clone(); grid_builder.add_field(text_field); + // date + let date_type_option = DateTypeOptionBuilder::default(); + let date_field = FieldBuilder::new(date_type_option) + .name("Date") + .visibility(true) + .build(); + let date_field_id = date_field.id.clone(); + let timestamp = timestamp(); + grid_builder.add_field(date_field); + // single select let in_progress_option = SelectOptionPB::new("In progress"); let not_started_option = SelectOptionPB::new("Not started"); let done_option = SelectOptionPB::new("Done"); - let single_select = SingleSelectTypeOptionBuilder::default() + let single_select_type_option = SingleSelectTypeOptionBuilder::default() .add_option(not_started_option.clone()) .add_option(in_progress_option) .add_option(done_option); - let single_select_field = FieldBuilder::new(single_select).name("Status").visibility(true).build(); + let single_select_field = FieldBuilder::new(single_select_type_option) + .name("Status") + .visibility(true) + .build(); let single_select_field_id = single_select_field.id.clone(); grid_builder.add_field(single_select_field); + // MultiSelect + let apple_option = SelectOptionPB::new("Apple"); + let banana_option = SelectOptionPB::new("Banana"); + let pear_option = SelectOptionPB::new("Pear"); + let multi_select_type_option = MultiSelectTypeOptionBuilder::default() + .add_option(banana_option.clone()) + .add_option(apple_option.clone()) + .add_option(pear_option.clone()); + let multi_select_field = FieldBuilder::new(multi_select_type_option) + .name("Fruit") + .visibility(true) + .build(); + let multi_select_field_id = multi_select_field.id.clone(); + grid_builder.add_field(multi_select_field); + + // Number + let number_type_option = NumberTypeOptionBuilder::default().set_format(NumberFormat::USD); + let number_field = FieldBuilder::new(number_type_option) + .name("Price") + .visibility(true) + .build(); + let number_field_id = number_field.id.clone(); + grid_builder.add_field(number_field); + + // Checkbox + let checkbox_type_option = CheckboxTypeOptionBuilder::default(); + let checkbox_field = FieldBuilder::new(checkbox_type_option).name("Reimbursement").build(); + let checkbox_field_id = checkbox_field.id.clone(); + grid_builder.add_field(checkbox_field); + + // Url + let url_type_option = URLTypeOptionBuilder::default(); + let url_field = FieldBuilder::new(url_type_option).name("Shop Link").build(); + let url_field_id = url_field.id.clone(); + grid_builder.add_field(url_field); + // Insert rows - for _ in 0..3 { + for i in 0..10 { + // insert single select let mut row_builder = RowRevisionBuilder::new(grid_builder.block_id(), grid_builder.field_revs()); row_builder.insert_select_option_cell(&single_select_field_id, not_started_option.id.clone()); + // insert multi select + row_builder.insert_select_option_cell(&multi_select_field_id, apple_option.id.clone()); + row_builder.insert_select_option_cell(&multi_select_field_id, banana_option.id.clone()); + // insert text + row_builder.insert_cell(&text_field_id, format!("Card {}", i)); + // insert date + row_builder.insert_date_cell(&date_field_id, timestamp); + // number + row_builder.insert_cell(&number_field_id, format!("{}", i)); + // checkbox + let is_check = if i % 2 == 0 { + CHECK.to_string() + } else { + UNCHECK.to_string() + }; + row_builder.insert_cell(&checkbox_field_id, is_check); + // url + row_builder.insert_cell(&url_field_id, "https://appflowy.io".to_string()); + let row = row_builder.build(); grid_builder.add_row(row); } diff --git a/frontend/rust-lib/flowy-grid/tests/grid/block_test/util.rs b/frontend/rust-lib/flowy-grid/tests/grid/block_test/util.rs index 9b97afbf56..a733926228 100644 --- a/frontend/rust-lib/flowy-grid/tests/grid/block_test/util.rs +++ b/frontend/rust-lib/flowy-grid/tests/grid/block_test/util.rs @@ -26,18 +26,14 @@ impl<'a> GridRowTestBuilder<'a> { pub fn insert_text_cell(&mut self, data: &str) -> String { let text_field = self.field_rev_with_type(&FieldType::RichText); - self.inner_builder - .insert_cell(&text_field.id, data.to_string()) - .unwrap(); + self.inner_builder.insert_cell(&text_field.id, data.to_string()); text_field.id.clone() } pub fn insert_number_cell(&mut self, data: &str) -> String { let number_field = self.field_rev_with_type(&FieldType::Number); - self.inner_builder - .insert_cell(&number_field.id, data.to_string()) - .unwrap(); + self.inner_builder.insert_cell(&number_field.id, data.to_string()); number_field.id.clone() } @@ -48,22 +44,20 @@ impl<'a> GridRowTestBuilder<'a> { }) .unwrap(); let date_field = self.field_rev_with_type(&FieldType::DateTime); - self.inner_builder.insert_cell(&date_field.id, value).unwrap(); + self.inner_builder.insert_cell(&date_field.id, value); date_field.id.clone() } pub fn insert_checkbox_cell(&mut self, data: &str) -> String { let checkbox_field = self.field_rev_with_type(&FieldType::Checkbox); - self.inner_builder - .insert_cell(&checkbox_field.id, data.to_string()) - .unwrap(); + self.inner_builder.insert_cell(&checkbox_field.id, data.to_string()); checkbox_field.id.clone() } pub fn insert_url_cell(&mut self, data: &str) -> String { let url_field = self.field_rev_with_type(&FieldType::URL); - self.inner_builder.insert_cell(&url_field.id, data.to_string()).unwrap(); + self.inner_builder.insert_cell(&url_field.id, data.to_string()); url_field.id.clone() } diff --git a/frontend/rust-lib/flowy-grid/tests/grid/grid_editor.rs b/frontend/rust-lib/flowy-grid/tests/grid/grid_editor.rs index a0f1b6d9cf..2528ce2b32 100644 --- a/frontend/rust-lib/flowy-grid/tests/grid/grid_editor.rs +++ b/frontend/rust-lib/flowy-grid/tests/grid/grid_editor.rs @@ -147,9 +147,9 @@ fn make_test_grid() -> BuildGridContext { FieldType::MultiSelect => { // MultiSelect let multi_select = MultiSelectTypeOptionBuilder::default() - .option(SelectOptionPB::new(GOOGLE)) - .option(SelectOptionPB::new(FACEBOOK)) - .option(SelectOptionPB::new(TWITTER)); + .add_option(SelectOptionPB::new(GOOGLE)) + .add_option(SelectOptionPB::new(FACEBOOK)) + .add_option(SelectOptionPB::new(TWITTER)); let multi_select_field = FieldBuilder::new(multi_select) .name("Platform") .visibility(true)