mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
Merge pull request #920 from AppFlowy-IO/feat/edit_create_card
Feat/edit create card
This commit is contained in:
commit
fcb144e514
@ -72,28 +72,33 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
|
||||
createRow: (groupId) async {
|
||||
final result = await _gridDataController.createBoardCard(groupId);
|
||||
result.fold(
|
||||
(rowPB) {
|
||||
emit(state.copyWith(editingRow: some(rowPB)));
|
||||
},
|
||||
(_) {},
|
||||
(err) => Log.error(err),
|
||||
);
|
||||
},
|
||||
didCreateRow: (String groupId, RowPB row) {
|
||||
emit(state.copyWith(
|
||||
editingRow: Some(BoardEditingRow(columnId: groupId, row: row)),
|
||||
));
|
||||
},
|
||||
endEditRow: (rowId) {
|
||||
assert(state.editingRow.isSome());
|
||||
state.editingRow.fold(() => null, (row) {
|
||||
assert(row.id == rowId);
|
||||
state.editingRow.fold(() => null, (editingRow) {
|
||||
assert(editingRow.row.id == rowId);
|
||||
emit(state.copyWith(editingRow: none()));
|
||||
});
|
||||
},
|
||||
didReceiveGridUpdate: (GridPB grid) {
|
||||
emit(state.copyWith(grid: Some(grid)));
|
||||
},
|
||||
didReceiveRows: (List<RowInfo> rowInfos) {
|
||||
emit(state.copyWith(rowInfos: rowInfos));
|
||||
},
|
||||
didReceiveError: (FlowyError error) {
|
||||
emit(state.copyWith(noneOrError: some(error)));
|
||||
},
|
||||
didReceiveGroups: (List<GroupPB> groups) {
|
||||
emit(state.copyWith(
|
||||
groupIds: groups.map((group) => group.groupId).toList(),
|
||||
));
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
@ -135,7 +140,12 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
|
||||
|
||||
void initializeGroups(List<GroupPB> groups) {
|
||||
for (final group in groups) {
|
||||
final delegate = GroupControllerDelegateImpl(boardController);
|
||||
final delegate = GroupControllerDelegateImpl(
|
||||
controller: boardController,
|
||||
didAddColumnItem: (groupId, row) {
|
||||
add(BoardEvent.didCreateRow(groupId, row));
|
||||
},
|
||||
);
|
||||
final controller = GroupController(
|
||||
gridId: state.gridId,
|
||||
group: group,
|
||||
@ -163,16 +173,14 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
|
||||
return AFBoardColumnData(
|
||||
id: group.groupId,
|
||||
name: group.desc,
|
||||
items: _buildRows(group.rows),
|
||||
items: _buildRows(group),
|
||||
customData: group,
|
||||
);
|
||||
}).toList();
|
||||
|
||||
boardController.addColumns(columns);
|
||||
initializeGroups(groups);
|
||||
},
|
||||
onRowsChanged: (List<RowInfo> rowInfos, RowsChangedReason reason) {
|
||||
add(BoardEvent.didReceiveRows(rowInfos));
|
||||
add(BoardEvent.didReceiveGroups(groups));
|
||||
},
|
||||
onDeletedGroup: (groupIds) {
|
||||
//
|
||||
@ -196,9 +204,12 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
|
||||
);
|
||||
}
|
||||
|
||||
List<AFColumnItem> _buildRows(List<RowPB> rows) {
|
||||
final items = rows.map((row) {
|
||||
return BoardColumnItem(row: row);
|
||||
List<AFColumnItem> _buildRows(GroupPB group) {
|
||||
final items = group.rows.map((row) {
|
||||
return BoardColumnItem(
|
||||
row: row,
|
||||
fieldId: group.fieldId,
|
||||
);
|
||||
}).toList();
|
||||
|
||||
return <AFColumnItem>[...items];
|
||||
@ -219,15 +230,17 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
|
||||
|
||||
@freezed
|
||||
class BoardEvent with _$BoardEvent {
|
||||
const factory BoardEvent.initial() = InitialGrid;
|
||||
const factory BoardEvent.initial() = _InitialBoard;
|
||||
const factory BoardEvent.createRow(String groupId) = _CreateRow;
|
||||
const factory BoardEvent.didCreateRow(String groupId, RowPB row) =
|
||||
_DidCreateRow;
|
||||
const factory BoardEvent.endEditRow(String rowId) = _EndEditRow;
|
||||
const factory BoardEvent.didReceiveError(FlowyError error) = _DidReceiveError;
|
||||
const factory BoardEvent.didReceiveRows(List<RowInfo> rowInfos) =
|
||||
_DidReceiveRows;
|
||||
const factory BoardEvent.didReceiveGridUpdate(
|
||||
GridPB grid,
|
||||
) = _DidReceiveGridUpdate;
|
||||
const factory BoardEvent.didReceiveGroups(List<GroupPB> groups) =
|
||||
_DidReceiveGroups;
|
||||
}
|
||||
|
||||
@freezed
|
||||
@ -235,16 +248,16 @@ class BoardState with _$BoardState {
|
||||
const factory BoardState({
|
||||
required String gridId,
|
||||
required Option<GridPB> grid,
|
||||
required Option<RowPB> editingRow,
|
||||
required List<RowInfo> rowInfos,
|
||||
required List<String> groupIds,
|
||||
required Option<BoardEditingRow> editingRow,
|
||||
required GridLoadingState loadingState,
|
||||
required Option<FlowyError> noneOrError,
|
||||
}) = _BoardState;
|
||||
|
||||
factory BoardState.initial(String gridId) => BoardState(
|
||||
rowInfos: [],
|
||||
grid: none(),
|
||||
gridId: gridId,
|
||||
groupIds: [],
|
||||
editingRow: none(),
|
||||
noneOrError: none(),
|
||||
loadingState: const _Loading(),
|
||||
@ -284,39 +297,68 @@ class GridFieldEquatable extends Equatable {
|
||||
class BoardColumnItem extends AFColumnItem {
|
||||
final RowPB row;
|
||||
|
||||
BoardColumnItem({required this.row});
|
||||
final String fieldId;
|
||||
|
||||
final bool requestFocus;
|
||||
|
||||
BoardColumnItem({
|
||||
required this.row,
|
||||
required this.fieldId,
|
||||
this.requestFocus = false,
|
||||
});
|
||||
|
||||
@override
|
||||
String get id => row.id;
|
||||
}
|
||||
|
||||
class CreateCardItem extends AFColumnItem {
|
||||
@override
|
||||
String get id => '$CreateCardItem';
|
||||
}
|
||||
|
||||
class GroupControllerDelegateImpl extends GroupControllerDelegate {
|
||||
final AFBoardDataController controller;
|
||||
final void Function(String, RowPB) didAddColumnItem;
|
||||
|
||||
GroupControllerDelegateImpl(this.controller);
|
||||
GroupControllerDelegateImpl({
|
||||
required this.controller,
|
||||
required this.didAddColumnItem,
|
||||
});
|
||||
|
||||
@override
|
||||
void insertRow(String groupId, RowPB row, int? index) {
|
||||
final item = BoardColumnItem(row: row);
|
||||
void insertRow(GroupPB group, RowPB row, int? index) {
|
||||
if (index != null) {
|
||||
controller.insertColumnItem(groupId, index, item);
|
||||
final item = BoardColumnItem(row: row, fieldId: group.fieldId);
|
||||
controller.insertColumnItem(group.groupId, index, item);
|
||||
} else {
|
||||
controller.addColumnItem(groupId, item);
|
||||
final item = BoardColumnItem(
|
||||
row: row,
|
||||
fieldId: group.fieldId,
|
||||
requestFocus: true,
|
||||
);
|
||||
controller.addColumnItem(group.groupId, item);
|
||||
didAddColumnItem(group.groupId, row);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void removeRow(String groupId, String rowId) {
|
||||
controller.removeColumnItem(groupId, rowId);
|
||||
void removeRow(GroupPB group, String rowId) {
|
||||
controller.removeColumnItem(group.groupId, rowId);
|
||||
}
|
||||
|
||||
@override
|
||||
void updateRow(String groupId, RowPB row) {
|
||||
controller.updateColumnItem(groupId, BoardColumnItem(row: row));
|
||||
void updateRow(GroupPB group, RowPB row) {
|
||||
controller.updateColumnItem(
|
||||
group.groupId,
|
||||
BoardColumnItem(
|
||||
row: row,
|
||||
fieldId: group.fieldId,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class BoardEditingRow {
|
||||
String columnId;
|
||||
RowPB row;
|
||||
|
||||
BoardEditingRow({
|
||||
required this.columnId,
|
||||
required this.row,
|
||||
});
|
||||
}
|
||||
|
@ -60,7 +60,7 @@ class BoardDataController {
|
||||
required OnGridChanged onGridChanged,
|
||||
OnFieldsChanged? onFieldsChanged,
|
||||
required DidLoadGroups didLoadGroups,
|
||||
required OnRowsChanged onRowsChanged,
|
||||
OnRowsChanged? onRowsChanged,
|
||||
required OnUpdatedGroup onUpdatedGroup,
|
||||
required OnDeletedGroup onDeletedGroup,
|
||||
required OnInsertedGroup onInsertedGroup,
|
||||
|
@ -4,7 +4,6 @@ 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';
|
||||
@ -14,10 +13,12 @@ import 'card_data_controller.dart';
|
||||
part 'card_bloc.freezed.dart';
|
||||
|
||||
class BoardCardBloc extends Bloc<BoardCardEvent, BoardCardState> {
|
||||
final String fieldId;
|
||||
final RowFFIService _rowService;
|
||||
final CardDataController _dataController;
|
||||
|
||||
BoardCardBloc({
|
||||
required this.fieldId,
|
||||
required String gridId,
|
||||
required CardDataController dataController,
|
||||
}) : _rowService = RowFFIService(
|
||||
@ -25,22 +26,22 @@ class BoardCardBloc extends Bloc<BoardCardEvent, BoardCardState> {
|
||||
blockId: dataController.rowPB.blockId,
|
||||
),
|
||||
_dataController = dataController,
|
||||
super(BoardCardState.initial(
|
||||
dataController.rowPB, dataController.loadData())) {
|
||||
super(
|
||||
BoardCardState.initial(
|
||||
dataController.rowPB,
|
||||
_makeCells(fieldId, dataController.loadData()),
|
||||
),
|
||||
) {
|
||||
on<BoardCardEvent>(
|
||||
(event, emit) async {
|
||||
await event.map(
|
||||
initial: (_InitialRow value) async {
|
||||
await event.when(
|
||||
initial: () async {
|
||||
await _startListening();
|
||||
},
|
||||
didReceiveCells: (_DidReceiveCells value) async {
|
||||
final cells = value.gridCellMap.values
|
||||
.map((e) => GridCellEquatable(e.field))
|
||||
.toList();
|
||||
didReceiveCells: (cells, reason) async {
|
||||
emit(state.copyWith(
|
||||
gridCellMap: value.gridCellMap,
|
||||
cells: UnmodifiableListView(cells),
|
||||
changeReason: value.reason,
|
||||
cells: cells,
|
||||
changeReason: reason,
|
||||
));
|
||||
},
|
||||
);
|
||||
@ -58,7 +59,7 @@ class BoardCardBloc extends Bloc<BoardCardEvent, BoardCardState> {
|
||||
return RowInfo(
|
||||
gridId: _rowService.gridId,
|
||||
fields: UnmodifiableListView(
|
||||
state.cells.map((cell) => cell._field).toList(),
|
||||
state.cells.map((cell) => cell.identifier.field).toList(),
|
||||
),
|
||||
rowPB: state.rowPB,
|
||||
);
|
||||
@ -66,8 +67,9 @@ class BoardCardBloc extends Bloc<BoardCardEvent, BoardCardState> {
|
||||
|
||||
Future<void> _startListening() async {
|
||||
_dataController.addListener(
|
||||
onRowChanged: (cells, reason) {
|
||||
onRowChanged: (cellMap, reason) {
|
||||
if (!isClosed) {
|
||||
final cells = _makeCells(fieldId, cellMap);
|
||||
add(BoardCardEvent.didReceiveCells(cells, reason));
|
||||
}
|
||||
},
|
||||
@ -75,42 +77,49 @@ class BoardCardBloc extends Bloc<BoardCardEvent, BoardCardState> {
|
||||
}
|
||||
}
|
||||
|
||||
UnmodifiableListView<BoardCellEquatable> _makeCells(
|
||||
String fieldId, GridCellMap originalCellMap) {
|
||||
List<BoardCellEquatable> cells = [];
|
||||
for (final entry in originalCellMap.entries) {
|
||||
if (entry.value.fieldId != fieldId) {
|
||||
cells.add(BoardCellEquatable(entry.value));
|
||||
}
|
||||
}
|
||||
return UnmodifiableListView(cells);
|
||||
}
|
||||
|
||||
@freezed
|
||||
class BoardCardEvent with _$BoardCardEvent {
|
||||
const factory BoardCardEvent.initial() = _InitialRow;
|
||||
const factory BoardCardEvent.didReceiveCells(
|
||||
GridCellMap gridCellMap, RowsChangedReason reason) = _DidReceiveCells;
|
||||
UnmodifiableListView<BoardCellEquatable> cells,
|
||||
RowsChangedReason reason,
|
||||
) = _DidReceiveCells;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class BoardCardState with _$BoardCardState {
|
||||
const factory BoardCardState({
|
||||
required RowPB rowPB,
|
||||
required GridCellMap gridCellMap,
|
||||
required UnmodifiableListView<GridCellEquatable> cells,
|
||||
required UnmodifiableListView<BoardCellEquatable> cells,
|
||||
RowsChangedReason? changeReason,
|
||||
}) = _BoardCardState;
|
||||
|
||||
factory BoardCardState.initial(RowPB rowPB, GridCellMap cellDataMap) =>
|
||||
BoardCardState(
|
||||
rowPB: rowPB,
|
||||
gridCellMap: cellDataMap,
|
||||
cells: UnmodifiableListView(
|
||||
cellDataMap.values.map((e) => GridCellEquatable(e.field)).toList(),
|
||||
),
|
||||
);
|
||||
factory BoardCardState.initial(
|
||||
RowPB rowPB, UnmodifiableListView<BoardCellEquatable> cells) =>
|
||||
BoardCardState(rowPB: rowPB, cells: cells);
|
||||
}
|
||||
|
||||
class GridCellEquatable extends Equatable {
|
||||
final FieldPB _field;
|
||||
class BoardCellEquatable extends Equatable {
|
||||
final GridCellIdentifier identifier;
|
||||
|
||||
const GridCellEquatable(FieldPB field) : _field = field;
|
||||
const BoardCellEquatable(this.identifier);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
_field.id,
|
||||
_field.fieldType,
|
||||
_field.visibility,
|
||||
_field.width,
|
||||
identifier.field.id,
|
||||
identifier.field.fieldType,
|
||||
identifier.field.visibility,
|
||||
identifier.field.width,
|
||||
];
|
||||
}
|
||||
|
@ -1,15 +1,14 @@
|
||||
import 'package:flowy_sdk/log.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/protobuf.dart';
|
||||
|
||||
import 'group_listener.dart';
|
||||
|
||||
typedef OnGroupError = void Function(FlowyError);
|
||||
|
||||
abstract class GroupControllerDelegate {
|
||||
void removeRow(String groupId, String rowId);
|
||||
void insertRow(String groupId, RowPB row, int? index);
|
||||
void updateRow(String groupId, RowPB row);
|
||||
void removeRow(GroupPB group, String rowId);
|
||||
void insertRow(GroupPB group, RowPB row, int? index);
|
||||
void updateRow(GroupPB group, RowPB row);
|
||||
}
|
||||
|
||||
class GroupController {
|
||||
@ -37,12 +36,11 @@ class GroupController {
|
||||
(GroupChangesetPB changeset) {
|
||||
for (final deletedRow in changeset.deletedRows) {
|
||||
group.rows.removeWhere((rowPB) => rowPB.id == deletedRow);
|
||||
delegate.removeRow(group.groupId, deletedRow);
|
||||
delegate.removeRow(group, deletedRow);
|
||||
}
|
||||
|
||||
for (final insertedRow in changeset.insertedRows) {
|
||||
final index = insertedRow.hasIndex() ? insertedRow.index : null;
|
||||
|
||||
if (insertedRow.hasIndex() &&
|
||||
group.rows.length > insertedRow.index) {
|
||||
group.rows.insert(insertedRow.index, insertedRow.row);
|
||||
@ -50,11 +48,7 @@ class GroupController {
|
||||
group.rows.add(insertedRow.row);
|
||||
}
|
||||
|
||||
delegate.insertRow(
|
||||
group.groupId,
|
||||
insertedRow.row,
|
||||
index,
|
||||
);
|
||||
delegate.insertRow(group, insertedRow.row, index);
|
||||
}
|
||||
|
||||
for (final updatedRow in changeset.updatedRows) {
|
||||
@ -66,7 +60,7 @@ class GroupController {
|
||||
group.rows[index] = updatedRow;
|
||||
}
|
||||
|
||||
delegate.updateRow(group.groupId, updatedRow);
|
||||
delegate.updateRow(group, updatedRow);
|
||||
}
|
||||
},
|
||||
(err) => Log.error(err),
|
||||
@ -74,6 +68,29 @@ class GroupController {
|
||||
});
|
||||
}
|
||||
|
||||
// GroupChangesetPB _transformChangeset(GroupChangesetPB changeset) {
|
||||
// final insertedRows = changeset.insertedRows
|
||||
// .where(
|
||||
// (delete) => !changeset.deletedRows.contains(delete.row.id),
|
||||
// )
|
||||
// .toList();
|
||||
|
||||
// final deletedRows = changeset.deletedRows
|
||||
// .where((deletedRowId) =>
|
||||
// changeset.insertedRows
|
||||
// .indexWhere((insert) => insert.row.id == deletedRowId) ==
|
||||
// -1)
|
||||
// .toList();
|
||||
|
||||
// return changeset.rebuild((rebuildChangeset) {
|
||||
// rebuildChangeset.insertedRows.clear();
|
||||
// rebuildChangeset.insertedRows.addAll(insertedRows);
|
||||
|
||||
// rebuildChangeset.deletedRows.clear();
|
||||
// rebuildChangeset.deletedRows.addAll(deletedRows);
|
||||
// });
|
||||
// }
|
||||
|
||||
Future<void> dispose() async {
|
||||
_listener.stop();
|
||||
}
|
||||
|
@ -32,13 +32,15 @@ class BoardPage extends StatelessWidget {
|
||||
create: (context) =>
|
||||
BoardBloc(view: view)..add(const BoardEvent.initial()),
|
||||
child: BlocBuilder<BoardBloc, BoardState>(
|
||||
buildWhen: (previous, current) =>
|
||||
previous.loadingState != current.loadingState,
|
||||
builder: (context, state) {
|
||||
return state.loadingState.map(
|
||||
loading: (_) =>
|
||||
const Center(child: CircularProgressIndicator.adaptive()),
|
||||
finish: (result) {
|
||||
return result.successOrFail.fold(
|
||||
(_) => BoardContent(),
|
||||
(_) => const BoardContent(),
|
||||
(err) => FlowyErrorPage(err.toString()),
|
||||
);
|
||||
},
|
||||
@ -49,42 +51,82 @@ class BoardPage extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class BoardContent extends StatelessWidget {
|
||||
class BoardContent extends StatefulWidget {
|
||||
const BoardContent({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<BoardContent> createState() => _BoardContentState();
|
||||
}
|
||||
|
||||
class _BoardContentState extends State<BoardContent> {
|
||||
late ScrollController scrollController;
|
||||
late AFBoardScrollManager scrollManager;
|
||||
|
||||
final config = AFBoardConfig(
|
||||
columnBackgroundColor: HexColor.fromHex('#F7F8FC'),
|
||||
);
|
||||
|
||||
BoardContent({Key? key}) : super(key: key);
|
||||
@override
|
||||
void initState() {
|
||||
scrollController = ScrollController();
|
||||
scrollManager = AFBoardScrollManager();
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<BoardBloc, BoardState>(
|
||||
builder: (context, state) {
|
||||
return Container(
|
||||
color: Colors.white,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 20),
|
||||
child: AFBoard(
|
||||
scrollController: ScrollController(),
|
||||
dataController: context.read<BoardBloc>().boardController,
|
||||
headerBuilder: _buildHeader,
|
||||
footBuilder: _buildFooter,
|
||||
cardBuilder: (_, column, columnItem) => _buildCard(
|
||||
context,
|
||||
column,
|
||||
columnItem,
|
||||
),
|
||||
columnConstraints: const BoxConstraints.tightFor(width: 240),
|
||||
config: AFBoardConfig(
|
||||
columnBackgroundColor: HexColor.fromHex('#F7F8FC'),
|
||||
),
|
||||
),
|
||||
),
|
||||
return BlocListener<BoardBloc, BoardState>(
|
||||
listener: (context, state) {
|
||||
state.editingRow.fold(
|
||||
() => null,
|
||||
(editingRow) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
scrollManager.scrollToBottom(editingRow.columnId, () {
|
||||
context
|
||||
.read<BoardBloc>()
|
||||
.add(BoardEvent.endEditRow(editingRow.row.id));
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
||||
},
|
||||
child: BlocBuilder<BoardBloc, BoardState>(
|
||||
buildWhen: (previous, current) =>
|
||||
previous.groupIds.length != current.groupIds.length,
|
||||
builder: (context, state) {
|
||||
return Container(
|
||||
color: Colors.white,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 20),
|
||||
child: AFBoard(
|
||||
scrollManager: scrollManager,
|
||||
scrollController: scrollController,
|
||||
dataController: context.read<BoardBloc>().boardController,
|
||||
headerBuilder: _buildHeader,
|
||||
footBuilder: _buildFooter,
|
||||
cardBuilder: (_, column, columnItem) => _buildCard(
|
||||
context,
|
||||
column,
|
||||
columnItem,
|
||||
),
|
||||
columnConstraints: const BoxConstraints.tightFor(width: 240),
|
||||
config: AFBoardConfig(
|
||||
columnBackgroundColor: HexColor.fromHex('#F7F8FC'),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
scrollController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Widget _buildHeader(
|
||||
BuildContext context, AFBoardColumnHeaderData headerData) {
|
||||
return AppFlowyColumnHeader(
|
||||
@ -138,7 +180,8 @@ class BoardContent extends StatelessWidget {
|
||||
AFBoardColumnData column,
|
||||
AFColumnItem columnItem,
|
||||
) {
|
||||
final rowPB = (columnItem as BoardColumnItem).row;
|
||||
final boardColumnItem = columnItem as BoardColumnItem;
|
||||
final rowPB = boardColumnItem.row;
|
||||
final rowCache = context.read<BoardBloc>().getRowCache(rowPB.blockId);
|
||||
|
||||
/// Return placeholder widget if the rowCache is null.
|
||||
@ -155,16 +198,17 @@ class BoardContent extends StatelessWidget {
|
||||
final cellBuilder = BoardCellBuilder(cardController);
|
||||
final isEditing = context.read<BoardBloc>().state.editingRow.fold(
|
||||
() => false,
|
||||
(editingRow) => editingRow.id == rowPB.id,
|
||||
(editingRow) => editingRow.row.id == rowPB.id,
|
||||
);
|
||||
|
||||
return AppFlowyColumnItemCard(
|
||||
key: ObjectKey(columnItem),
|
||||
key: ValueKey(columnItem.id),
|
||||
margin: config.cardPadding,
|
||||
decoration: _makeBoxDecoration(context),
|
||||
child: BoardCard(
|
||||
gridId: gridId,
|
||||
groupId: column.id,
|
||||
fieldId: boardColumnItem.fieldId,
|
||||
isEditing: isEditing,
|
||||
cellBuilder: cellBuilder,
|
||||
dataController: cardController,
|
||||
|
@ -35,8 +35,9 @@ class _BoardSelectOptionCellState extends State<BoardSelectOptionCell> {
|
||||
return BlocProvider.value(
|
||||
value: _cellBloc,
|
||||
child: BlocBuilder<BoardSelectOptionCellBloc, BoardSelectOptionCellState>(
|
||||
buildWhen: (previous, current) =>
|
||||
previous.selectedOptions != current.selectedOptions,
|
||||
buildWhen: (previous, current) {
|
||||
return previous.selectedOptions != current.selectedOptions;
|
||||
},
|
||||
builder: (context, state) {
|
||||
if (state.selectedOptions
|
||||
.where((element) => element.id == widget.groupId)
|
||||
|
@ -15,6 +15,7 @@ typedef OnEndEditing = void Function(String rowId);
|
||||
class BoardCard extends StatefulWidget {
|
||||
final String gridId;
|
||||
final String groupId;
|
||||
final String fieldId;
|
||||
final bool isEditing;
|
||||
final CardDataController dataController;
|
||||
final BoardCellBuilder cellBuilder;
|
||||
@ -24,6 +25,7 @@ class BoardCard extends StatefulWidget {
|
||||
const BoardCard({
|
||||
required this.gridId,
|
||||
required this.groupId,
|
||||
required this.fieldId,
|
||||
required this.isEditing,
|
||||
required this.dataController,
|
||||
required this.cellBuilder,
|
||||
@ -43,6 +45,7 @@ class _BoardCardState extends State<BoardCard> {
|
||||
void initState() {
|
||||
_cardBloc = BoardCardBloc(
|
||||
gridId: widget.gridId,
|
||||
fieldId: widget.fieldId,
|
||||
dataController: widget.dataController,
|
||||
)..add(const BoardCardEvent.initial());
|
||||
super.initState();
|
||||
@ -53,6 +56,9 @@ class _BoardCardState extends State<BoardCard> {
|
||||
return BlocProvider.value(
|
||||
value: _cardBloc,
|
||||
child: BlocBuilder<BoardCardBloc, BoardCardState>(
|
||||
buildWhen: (previous, current) {
|
||||
return previous.cells.length != current.cells.length;
|
||||
},
|
||||
builder: (context, state) {
|
||||
return BoardCardContainer(
|
||||
accessoryBuilder: (context) {
|
||||
@ -62,7 +68,10 @@ class _BoardCardState extends State<BoardCard> {
|
||||
widget.openCard(context);
|
||||
},
|
||||
child: Column(
|
||||
children: _makeCells(context, state.gridCellMap),
|
||||
children: _makeCells(
|
||||
context,
|
||||
state.cells.map((cell) => cell.identifier).toList(),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
@ -70,9 +79,12 @@ class _BoardCardState extends State<BoardCard> {
|
||||
);
|
||||
}
|
||||
|
||||
List<Widget> _makeCells(BuildContext context, GridCellMap cellMap) {
|
||||
return cellMap.values.map(
|
||||
(cellId) {
|
||||
List<Widget> _makeCells(
|
||||
BuildContext context,
|
||||
List<GridCellIdentifier> cells,
|
||||
) {
|
||||
return cells.map(
|
||||
(GridCellIdentifier cellId) {
|
||||
final child = widget.cellBuilder.buildCell(widget.groupId, cellId);
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(left: 4, right: 4, top: 6),
|
||||
|
@ -35,7 +35,7 @@ class RowDetailPage extends StatefulWidget with FlowyOverlayDelegate {
|
||||
|
||||
void show(BuildContext context) async {
|
||||
final windowSize = MediaQuery.of(context).size;
|
||||
final size = windowSize * 0.7;
|
||||
final size = windowSize * 0.5;
|
||||
FlowyOverlay.of(context).insertWithRect(
|
||||
widget: OverlayContainer(
|
||||
child: this,
|
||||
|
@ -114,7 +114,7 @@ class _MultiBoardListExampleState extends State<MultiBoardListExample> {
|
||||
return Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 60),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 30),
|
||||
child: Text(item.s),
|
||||
),
|
||||
);
|
||||
@ -124,7 +124,7 @@ class _MultiBoardListExampleState extends State<MultiBoardListExample> {
|
||||
return Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 60),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 20),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
|
@ -1,13 +1,27 @@
|
||||
import 'package:appflowy_board/src/utils/log.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'board_column/board_column.dart';
|
||||
import 'board_column/board_column_data.dart';
|
||||
import 'board_data.dart';
|
||||
import 'reorder_flex/drag_state.dart';
|
||||
import 'reorder_flex/drag_target_interceptor.dart';
|
||||
import 'reorder_flex/reorder_flex.dart';
|
||||
import 'reorder_phantom/phantom_controller.dart';
|
||||
import '../rendering/board_overlay.dart';
|
||||
|
||||
class AFBoardScrollManager {
|
||||
BoardColumnsState? _columnState;
|
||||
|
||||
// AFBoardScrollManager();
|
||||
|
||||
void scrollToBottom(String columnId, VoidCallback? completed) {
|
||||
_columnState
|
||||
?.getReorderFlexState(columnId: columnId)
|
||||
?.scrollToBottom(completed);
|
||||
}
|
||||
}
|
||||
|
||||
class AFBoardConfig {
|
||||
final double cornerRadius;
|
||||
final EdgeInsets columnPadding;
|
||||
@ -56,6 +70,10 @@ class AFBoard extends StatelessWidget {
|
||||
|
||||
final AFBoardConfig config;
|
||||
|
||||
final AFBoardScrollManager? scrollManager;
|
||||
|
||||
final BoardColumnsState _columnState = BoardColumnsState();
|
||||
|
||||
AFBoard({
|
||||
required this.dataController,
|
||||
required this.cardBuilder,
|
||||
@ -63,6 +81,7 @@ class AFBoard extends StatelessWidget {
|
||||
this.footBuilder,
|
||||
this.headerBuilder,
|
||||
this.scrollController,
|
||||
this.scrollManager,
|
||||
this.columnConstraints = const BoxConstraints(maxWidth: 200),
|
||||
this.config = const AFBoardConfig(),
|
||||
Key? key,
|
||||
@ -75,10 +94,16 @@ class AFBoard extends StatelessWidget {
|
||||
value: dataController,
|
||||
child: Consumer<AFBoardDataController>(
|
||||
builder: (context, notifier, child) {
|
||||
return BoardContent(
|
||||
if (scrollManager != null) {
|
||||
scrollManager!._columnState = _columnState;
|
||||
}
|
||||
|
||||
return AFBoardContent(
|
||||
config: config,
|
||||
dataController: dataController,
|
||||
scrollController: scrollController,
|
||||
scrollManager: scrollManager,
|
||||
columnsState: _columnState,
|
||||
background: background,
|
||||
delegate: phantomController,
|
||||
columnConstraints: columnConstraints,
|
||||
@ -94,7 +119,7 @@ class AFBoard extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class BoardContent extends StatefulWidget {
|
||||
class AFBoardContent extends StatefulWidget {
|
||||
final ScrollController? scrollController;
|
||||
final OnDragStarted? onDragStarted;
|
||||
final OnReorder onReorder;
|
||||
@ -104,6 +129,8 @@ class BoardContent extends StatefulWidget {
|
||||
final AFBoardConfig config;
|
||||
final ReorderFlexConfig reorderFlexConfig;
|
||||
final BoxConstraints columnConstraints;
|
||||
final AFBoardScrollManager? scrollManager;
|
||||
final BoardColumnsState columnsState;
|
||||
|
||||
///
|
||||
final AFBoardColumnCardBuilder cardBuilder;
|
||||
@ -118,11 +145,13 @@ class BoardContent extends StatefulWidget {
|
||||
|
||||
final BoardPhantomController phantomController;
|
||||
|
||||
const BoardContent({
|
||||
const AFBoardContent({
|
||||
required this.config,
|
||||
required this.onReorder,
|
||||
required this.delegate,
|
||||
required this.dataController,
|
||||
required this.scrollManager,
|
||||
required this.columnsState,
|
||||
this.onDragStarted,
|
||||
this.onDragEnded,
|
||||
this.scrollController,
|
||||
@ -137,12 +166,12 @@ class BoardContent extends StatefulWidget {
|
||||
super(key: key);
|
||||
|
||||
@override
|
||||
State<BoardContent> createState() => _BoardContentState();
|
||||
State<AFBoardContent> createState() => _AFBoardContentState();
|
||||
}
|
||||
|
||||
class _BoardContentState extends State<BoardContent> {
|
||||
final GlobalKey _columnContainerOverlayKey =
|
||||
GlobalKey(debugLabel: '$BoardContent overlay key');
|
||||
class _AFBoardContentState extends State<AFBoardContent> {
|
||||
final GlobalKey _boardContentKey =
|
||||
GlobalKey(debugLabel: '$AFBoardContent overlay key');
|
||||
late BoardOverlayEntry _overlayEntry;
|
||||
|
||||
@override
|
||||
@ -153,10 +182,10 @@ class _BoardContentState extends State<BoardContent> {
|
||||
reorderFlexId: widget.dataController.identifier,
|
||||
acceptedReorderFlexId: widget.dataController.columnIds,
|
||||
delegate: widget.delegate,
|
||||
columnsState: widget.columnsState,
|
||||
);
|
||||
|
||||
final reorderFlex = ReorderFlex(
|
||||
key: widget.key,
|
||||
config: widget.reorderFlexConfig,
|
||||
scrollController: widget.scrollController,
|
||||
onDragStarted: widget.onDragStarted,
|
||||
@ -165,7 +194,7 @@ class _BoardContentState extends State<BoardContent> {
|
||||
dataSource: widget.dataController,
|
||||
direction: Axis.horizontal,
|
||||
interceptor: interceptor,
|
||||
children: _buildColumns(interceptor.columnKeys),
|
||||
children: _buildColumns(),
|
||||
);
|
||||
|
||||
return Stack(
|
||||
@ -192,12 +221,12 @@ class _BoardContentState extends State<BoardContent> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BoardOverlay(
|
||||
key: _columnContainerOverlayKey,
|
||||
key: _boardContentKey,
|
||||
initialEntries: [_overlayEntry],
|
||||
);
|
||||
}
|
||||
|
||||
List<Widget> _buildColumns(List<ColumnKey> columnKeys) {
|
||||
List<Widget> _buildColumns() {
|
||||
final List<Widget> children =
|
||||
widget.dataController.columnDatas.asMap().entries.map(
|
||||
(item) {
|
||||
@ -215,6 +244,8 @@ class _BoardContentState extends State<BoardContent> {
|
||||
child: Consumer<AFBoardColumnDataController>(
|
||||
builder: (context, value, child) {
|
||||
final boardColumn = AFBoardColumnWidget(
|
||||
key: PageStorageKey<String>(columnData.id),
|
||||
// key: GlobalObjectKey(columnData.id),
|
||||
margin: _marginFromIndex(columnIndex),
|
||||
itemMargin: widget.config.columnItemPadding,
|
||||
headerBuilder: _buildHeader,
|
||||
@ -226,16 +257,11 @@ class _BoardContentState extends State<BoardContent> {
|
||||
onReorder: widget.dataController.moveColumnItem,
|
||||
cornerRadius: widget.config.cornerRadius,
|
||||
backgroundColor: widget.config.columnBackgroundColor,
|
||||
dragStateStorage: widget.columnsState,
|
||||
dragTargetIndexKeyStorage: widget.columnsState,
|
||||
);
|
||||
|
||||
// columnKeys
|
||||
// .removeWhere((element) => element.columnId == columnData.id);
|
||||
// columnKeys.add(
|
||||
// ColumnKey(
|
||||
// columnId: columnData.id,
|
||||
// key: boardColumn.columnGlobalKey,
|
||||
// ),
|
||||
// );
|
||||
widget.columnsState.addColumn(columnData.id, boardColumn);
|
||||
|
||||
return ConstrainedBox(
|
||||
constraints: widget.columnConstraints,
|
||||
@ -296,3 +322,77 @@ class _BoardColumnDataSourceImpl extends AFBoardColumnDataDataSource {
|
||||
@override
|
||||
List<String> get acceptedColumnIds => dataController.columnIds;
|
||||
}
|
||||
|
||||
class BoardColumnContext {
|
||||
GlobalKey? columnKey;
|
||||
DraggingState? draggingState;
|
||||
}
|
||||
|
||||
class BoardColumnsState extends DraggingStateStorage
|
||||
with ReorderDragTargetIndexKeyStorage {
|
||||
/// Quick access to the [AFBoardColumnWidget]
|
||||
final Map<String, GlobalKey> columnKeys = {};
|
||||
final Map<String, DraggingState> columnDragStates = {};
|
||||
final Map<String, Map<String, GlobalObjectKey>> columnDragDragTargets = {};
|
||||
|
||||
void addColumn(String columnId, AFBoardColumnWidget columnWidget) {
|
||||
columnKeys[columnId] = columnWidget.globalKey;
|
||||
}
|
||||
|
||||
ReorderFlexState? getReorderFlexState({required String columnId}) {
|
||||
final flexGlobalKey = columnKeys[columnId];
|
||||
if (flexGlobalKey == null) return null;
|
||||
if (flexGlobalKey.currentState is! ReorderFlexState) return null;
|
||||
final state = flexGlobalKey.currentState as ReorderFlexState;
|
||||
return state;
|
||||
}
|
||||
|
||||
ReorderFlex? getReorderFlex({required String columnId}) {
|
||||
final flexGlobalKey = columnKeys[columnId];
|
||||
if (flexGlobalKey == null) return null;
|
||||
if (flexGlobalKey.currentWidget is! ReorderFlex) return null;
|
||||
final widget = flexGlobalKey.currentWidget as ReorderFlex;
|
||||
return widget;
|
||||
}
|
||||
|
||||
@override
|
||||
DraggingState? read(String reorderFlexId) {
|
||||
return columnDragStates[reorderFlexId];
|
||||
}
|
||||
|
||||
@override
|
||||
void write(String reorderFlexId, DraggingState state) {
|
||||
Log.trace('$reorderFlexId Write dragging state: $state');
|
||||
columnDragStates[reorderFlexId] = state;
|
||||
}
|
||||
|
||||
@override
|
||||
void remove(String reorderFlexId) {
|
||||
columnDragStates.remove(reorderFlexId);
|
||||
}
|
||||
|
||||
@override
|
||||
void addKey(
|
||||
String reorderFlexId,
|
||||
String key,
|
||||
GlobalObjectKey<State<StatefulWidget>> value,
|
||||
) {
|
||||
Map<String, GlobalObjectKey>? column = columnDragDragTargets[reorderFlexId];
|
||||
if (column == null) {
|
||||
column = {};
|
||||
columnDragDragTargets[reorderFlexId] = column;
|
||||
}
|
||||
column[key] = value;
|
||||
}
|
||||
|
||||
@override
|
||||
GlobalObjectKey<State<StatefulWidget>>? readKey(
|
||||
String reorderFlexId, String key) {
|
||||
Map<String, GlobalObjectKey>? column = columnDragDragTargets[reorderFlexId];
|
||||
if (column != null) {
|
||||
return column[key];
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
import 'dart:collection';
|
||||
|
||||
import 'package:appflowy_board/src/widgets/reorder_flex/drag_state.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../rendering/board_overlay.dart';
|
||||
import '../../utils/log.dart';
|
||||
@ -65,7 +66,6 @@ class AFBoardColumnWidget extends StatefulWidget {
|
||||
final AFBoardColumnDataDataSource dataSource;
|
||||
final ScrollController? scrollController;
|
||||
final ReorderFlexConfig config;
|
||||
|
||||
final OnColumnDragStarted? onDragStarted;
|
||||
final OnColumnReorder onReorder;
|
||||
final OnColumnDragEnded? onDragEnded;
|
||||
@ -88,7 +88,11 @@ class AFBoardColumnWidget extends StatefulWidget {
|
||||
|
||||
final Color backgroundColor;
|
||||
|
||||
final GlobalKey columnGlobalKey = GlobalKey();
|
||||
final DraggingStateStorage? dragStateStorage;
|
||||
|
||||
final ReorderDragTargetIndexKeyStorage? dragTargetIndexKeyStorage;
|
||||
|
||||
final GlobalKey globalKey;
|
||||
|
||||
AFBoardColumnWidget({
|
||||
Key? key,
|
||||
@ -98,14 +102,17 @@ class AFBoardColumnWidget extends StatefulWidget {
|
||||
required this.onReorder,
|
||||
required this.dataSource,
|
||||
required this.phantomController,
|
||||
this.onDragStarted,
|
||||
this.dragStateStorage,
|
||||
this.dragTargetIndexKeyStorage,
|
||||
this.scrollController,
|
||||
this.onDragStarted,
|
||||
this.onDragEnded,
|
||||
this.margin = EdgeInsets.zero,
|
||||
this.itemMargin = EdgeInsets.zero,
|
||||
this.cornerRadius = 0.0,
|
||||
this.backgroundColor = Colors.transparent,
|
||||
}) : config = const ReorderFlexConfig(),
|
||||
}) : globalKey = GlobalKey(),
|
||||
config = const ReorderFlexConfig(),
|
||||
super(key: key);
|
||||
|
||||
@override
|
||||
@ -115,7 +122,6 @@ class AFBoardColumnWidget extends StatefulWidget {
|
||||
class _AFBoardColumnWidgetState extends State<AFBoardColumnWidget> {
|
||||
final GlobalKey _columnOverlayKey =
|
||||
GlobalKey(debugLabel: '$AFBoardColumnWidget overlay key');
|
||||
|
||||
late BoardOverlayEntry _overlayEntry;
|
||||
|
||||
@override
|
||||
@ -140,7 +146,9 @@ class _AFBoardColumnWidgetState extends State<AFBoardColumnWidget> {
|
||||
);
|
||||
|
||||
Widget reorderFlex = ReorderFlex(
|
||||
key: widget.columnGlobalKey,
|
||||
key: widget.globalKey,
|
||||
dragStateStorage: widget.dragStateStorage,
|
||||
dragTargetIndexKeyStorage: widget.dragTargetIndexKeyStorage,
|
||||
scrollController: widget.scrollController,
|
||||
config: widget.config,
|
||||
onDragStarted: (index) {
|
||||
@ -163,9 +171,6 @@ class _AFBoardColumnWidgetState extends State<AFBoardColumnWidget> {
|
||||
children: children,
|
||||
);
|
||||
|
||||
// reorderFlex =
|
||||
// KeyedSubtree(key: widget.columnGlobalKey, child: reorderFlex);
|
||||
|
||||
return Container(
|
||||
margin: widget.margin,
|
||||
clipBehavior: Clip.hardEdge,
|
||||
@ -177,10 +182,7 @@ class _AFBoardColumnWidgetState extends State<AFBoardColumnWidget> {
|
||||
children: [
|
||||
if (header != null) header,
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: widget.itemMargin,
|
||||
child: reorderFlex,
|
||||
),
|
||||
child: Padding(padding: widget.itemMargin, child: reorderFlex),
|
||||
),
|
||||
if (footer != null) footer,
|
||||
],
|
||||
|
@ -196,7 +196,6 @@ class AFBoardDataController extends ChangeNotifier
|
||||
final index =
|
||||
columnDataController.items.indexWhere((item) => item.isPhantom);
|
||||
|
||||
assert(index != -1);
|
||||
if (index != -1) {
|
||||
if (index != newIndex) {
|
||||
Log.trace(
|
||||
|
@ -24,6 +24,10 @@ class FlexDragTargetData extends DragTargetData {
|
||||
|
||||
final String dragTargetId;
|
||||
|
||||
Offset dragTargetOffset = Offset.zero;
|
||||
|
||||
final GlobalObjectKey dragTargetIndexKey;
|
||||
|
||||
final String reorderFlexId;
|
||||
|
||||
final ReoderFlexItem reorderFlexItem;
|
||||
@ -33,6 +37,7 @@ class FlexDragTargetData extends DragTargetData {
|
||||
required this.draggingIndex,
|
||||
required this.reorderFlexId,
|
||||
required this.reorderFlexItem,
|
||||
required this.dragTargetIndexKey,
|
||||
required DraggingState state,
|
||||
}) : _state = state;
|
||||
|
||||
@ -40,6 +45,50 @@ class FlexDragTargetData extends DragTargetData {
|
||||
String toString() {
|
||||
return 'ReorderFlexId: $reorderFlexId, dragTargetId: $dragTargetId';
|
||||
}
|
||||
|
||||
bool isOverlapWithWidgets(List<GlobalObjectKey> widgetKeys) {
|
||||
final renderBox = dragTargetIndexKey.currentContext?.findRenderObject();
|
||||
|
||||
if (renderBox == null) return false;
|
||||
if (renderBox is! RenderBox) return false;
|
||||
final size = feedbackSize ?? Size.zero;
|
||||
final Rect rect = dragTargetOffset & size;
|
||||
|
||||
for (final widgetKey in widgetKeys) {
|
||||
final renderObject = widgetKey.currentContext?.findRenderObject();
|
||||
if (renderObject != null && renderObject is RenderBox) {
|
||||
Rect widgetRect =
|
||||
renderObject.localToGlobal(Offset.zero) & renderObject.size;
|
||||
// return rect.overlaps(widgetRect);
|
||||
if (rect.right <= widgetRect.left || widgetRect.right <= rect.left) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (rect.bottom <= widgetRect.top || widgetRect.bottom <= rect.top) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// final HitTestResult result = HitTestResult();
|
||||
// WidgetsBinding.instance.hitTest(result, position);
|
||||
// for (final HitTestEntry entry in result.path) {
|
||||
// final HitTestTarget target = entry.target;
|
||||
// if (target is RenderMetaData) {
|
||||
// print(target.metaData);
|
||||
// }
|
||||
// print(target);
|
||||
// }
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
abstract class DraggingStateStorage {
|
||||
void write(String reorderFlexId, DraggingState state);
|
||||
void remove(String reorderFlexId);
|
||||
DraggingState? read(String reorderFlexId);
|
||||
}
|
||||
|
||||
class DraggingState {
|
||||
@ -128,6 +177,7 @@ class DraggingState {
|
||||
|
||||
/// Set the currentIndex to nextIndex
|
||||
void moveDragTargetToNext() {
|
||||
Log.debug('$reorderFlexId updateCurrentIndex: $nextIndex');
|
||||
currentIndex = nextIndex;
|
||||
}
|
||||
|
||||
@ -136,6 +186,14 @@ class DraggingState {
|
||||
nextIndex = index;
|
||||
}
|
||||
|
||||
void setStartDraggingIndex(int index) {
|
||||
Log.debug('$reorderFlexId setDragIndex: $index');
|
||||
dragStartIndex = index;
|
||||
phantomIndex = index;
|
||||
currentIndex = index;
|
||||
nextIndex = index;
|
||||
}
|
||||
|
||||
bool isNotDragging() {
|
||||
return dragStartIndex == -1;
|
||||
}
|
||||
|
@ -26,6 +26,11 @@ typedef DragTargetWillAccepted<T extends DragTargetData> = bool Function(
|
||||
///
|
||||
typedef DragTargetOnStarted = void Function(Widget, int, Size?);
|
||||
|
||||
typedef DragTargetOnMove<T extends DragTargetData> = void Function(
|
||||
T dragTargetData,
|
||||
Offset offset,
|
||||
);
|
||||
|
||||
///
|
||||
typedef DragTargetOnEnded<T extends DragTargetData> = void Function(
|
||||
T dragTargetData);
|
||||
@ -39,13 +44,15 @@ class ReorderDragTarget<T extends DragTargetData> extends StatefulWidget {
|
||||
final Widget child;
|
||||
final T dragTargetData;
|
||||
|
||||
final GlobalObjectKey _indexGlobalKey;
|
||||
final GlobalObjectKey indexGlobalKey;
|
||||
|
||||
/// Called when dragTarget is being dragging.
|
||||
final DragTargetOnStarted onDragStarted;
|
||||
|
||||
final DragTargetOnEnded<T> onDragEnded;
|
||||
|
||||
final DragTargetOnMove<T> onDragMoved;
|
||||
|
||||
/// Called to determine whether this widget is interested in receiving a given
|
||||
/// piece of data being dragged over this drag target.
|
||||
///
|
||||
@ -69,11 +76,13 @@ class ReorderDragTarget<T extends DragTargetData> extends StatefulWidget {
|
||||
|
||||
final bool useMoveAnimation;
|
||||
|
||||
ReorderDragTarget({
|
||||
const ReorderDragTarget({
|
||||
Key? key,
|
||||
required this.child,
|
||||
required this.indexGlobalKey,
|
||||
required this.dragTargetData,
|
||||
required this.onDragStarted,
|
||||
required this.onDragMoved,
|
||||
required this.onDragEnded,
|
||||
required this.onWillAccept,
|
||||
required this.insertAnimationController,
|
||||
@ -82,8 +91,7 @@ class ReorderDragTarget<T extends DragTargetData> extends StatefulWidget {
|
||||
this.onAccept,
|
||||
this.onLeave,
|
||||
this.draggableTargetBuilder,
|
||||
}) : _indexGlobalKey = GlobalObjectKey(child.key!),
|
||||
super(key: key);
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<ReorderDragTarget<T>> createState() => _ReorderDragTargetState<T>();
|
||||
@ -104,6 +112,9 @@ class _ReorderDragTargetState<T extends DragTargetData>
|
||||
return widget.onWillAccept(dragTargetData);
|
||||
},
|
||||
onAccept: widget.onAccept,
|
||||
onMove: (detail) {
|
||||
widget.onDragMoved(detail.data, detail.offset);
|
||||
},
|
||||
onLeave: (dragTargetData) {
|
||||
assert(dragTargetData != null);
|
||||
if (dragTargetData != null) {
|
||||
@ -112,7 +123,7 @@ class _ReorderDragTargetState<T extends DragTargetData>
|
||||
},
|
||||
);
|
||||
|
||||
dragTarget = KeyedSubtree(key: widget._indexGlobalKey, child: dragTarget);
|
||||
dragTarget = KeyedSubtree(key: widget.indexGlobalKey, child: dragTarget);
|
||||
return dragTarget;
|
||||
}
|
||||
|
||||
@ -150,7 +161,7 @@ class _ReorderDragTargetState<T extends DragTargetData>
|
||||
child: widget.child,
|
||||
),
|
||||
onDragStarted: () {
|
||||
_draggingFeedbackSize = widget._indexGlobalKey.currentContext?.size;
|
||||
_draggingFeedbackSize = widget.indexGlobalKey.currentContext?.size;
|
||||
widget.onDragStarted(
|
||||
widget.child,
|
||||
widget.dragTargetData.draggingIndex,
|
||||
|
@ -1,7 +1,7 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:appflowy_board/src/widgets/board.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../utils/log.dart';
|
||||
import 'drag_state.dart';
|
||||
import 'drag_target.dart';
|
||||
@ -41,7 +41,7 @@ abstract class OverlapDragTargetDelegate {
|
||||
int dragTargetIndex,
|
||||
);
|
||||
|
||||
int canMoveTo(String dragTargetId);
|
||||
int getInsertedIndex(String dragTargetId);
|
||||
}
|
||||
|
||||
/// [OverlappingDragTargetInterceptor] is used to receive the overlapping
|
||||
@ -55,13 +55,14 @@ class OverlappingDragTargetInterceptor extends DragTargetInterceptor {
|
||||
final String reorderFlexId;
|
||||
final List<String> acceptedReorderFlexId;
|
||||
final OverlapDragTargetDelegate delegate;
|
||||
final List<ColumnKey> columnKeys = [];
|
||||
final BoardColumnsState columnsState;
|
||||
Timer? _delayOperation;
|
||||
|
||||
OverlappingDragTargetInterceptor({
|
||||
required this.delegate,
|
||||
required this.reorderFlexId,
|
||||
required this.acceptedReorderFlexId,
|
||||
required this.columnsState,
|
||||
});
|
||||
|
||||
@override
|
||||
@ -79,24 +80,30 @@ class OverlappingDragTargetInterceptor extends DragTargetInterceptor {
|
||||
if (dragTargetId == dragTargetData.reorderFlexId) {
|
||||
delegate.cancel();
|
||||
} else {
|
||||
// Ignore the event if the dragTarget overlaps with the other column's dragTargets.
|
||||
final columnKeys = columnsState.columnDragDragTargets[dragTargetId];
|
||||
if (columnKeys != null) {
|
||||
final keys = columnKeys.values.toList();
|
||||
if (dragTargetData.isOverlapWithWidgets(keys)) {
|
||||
_delayOperation?.cancel();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/// The priority of the column interactions is high than the cross column.
|
||||
/// Workaround: delay 100 milliseconds to lower the cross column event priority.
|
||||
///
|
||||
_delayOperation?.cancel();
|
||||
_delayOperation = Timer(const Duration(milliseconds: 100), () {
|
||||
final index = delegate.canMoveTo(dragTargetId);
|
||||
final index = delegate.getInsertedIndex(dragTargetId);
|
||||
if (index != -1) {
|
||||
Log.trace(
|
||||
'[$OverlappingDragTargetInterceptor] move to $dragTargetId at $index');
|
||||
delegate.moveTo(dragTargetId, dragTargetData, index);
|
||||
|
||||
// final columnIndex = columnKeys
|
||||
// .indexWhere((element) => element.columnId == dragTargetId);
|
||||
// if (columnIndex != -1) {
|
||||
// final state = columnKeys[columnIndex].key.currentState;
|
||||
// if (state is ReorderFlexState) {
|
||||
// state.handleOnWillAccept(context, index);
|
||||
// }
|
||||
// }
|
||||
columnsState
|
||||
.getReorderFlexState(columnId: dragTargetId)
|
||||
?.resetDragTargetIndex(index);
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -105,12 +112,6 @@ class OverlappingDragTargetInterceptor extends DragTargetInterceptor {
|
||||
}
|
||||
}
|
||||
|
||||
class ColumnKey {
|
||||
String columnId;
|
||||
GlobalKey key;
|
||||
ColumnKey({required this.columnId, required this.key});
|
||||
}
|
||||
|
||||
abstract class CrossReorderFlexDragTargetDelegate {
|
||||
/// * [reorderFlexId] is the id that the [ReorderFlex] passed in.
|
||||
bool acceptNewDragTargetData(
|
||||
|
@ -31,6 +31,11 @@ abstract class ReoderFlexItem {
|
||||
String get id;
|
||||
}
|
||||
|
||||
abstract class ReorderDragTargetIndexKeyStorage {
|
||||
void addKey(String reorderFlexId, String key, GlobalObjectKey value);
|
||||
GlobalObjectKey? readKey(String reorderFlexId, String key);
|
||||
}
|
||||
|
||||
class ReorderFlexConfig {
|
||||
/// The opacity of the dragging widget
|
||||
final double draggingWidgetOpacity = 0.3;
|
||||
@ -52,7 +57,6 @@ class ReorderFlexConfig {
|
||||
|
||||
class ReorderFlex extends StatefulWidget {
|
||||
final ReorderFlexConfig config;
|
||||
|
||||
final List<Widget> children;
|
||||
|
||||
/// [direction] How to place the children, default is Axis.vertical
|
||||
@ -74,18 +78,26 @@ class ReorderFlex extends StatefulWidget {
|
||||
|
||||
final DragTargetInterceptor? interceptor;
|
||||
|
||||
const ReorderFlex({
|
||||
final DraggingStateStorage? dragStateStorage;
|
||||
|
||||
final ReorderDragTargetIndexKeyStorage? dragTargetIndexKeyStorage;
|
||||
|
||||
ReorderFlex({
|
||||
Key? key,
|
||||
this.scrollController,
|
||||
required this.dataSource,
|
||||
required this.children,
|
||||
required this.config,
|
||||
required this.onReorder,
|
||||
this.dragStateStorage,
|
||||
this.dragTargetIndexKeyStorage,
|
||||
this.onDragStarted,
|
||||
this.onDragEnded,
|
||||
this.interceptor,
|
||||
this.direction = Axis.vertical,
|
||||
}) : super(key: key);
|
||||
}) : assert(children.every((Widget w) => w.key != null),
|
||||
'All child must have a key.'),
|
||||
super(key: key);
|
||||
|
||||
@override
|
||||
State<ReorderFlex> createState() => ReorderFlexState();
|
||||
@ -115,7 +127,12 @@ class ReorderFlexState extends State<ReorderFlex>
|
||||
@override
|
||||
void initState() {
|
||||
_notifier = ReorderFlexNotifier();
|
||||
dragState = DraggingState(widget.reorderFlexId);
|
||||
final flexId = widget.reorderFlexId;
|
||||
dragState = widget.dragStateStorage?.read(flexId) ??
|
||||
DraggingState(widget.reorderFlexId);
|
||||
Log.trace('[DragTarget] init dragState: $dragState');
|
||||
|
||||
widget.dragStateStorage?.remove(flexId);
|
||||
|
||||
_animation = DragTargetAnimation(
|
||||
reorderAnimationDuration: widget.config.reorderAnimationDuration,
|
||||
@ -159,7 +176,17 @@ class ReorderFlexState extends State<ReorderFlex>
|
||||
|
||||
for (int i = 0; i < widget.children.length; i += 1) {
|
||||
Widget child = widget.children[i];
|
||||
children.add(_wrap(child, i));
|
||||
final ReoderFlexItem item = widget.dataSource.items[i];
|
||||
|
||||
final indexKey = GlobalObjectKey(child.key!);
|
||||
// Save the index key for quick access
|
||||
widget.dragTargetIndexKeyStorage?.addKey(
|
||||
widget.reorderFlexId,
|
||||
item.id,
|
||||
indexKey,
|
||||
);
|
||||
|
||||
children.add(_wrap(child, i, indexKey));
|
||||
|
||||
// if (widget.config.useMovePlaceholder) {
|
||||
// children.add(DragTargeMovePlaceholder(
|
||||
@ -203,10 +230,10 @@ class ReorderFlexState extends State<ReorderFlex>
|
||||
|
||||
/// [child]: the child will be wrapped with dartTarget
|
||||
/// [childIndex]: the index of the child in a list
|
||||
Widget _wrap(Widget child, int childIndex) {
|
||||
Widget _wrap(Widget child, int childIndex, GlobalObjectKey indexKey) {
|
||||
return Builder(builder: (context) {
|
||||
final ReorderDragTarget dragTarget =
|
||||
_buildDragTarget(context, child, childIndex);
|
||||
_buildDragTarget(context, child, childIndex, indexKey);
|
||||
int shiftedIndex = childIndex;
|
||||
|
||||
if (dragState.isOverlapWithPhantom()) {
|
||||
@ -312,22 +339,31 @@ class ReorderFlexState extends State<ReorderFlex>
|
||||
}
|
||||
|
||||
ReorderDragTarget _buildDragTarget(
|
||||
BuildContext builderContext, Widget child, int dragTargetIndex) {
|
||||
final ReoderFlexItem reorderFlexItem =
|
||||
widget.dataSource.items[dragTargetIndex];
|
||||
BuildContext builderContext,
|
||||
Widget child,
|
||||
int dragTargetIndex,
|
||||
GlobalObjectKey indexKey,
|
||||
) {
|
||||
final reorderFlexItem = widget.dataSource.items[dragTargetIndex];
|
||||
return ReorderDragTarget<FlexDragTargetData>(
|
||||
indexGlobalKey: indexKey,
|
||||
dragTargetData: FlexDragTargetData(
|
||||
draggingIndex: dragTargetIndex,
|
||||
reorderFlexId: widget.reorderFlexId,
|
||||
reorderFlexItem: reorderFlexItem,
|
||||
state: dragState,
|
||||
dragTargetId: reorderFlexItem.id,
|
||||
dragTargetIndexKey: indexKey,
|
||||
),
|
||||
onDragStarted: (draggingWidget, draggingIndex, size) {
|
||||
Log.debug(
|
||||
"[DragTarget] Column:[${widget.dataSource.identifier}] start dragging item at $draggingIndex");
|
||||
_startDragging(draggingWidget, draggingIndex, size);
|
||||
widget.onDragStarted?.call(draggingIndex);
|
||||
widget.dragStateStorage?.remove(widget.reorderFlexId);
|
||||
},
|
||||
onDragMoved: (dragTargetData, offset) {
|
||||
dragTargetData.dragTargetOffset = offset;
|
||||
},
|
||||
onDragEnded: (dragTargetData) {
|
||||
if (!mounted) return;
|
||||
@ -431,14 +467,20 @@ class ReorderFlexState extends State<ReorderFlex>
|
||||
});
|
||||
}
|
||||
|
||||
void resetDragTargetIndex(int dragTargetIndex) {
|
||||
dragState.setStartDraggingIndex(dragTargetIndex);
|
||||
widget.dragStateStorage?.write(
|
||||
widget.reorderFlexId,
|
||||
dragState,
|
||||
);
|
||||
}
|
||||
|
||||
bool handleOnWillAccept(BuildContext context, int dragTargetIndex) {
|
||||
final dragIndex = dragState.dragStartIndex;
|
||||
|
||||
/// The [willAccept] will be true if the dargTarget is the widget that gets
|
||||
/// dragged and it is dragged on top of the other dragTargets.
|
||||
///
|
||||
Log.trace(
|
||||
'[$ReorderDragTarget] ${widget.dataSource.identifier} on will accept, dragIndex:$dragIndex, dragTargetIndex:$dragTargetIndex, count: ${widget.dataSource.items.length}');
|
||||
|
||||
bool willAccept =
|
||||
dragState.dragStartIndex == dragIndex && dragIndex != dragTargetIndex;
|
||||
@ -452,6 +494,9 @@ class ReorderFlexState extends State<ReorderFlex>
|
||||
_requestAnimationToNextIndex(isAcceptingNewTarget: true);
|
||||
});
|
||||
|
||||
Log.trace(
|
||||
'[$ReorderDragTarget] ${widget.reorderFlexId} dragging state: $dragState}');
|
||||
|
||||
_scrollTo(context);
|
||||
|
||||
/// If the target is not the original starting point, then we will accept the drop.
|
||||
@ -515,6 +560,50 @@ class ReorderFlexState extends State<ReorderFlex>
|
||||
}
|
||||
}
|
||||
|
||||
void scrollToBottom(VoidCallback? completed) {
|
||||
if (_scrolling) {
|
||||
completed?.call();
|
||||
return;
|
||||
}
|
||||
|
||||
if (widget.dataSource.items.isNotEmpty) {
|
||||
final item = widget.dataSource.items.last;
|
||||
final indexKey = widget.dragTargetIndexKeyStorage?.readKey(
|
||||
widget.reorderFlexId,
|
||||
item.id,
|
||||
);
|
||||
if (indexKey == null) {
|
||||
completed?.call();
|
||||
return;
|
||||
}
|
||||
|
||||
final indexContext = indexKey.currentContext;
|
||||
if (indexContext == null || _scrollController.hasClients == false) {
|
||||
completed?.call();
|
||||
return;
|
||||
}
|
||||
|
||||
final renderObject = indexContext.findRenderObject();
|
||||
if (renderObject != null) {
|
||||
_scrolling = true;
|
||||
_scrollController.position
|
||||
.ensureVisible(
|
||||
renderObject,
|
||||
alignment: 0.5,
|
||||
duration: const Duration(milliseconds: 120),
|
||||
)
|
||||
.then((value) {
|
||||
setState(() {
|
||||
_scrolling = false;
|
||||
completed?.call();
|
||||
});
|
||||
});
|
||||
} else {
|
||||
completed?.call();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Scrolls to a target context if that context is not on the screen.
|
||||
void _scrollTo(BuildContext context) {
|
||||
if (_scrolling) return;
|
||||
|
@ -203,7 +203,7 @@ class BoardPhantomController extends OverlapDragTargetDelegate
|
||||
}
|
||||
|
||||
@override
|
||||
int canMoveTo(String dragTargetId) {
|
||||
int getInsertedIndex(String dragTargetId) {
|
||||
if (columnsState.isDragging(dragTargetId)) {
|
||||
return -1;
|
||||
}
|
||||
|
@ -18,7 +18,7 @@ use flowy_sync::client_grid::{GridRevisionChangeset, GridRevisionPad, JsonDeseri
|
||||
use flowy_sync::entities::revision::Revision;
|
||||
use flowy_sync::errors::CollaborateResult;
|
||||
use flowy_sync::util::make_text_delta_from_revisions;
|
||||
use lib_infra::future::FutureResult;
|
||||
use lib_infra::future::{wrap_future, FutureResult};
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
@ -591,34 +591,33 @@ impl GridRevisionEditor {
|
||||
match self.block_manager.get_row_rev(&from_row_id).await? {
|
||||
None => tracing::warn!("Move row failed, can not find the row:{}", from_row_id),
|
||||
Some(row_rev) => {
|
||||
if let Some(row_changeset) = self
|
||||
.view_manager
|
||||
.move_group_row(row_rev, to_group_id, to_row_id.clone())
|
||||
.await
|
||||
{
|
||||
tracing::trace!("Move group row cause row data changed: {:?}", row_changeset);
|
||||
let block_manager = self.block_manager.clone();
|
||||
self.view_manager
|
||||
.move_group_row(row_rev, to_group_id, to_row_id.clone(), |row_changeset| {
|
||||
wrap_future(async move {
|
||||
tracing::trace!("Move group row cause row data changed: {:?}", row_changeset);
|
||||
let cell_changesets = row_changeset
|
||||
.cell_by_field_id
|
||||
.into_iter()
|
||||
.map(|(field_id, cell_rev)| CellChangesetPB {
|
||||
grid_id: view_id.clone(),
|
||||
row_id: row_changeset.row_id.clone(),
|
||||
field_id,
|
||||
content: cell_rev.data,
|
||||
})
|
||||
.collect::<Vec<CellChangesetPB>>();
|
||||
|
||||
let cell_changesets = row_changeset
|
||||
.cell_by_field_id
|
||||
.into_iter()
|
||||
.map(|(field_id, cell_rev)| CellChangesetPB {
|
||||
grid_id: view_id.clone(),
|
||||
row_id: row_changeset.row_id.clone(),
|
||||
field_id,
|
||||
content: cell_rev.data,
|
||||
for cell_changeset in cell_changesets {
|
||||
match block_manager.update_cell(cell_changeset).await {
|
||||
Ok(_) => {}
|
||||
Err(e) => tracing::error!("Apply cell changeset error:{:?}", e),
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect::<Vec<CellChangesetPB>>();
|
||||
|
||||
for cell_changeset in cell_changesets {
|
||||
match self.block_manager.update_cell(cell_changeset).await {
|
||||
Ok(_) => {}
|
||||
Err(e) => tracing::error!("Apply cell changeset error:{:?}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
@ -140,8 +140,8 @@ impl GridViewRevisionEditor {
|
||||
row_changeset: &mut RowChangeset,
|
||||
to_group_id: &str,
|
||||
to_row_id: Option<String>,
|
||||
) {
|
||||
if let Some(changesets) = self
|
||||
) -> Vec<GroupChangesetPB> {
|
||||
match self
|
||||
.group_service
|
||||
.write()
|
||||
.await
|
||||
@ -150,9 +150,8 @@ impl GridViewRevisionEditor {
|
||||
})
|
||||
.await
|
||||
{
|
||||
for changeset in changesets {
|
||||
self.notify_did_update_group(changeset).await;
|
||||
}
|
||||
None => vec![],
|
||||
Some(changesets) => changesets,
|
||||
}
|
||||
}
|
||||
/// Only call once after grid view editor initialized
|
||||
@ -266,7 +265,7 @@ impl GridViewRevisionEditor {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn notify_did_update_group(&self, changeset: GroupChangesetPB) {
|
||||
pub async fn notify_did_update_group(&self, changeset: GroupChangesetPB) {
|
||||
send_dart_notification(&changeset.group_id, GridNotification::DidUpdateGroup)
|
||||
.payload(changeset)
|
||||
.send();
|
||||
|
@ -134,19 +134,23 @@ impl GridViewManager {
|
||||
row_rev: Arc<RowRevision>,
|
||||
to_group_id: String,
|
||||
to_row_id: Option<String>,
|
||||
) -> Option<RowChangeset> {
|
||||
with_row_changeset: impl FnOnce(RowChangeset) -> AFFuture<()>,
|
||||
) -> FlowyResult<()> {
|
||||
let mut row_changeset = RowChangeset::new(row_rev.id.clone());
|
||||
for view_editor in self.view_editors.iter() {
|
||||
view_editor
|
||||
.move_group_row(&row_rev, &mut row_changeset, &to_group_id, to_row_id.clone())
|
||||
.await;
|
||||
let view_editor = self.get_default_view_editor().await?;
|
||||
let group_changesets = view_editor
|
||||
.move_group_row(&row_rev, &mut row_changeset, &to_group_id, to_row_id.clone())
|
||||
.await;
|
||||
|
||||
if row_changeset.is_empty() == false {
|
||||
with_row_changeset(row_changeset).await;
|
||||
}
|
||||
|
||||
if row_changeset.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(row_changeset)
|
||||
for group_changeset in group_changesets {
|
||||
view_editor.notify_did_update_group(group_changeset).await;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn did_update_field(&self, field_id: &str) -> FlowyResult<()> {
|
||||
|
Loading…
Reference in New Issue
Block a user