Merge pull request #843 from AppFlowy-IO/feat/board_card

Board: Support display text/number/url/single-select/muti-select/date card style
This commit is contained in:
Nathan.fooo 2022-08-13 14:57:49 +08:00 committed by GitHub
commit 6b0becd9ca
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
47 changed files with 1356 additions and 189 deletions

View File

@ -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<BoardEvent, BoardState> {
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<BoardEvent, BoardState> {
didReceiveGroups: (List<GroupPB> groups) {
emit(state.copyWith(groups: groups));
},
didReceiveRows: (List<RowInfo> rowInfos) {
emit(state.copyWith(rowInfos: rowInfos));
},
);
},
);
@ -68,7 +75,7 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
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<BoardEvent, BoardState> {
boardDataController.addColumns(columns);
},
onRowsChanged: (List<RowInfo> rowInfos, RowChangeReason reason) {
add(BoardEvent.didReceiveRows(rowInfos));
},
onError: (err) {
Log.error(err);
},
);
}
List<BoardColumnItem> _buildRows(List<RowPB> 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<AFColumnItem> _buildRows(List<RowPB> 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 <AFColumnItem>[...items];
}
Future<void> _loadGrid(Emitter<BoardState> emit) async {
@ -131,6 +143,8 @@ class BoardEvent with _$BoardEvent {
const factory BoardEvent.createRow() = _CreateRow;
const factory BoardEvent.didReceiveGroups(List<GroupPB> groups) =
_DidReceiveGroup;
const factory BoardEvent.didReceiveRows(List<RowInfo> 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';
}

View File

@ -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<FieldPB>);
typedef OnGridChanged = void Function(GridPB);
typedef OnGroupChanged = void Function(List<GroupPB>);
typedef OnRowsChanged = void Function(
List<RowInfo> rowInfos,
RowChangeReason,
);
typedef OnError = void Function(FlowyError);
class BoardDataController {
@ -21,17 +27,25 @@ class BoardDataController {
// key: the block id
final LinkedHashMap<String, GridBlockCache> _blocks;
UnmodifiableMapView<String, GridBlockCache> get blocks =>
UnmodifiableMapView(_blocks);
LinkedHashMap<String, GridBlockCache> get blocks => _blocks;
OnFieldsChanged? _onFieldsChanged;
OnGridChanged? _onGridChanged;
OnGroupChanged? _onGroupChanged;
OnRowsChanged? _onRowsChanged;
OnError? _onError;
List<RowInfo> get rowInfos {
final List<RowInfo> 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<Either<RowPB, FlowyError>> createRow() {
return _gridFFIService.createRow();
}
Future<void> dispose() async {
@ -85,6 +102,29 @@ class BoardDataController {
}
}
void _initialBlocks(List<BlockPB> 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<Either<Unit, FlowyError>> _loadFields(GridPB grid) async {
final result = await _gridFFIService.getFields(fieldIds: grid.fields);
return Future(

View File

@ -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<BoardCheckboxCellEvent, BoardCheckboxCellState> {
final GridCheckboxCellController cellController;
void Function()? _onCellChangedFn;
BoardCheckboxCellBloc({
required this.cellController,
}) : super(BoardCheckboxCellState.initial(cellController)) {
on<BoardCheckboxCellEvent>(
(event, emit) async {
await event.when(
initial: () async {
_startListening();
},
didReceiveCellUpdate: (cellData) {
emit(state.copyWith(isSelected: _isSelected(cellData)));
},
);
},
);
}
@override
Future<void> 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";
}

View File

@ -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<BoardDateCellEvent, BoardDateCellState> {
final GridDateCellController cellController;
void Function()? _onCellChangedFn;
BoardDateCellBloc({required this.cellController})
: super(BoardDateCellState.initial(cellController)) {
on<BoardDateCellEvent>(
(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<void> 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;
}

View File

@ -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<BoardNumberCellEvent, BoardNumberCellState> {
final GridNumberCellController cellController;
void Function()? _onCellChangedFn;
BoardNumberCellBloc({
required this.cellController,
}) : super(BoardNumberCellState.initial(cellController)) {
on<BoardNumberCellEvent>(
(event, emit) async {
await event.when(
initial: () async {
_startListening();
},
didReceiveCellUpdate: (content) {
emit(state.copyWith(content: content));
},
);
},
);
}
@override
Future<void> 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() ?? "",
);
}

View File

@ -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<BoardURLCellEvent, BoardURLCellState> {
final GridURLCellController cellController;
void Function()? _onCellChangedFn;
BoardURLCellBloc({
required this.cellController,
}) : super(BoardURLCellState.initial(cellController)) {
on<BoardURLCellEvent>(
(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<void> 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 ?? "",
);
}
}

View File

@ -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<BoardCardEvent, BoardCardState> {
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<BoardCardEvent>(
(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<void> close() async {
_dataController.dispose();
return super.close();
}
Future<void> _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<GridCellEquatable> 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<Object?> get props => [
_field.id,
_field.fieldType,
_field.visibility,
_field.width,
];
}

View File

@ -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<VoidCallback> _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;
}

View File

@ -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<BoardBloc>().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<BoardBloc>().getRowCache(rowPB.blockId);
/// Return placeholder widget if the rowCache is null.
if (rowCache == null) return SizedBox(key: ObjectKey(item));
final fieldCache = context.read<BoardBloc>().fieldCache;
final gridId = context.read<BoardBloc>().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,
),
);
}
}

View File

@ -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<BoardCheckboxCell> createState() => _BoardCheckboxCellState();
}
class _BoardCheckboxCellState extends State<BoardCheckboxCell> {
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<BoardCheckboxCellBloc, BoardCheckboxCellState>(
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<void> dispose() async {
_cellBloc.close();
super.dispose();
}
}

View File

@ -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<BoardDateCell> createState() => _BoardDateCellState();
}
class _BoardDateCellState extends State<BoardDateCell> {
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<BoardDateCellBloc, BoardDateCellState>(
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<void> dispose() async {
_cellBloc.close();
super.dispose();
}
}

View File

@ -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<BoardNumberCell> createState() => _BoardNumberCellState();
}
class _BoardNumberCellState extends State<BoardNumberCell> {
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<BoardNumberCellBloc, BoardNumberCellState>(
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<void> dispose() async {
_cellBloc.close();
super.dispose();
}
}

View File

@ -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<BoardSelectOptionCell> {
value: _cellBloc,
child: BlocBuilder<BoardSelectOptionCellBloc, BoardSelectOptionCellState>(
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),
);
},
),

View File

@ -32,10 +32,17 @@ class _BoardTextCellState extends State<BoardTextCell> {
value: _cellBloc,
child: BlocBuilder<BoardTextCellBloc, BoardTextCellState>(
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,
),
);
}
},
),
);

View File

@ -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<BoardUrlCell> createState() => _BoardUrlCellState();
}
class _BoardUrlCellState extends State<BoardUrlCell> {
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<AppTheme>();
return BlocProvider.value(
value: _cellBloc,
child: BlocBuilder<BoardURLCellBloc, BoardURLCellState>(
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<void> dispose() async {
_cellBloc.close();
super.dispose();
}
}

View File

@ -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<BoardCard> createState() => _BoardCardState();
}
class _BoardCardState extends State<BoardCard> {
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<BoardCardBloc, BoardCardState>(
builder: (context, state) {
return BoardCardContainer(
accessoryBuilder: (context) {
return [const _CardMoreOption()];
},
child: Column(
children: _makeCells(context, state.gridCellMap),
),
);
},
),
);
}
List<Widget> _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<AppTheme>().iconColor);
}
@override
void onTap(BuildContext context) {
Log.debug('show options');
}
}

View File

@ -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;
}
}

View File

@ -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<CardAccessory> Function(
BuildContext buildContext,
);
class CardAccessoryContainer extends StatelessWidget {
final List<CardAccessory> accessories;
const CardAccessoryContainer({required this.accessories, Key? key})
: super(key: key);
@override
Widget build(BuildContext context) {
final theme = context.read<AppTheme>();
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<CardAccessory> 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<Widget> 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;
}

View File

@ -1,6 +1,8 @@
part of 'cell_service.dart';
typedef GridCellController = IGridCellController<String, String>;
typedef GridCheckboxCellController = IGridCellController<String, String>;
typedef GridNumberCellController = IGridCellController<String, String>;
typedef GridSelectOptionCellController
= IGridCellController<SelectOptionCellDataPB, String>;
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<T, D> extends Equatable {
final GridCellDataLoader<T> _cellDataLoader;
final IGridCellDataPersistence<D> _cellDataPersistence;
late final CellListener _cellListener;
CellListener? _cellListener;
ValueNotifier<T?>? _cellDataNotifier;
bool isListening = false;
@ -186,7 +188,7 @@ class IGridCellController<T, D> 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<T, D> extends Equatable {
return;
}
_isDispose = true;
_cellListener.stop();
_cellListener?.stop();
_loadDataOperation?.cancel();
_saveDataOperation?.cancel();
_cellDataNotifier = null;

View File

@ -6,7 +6,7 @@ import 'cell_service/cell_service.dart';
part 'checkbox_cell_bloc.freezed.dart';
class CheckboxCellBloc extends Bloc<CheckboxCellEvent, CheckboxCellState> {
final GridCellController cellController;
final GridCheckboxCellController cellController;
void Function()? _onCellChangedFn;
CheckboxCellBloc({

View File

@ -8,7 +8,7 @@ import 'cell_service/cell_service.dart';
part 'number_cell_bloc.freezed.dart';
class NumberCellBloc extends Bloc<NumberCellEvent, NumberCellState> {
final GridCellController cellController;
final GridNumberCellController cellController;
void Function()? _onCellChangedFn;
NumberCellBloc({

View File

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

View File

@ -12,10 +12,10 @@ part 'row_action_sheet_bloc.freezed.dart';
class RowActionSheetBloc
extends Bloc<RowActionSheetEvent, RowActionSheetState> {
final RowService _rowService;
final RowFFIService _rowService;
RowActionSheetBloc({required RowInfo rowData})
: _rowService = RowService(
: _rowService = RowFFIService(
gridId: rowData.gridId,
blockId: rowData.blockId,
rowId: rowData.id,

View File

@ -12,13 +12,13 @@ import 'row_service.dart';
part 'row_bloc.freezed.dart';
class RowBloc extends Bloc<RowEvent, RowState> {
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<RowEvent, RowState> {
_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<GridCellEquatable> snapshots,
required UnmodifiableListView<GridCellEquatable> 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(),
),
);
}

View File

@ -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;
}

View File

@ -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<Either<RowPB, FlowyError>> createRow() {

View File

@ -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<GridCellAccessory> 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<bool>? get onAccessoryHover;
AccessoryBuilder? get accessoryBuilder;
}
class AccessoryHover extends StatefulWidget {
final CellAccessory child;
final EdgeInsets contentPadding;

View File

@ -94,6 +94,18 @@ abstract class CellEditable {
ValueNotifier<bool> get onCellEditing;
}
typedef AccessoryBuilder = List<GridCellAccessory> 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<bool>? get onAccessoryHover;
AccessoryBuilder? get accessoryBuilder;
}
abstract class GridCellWidget extends StatefulWidget
implements CellAccessory, CellEditable, CellShortcuts {
GridCellWidget({Key? key}) : super(key: key) {

View File

@ -25,24 +25,28 @@ class CellContainer extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ChangeNotifierProxyProvider<RegionStateNotifier,
CellContainerNotifier>(
create: (_) => CellContainerNotifier(child),
_CellContainerNotifier>(
create: (_) => _CellContainerNotifier(child),
update: (_, rowStateNotifier, cellStateNotifier) =>
cellStateNotifier!..onEnter = rowStateNotifier.onEnter,
child: Selector<CellContainerNotifier, bool>(
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<GridCellAccessory> accessories;
const CellEnterRegion(
const _GridCellEnterRegion(
{required this.child, required this.accessories, Key? key})
: super(key: key);
@override
Widget build(BuildContext context) {
return Selector<CellContainerNotifier, bool>(
return Selector<_CellContainerNotifier, bool>(
selector: (context, notifier) => notifier.onEnter,
builder: (context, onEnter, _) {
List<Widget> children = [child];
@ -95,10 +99,10 @@ class CellEnterRegion extends StatelessWidget {
return MouseRegion(
cursor: SystemMouseCursors.click,
onEnter: (p) =>
Provider.of<CellContainerNotifier>(context, listen: false)
Provider.of<_CellContainerNotifier>(context, listen: false)
.onEnter = true,
onExit: (p) =>
Provider.of<CellContainerNotifier>(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!);
}

View File

@ -22,7 +22,8 @@ class _CheckboxCellState extends GridCellState<GridCheckboxCell> {
@override
void initState() {
final cellController = widget.cellControllerBuilder.build();
final cellController =
widget.cellControllerBuilder.build() as GridCheckboxCellController;
_cellBloc = getIt<CheckboxCellBloc>(param1: cellController)
..add(const CheckboxCellEvent.initial());
super.initState();

View File

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

View File

@ -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),
);
}),
],
);
}

View File

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

View File

@ -90,9 +90,9 @@ class _GridURLCellState extends GridCellState<GridURLCell> {
@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();
}

View File

@ -164,7 +164,7 @@ class RowContent extends StatelessWidget {
Widget build(BuildContext context) {
return BlocBuilder<RowBloc, RowState>(
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<GridCellAccessory> 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<RegionStateNotifier>(context, listen: false),
accessoryBuilder: accessoryBuilder,
accessoryBuilder: (buildContext) {
final builder = child.accessoryBuilder;
List<GridCellAccessory> accessories = [];
if (cellId.field.isPrimary) {
accessories.add(PrimaryCellAccessory(
onTapCallback: onExpand,
isCellEditing: buildContext.isCellEditing,
));
}
if (builder != null) {
accessories.addAll(builder(buildContext));
}
return accessories;
},
);
},
).toList();

View File

@ -23,18 +23,19 @@ class _MultiBoardListExampleState extends State<MultiBoardListExample> {
@override
void initState() {
final column1 = AFBoardColumnData(id: "To Do", items: [
List<AFColumnItem> 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: <AFColumnItem>[
// 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: <AFColumnItem>[]);
boardDataController.addColumn(column1);
boardDataController.addColumn(column2);

View File

@ -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<BoardContent> {
return ChangeNotifierProvider.value(
key: ValueKey(columnData.id),
value: widget.dataController.columnController(columnData.id),
child: Consumer<BoardColumnDataController>(
child: Consumer<AFBoardColumnDataController>(
builder: (context, value, child) {
return ConstrainedBox(
constraints: widget.columnConstraints,

View File

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

View File

@ -35,7 +35,7 @@ class AFBoardDataController extends ChangeNotifier
List<String> get columnIds =>
_columnDatas.map((columnData) => columnData.id).toList();
final LinkedHashMap<String, BoardColumnDataController> _columnControllers =
final LinkedHashMap<String, AFBoardColumnDataController> _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];
}

View File

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

View File

@ -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");
}
}
}

View File

@ -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")

View File

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

View File

@ -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(())
}
}
}

View File

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

View File

@ -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()
}

View File

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