Merge pull request #1197 from AppFlowy-IO/feat/support_drag_group

Feat/support drag group
This commit is contained in:
Nathan.fooo 2022-10-01 17:19:07 +08:00 committed by GitHub
commit f355ff01e4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 321 additions and 240 deletions

View File

@ -36,21 +36,21 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
super(BoardState.initial(view.id)) { super(BoardState.initial(view.id)) {
boardController = AppFlowyBoardController( boardController = AppFlowyBoardController(
onMoveGroup: ( onMoveGroup: (
fromColumnId, fromGroupId,
fromIndex, fromIndex,
toColumnId, toGroupId,
toIndex, toIndex,
) { ) {
_moveGroup(fromColumnId, toColumnId); _moveGroup(fromGroupId, toGroupId);
}, },
onMoveGroupItem: ( onMoveGroupItem: (
columnId, groupId,
fromIndex, fromIndex,
toIndex, toIndex,
) { ) {
final fromRow = groupControllers[columnId]?.rowAtIndex(fromIndex); final fromRow = groupControllers[groupId]?.rowAtIndex(fromIndex);
final toRow = groupControllers[columnId]?.rowAtIndex(toIndex); final toRow = groupControllers[groupId]?.rowAtIndex(toIndex);
_moveRow(fromRow, columnId, toRow); _moveRow(fromRow, groupId, toRow);
}, },
onMoveGroupItemToGroup: ( onMoveGroupItemToGroup: (
fromGroupId, fromGroupId,

View File

@ -69,7 +69,6 @@ class BoardContent extends StatefulWidget {
class _BoardContentState extends State<BoardContent> { class _BoardContentState extends State<BoardContent> {
late AppFlowyBoardScrollController scrollManager; late AppFlowyBoardScrollController scrollManager;
final Map<String, ValueKey> cardKeysCache = {};
final config = AppFlowyBoardConfig( final config = AppFlowyBoardConfig(
groupBackgroundColor: HexColor.fromHex('#F7F8FC'), groupBackgroundColor: HexColor.fromHex('#F7F8FC'),
@ -139,7 +138,7 @@ class _BoardContentState extends State<BoardContent> {
.read<BoardBloc>() .read<BoardBloc>()
.add(BoardEvent.endEditRow(editingRow.row.id)); .add(BoardEvent.endEditRow(editingRow.row.id));
} else { } else {
scrollManager.scrollToBottom(editingRow.columnId, () { scrollManager.scrollToBottom(editingRow.columnId, (boardContext) {
context context
.read<BoardBloc>() .read<BoardBloc>()
.add(BoardEvent.endEditRow(editingRow.row.id)); .add(BoardEvent.endEditRow(editingRow.row.id));
@ -247,15 +246,8 @@ class _BoardContentState extends State<BoardContent> {
); );
final groupItemId = columnItem.id + group.id; final groupItemId = columnItem.id + group.id;
ValueKey? key = cardKeysCache[groupItemId];
if (key == null) {
final newKey = ValueKey(groupItemId);
cardKeysCache[groupItemId] = newKey;
key = newKey;
}
return AppFlowyGroupCard( return AppFlowyGroupCard(
key: key, key: ValueKey(groupItemId),
margin: config.cardPadding, margin: config.cardPadding,
decoration: _makeBoxDecoration(context), decoration: _makeBoxDecoration(context),
child: BoardCard( child: BoardCard(

View File

@ -1,3 +1,5 @@
# 0.0.8
* Enable drag and drop group
# 0.0.7 # 0.0.7
* Rename some classes * Rename some classes
* Add documentation * Add documentation
@ -7,7 +9,7 @@
# 0.0.5 # 0.0.5
* Optimize insert card animation * Optimize insert card animation
* Enable insert card at the end of the column * Enable insert card at the end of the group
* Fix some bugs * Fix some bugs
# 0.0.4 # 0.0.4
@ -24,6 +26,5 @@
# 0.0.1 # 0.0.1
* Support drag and drop column * Support drag and drop group items from one to another
* Support drag and drop column items from one to another

View File

@ -34,7 +34,7 @@ class _MyAppState extends State<MyApp> {
appBar: AppBar( appBar: AppBar(
title: const Text('AppFlowy Board'), title: const Text('AppFlowy Board'),
), ),
body: _examples[_currentIndex], body: Container(color: Colors.white, child: _examples[_currentIndex]),
bottomNavigationBar: BottomNavigationBar( bottomNavigationBar: BottomNavigationBar(
fixedColor: _bottomNavigationColor, fixedColor: _bottomNavigationColor,
showSelectedLabels: true, showSelectedLabels: true,

View File

@ -21,8 +21,11 @@ class _MultiBoardListExampleState extends State<MultiBoardListExample> {
}, },
); );
late AppFlowyBoardScrollController boardController;
@override @override
void initState() { void initState() {
boardController = AppFlowyBoardScrollController();
final group1 = AppFlowyGroupData(id: "To Do", name: "To Do", items: [ final group1 = AppFlowyGroupData(id: "To Do", name: "To Do", items: [
TextItem("Card 1"), TextItem("Card 1"),
TextItem("Card 2"), TextItem("Card 2"),
@ -67,12 +70,16 @@ class _MultiBoardListExampleState extends State<MultiBoardListExample> {
child: _buildCard(groupItem), child: _buildCard(groupItem),
); );
}, },
boardScrollController: boardController,
footerBuilder: (context, columnData) { footerBuilder: (context, columnData) {
return AppFlowyGroupFooter( return AppFlowyGroupFooter(
icon: const Icon(Icons.add, size: 20), icon: const Icon(Icons.add, size: 20),
title: const Text('New'), title: const Text('New'),
height: 50, height: 50,
margin: config.groupItemPadding, margin: config.groupItemPadding,
onAddButtonClick: () {
boardController.scrollToBottom(columnData.id, (p0) {});
},
); );
}, },
headerBuilder: (context, columnData) { headerBuilder: (context, columnData) {

View File

@ -13,10 +13,8 @@ import '../rendering/board_overlay.dart';
class AppFlowyBoardScrollController { class AppFlowyBoardScrollController {
AppFlowyBoardState? _groupState; AppFlowyBoardState? _groupState;
void scrollToBottom(String groupId, VoidCallback? completed) { void scrollToBottom(String groupId, void Function(BuildContext)? completed) {
_groupState _groupState?.reorderFlexActionMap[groupId]?.scrollToBottom(completed);
?.getReorderFlexState(groupId: groupId)
?.scrollToBottom(completed);
} }
} }
@ -133,7 +131,7 @@ class AppFlowyBoard extends StatelessWidget {
dataController: controller, dataController: controller,
scrollController: scrollController, scrollController: scrollController,
scrollManager: boardScrollController, scrollManager: boardScrollController,
columnsState: _groupState, groupState: _groupState,
background: background, background: background,
delegate: _phantomController, delegate: _phantomController,
groupConstraints: groupConstraints, groupConstraints: groupConstraints,
@ -158,7 +156,7 @@ class _AppFlowyBoardContent extends StatefulWidget {
final ReorderFlexConfig reorderFlexConfig; final ReorderFlexConfig reorderFlexConfig;
final BoxConstraints groupConstraints; final BoxConstraints groupConstraints;
final AppFlowyBoardScrollController? scrollManager; final AppFlowyBoardScrollController? scrollManager;
final AppFlowyBoardState columnsState; final AppFlowyBoardState groupState;
final AppFlowyBoardCardBuilder cardBuilder; final AppFlowyBoardCardBuilder cardBuilder;
final AppFlowyBoardHeaderBuilder? headerBuilder; final AppFlowyBoardHeaderBuilder? headerBuilder;
final AppFlowyBoardFooterBuilder? footerBuilder; final AppFlowyBoardFooterBuilder? footerBuilder;
@ -171,7 +169,7 @@ class _AppFlowyBoardContent extends StatefulWidget {
required this.delegate, required this.delegate,
required this.dataController, required this.dataController,
required this.scrollManager, required this.scrollManager,
required this.columnsState, required this.groupState,
this.scrollController, this.scrollController,
this.background, this.background,
required this.groupConstraints, required this.groupConstraints,
@ -192,8 +190,6 @@ class _AppFlowyBoardContentState extends State<_AppFlowyBoardContent> {
GlobalKey(debugLabel: '$_AppFlowyBoardContent overlay key'); GlobalKey(debugLabel: '$_AppFlowyBoardContent overlay key');
late BoardOverlayEntry _overlayEntry; late BoardOverlayEntry _overlayEntry;
final Map<String, GlobalObjectKey> _reorderFlexKeys = {};
@override @override
void initState() { void initState() {
_overlayEntry = BoardOverlayEntry( _overlayEntry = BoardOverlayEntry(
@ -202,7 +198,7 @@ class _AppFlowyBoardContentState extends State<_AppFlowyBoardContent> {
reorderFlexId: widget.dataController.identifier, reorderFlexId: widget.dataController.identifier,
acceptedReorderFlexId: widget.dataController.groupIds, acceptedReorderFlexId: widget.dataController.groupIds,
delegate: widget.delegate, delegate: widget.delegate,
columnsState: widget.columnsState, columnsState: widget.groupState,
); );
final reorderFlex = ReorderFlex( final reorderFlex = ReorderFlex(
@ -212,7 +208,7 @@ class _AppFlowyBoardContentState extends State<_AppFlowyBoardContent> {
dataSource: widget.dataController, dataSource: widget.dataController,
direction: Axis.horizontal, direction: Axis.horizontal,
interceptor: interceptor, interceptor: interceptor,
reorderable: false, reorderable: true,
children: _buildColumns(), children: _buildColumns(),
); );
@ -257,18 +253,16 @@ class _AppFlowyBoardContentState extends State<_AppFlowyBoardContent> {
dataController: widget.dataController, dataController: widget.dataController,
); );
if (_reorderFlexKeys[columnData.id] == null) { final reorderFlexAction = ReorderFlexActionImpl();
_reorderFlexKeys[columnData.id] = GlobalObjectKey(columnData.id); widget.groupState.reorderFlexActionMap[columnData.id] =
} reorderFlexAction;
GlobalObjectKey reorderFlexKey = _reorderFlexKeys[columnData.id]!;
return ChangeNotifierProvider.value( return ChangeNotifierProvider.value(
key: ValueKey(columnData.id), key: ValueKey(columnData.id),
value: widget.dataController.getGroupController(columnData.id), value: widget.dataController.getGroupController(columnData.id),
child: Consumer<AppFlowyGroupController>( child: Consumer<AppFlowyGroupController>(
builder: (context, value, child) { builder: (context, value, child) {
final boardColumn = AppFlowyBoardGroup( final boardColumn = AppFlowyBoardGroup(
reorderFlexKey: reorderFlexKey,
// key: PageStorageKey<String>(columnData.id), // key: PageStorageKey<String>(columnData.id),
margin: _marginFromIndex(columnIndex), margin: _marginFromIndex(columnIndex),
itemMargin: widget.config.groupItemPadding, itemMargin: widget.config.groupItemPadding,
@ -281,11 +275,11 @@ class _AppFlowyBoardContentState extends State<_AppFlowyBoardContent> {
onReorder: widget.dataController.moveGroupItem, onReorder: widget.dataController.moveGroupItem,
cornerRadius: widget.config.cornerRadius, cornerRadius: widget.config.cornerRadius,
backgroundColor: widget.config.groupBackgroundColor, backgroundColor: widget.config.groupBackgroundColor,
dragStateStorage: widget.columnsState, dragStateStorage: widget.groupState,
dragTargetIndexKeyStorage: widget.columnsState, dragTargetKeys: widget.groupState,
reorderFlexAction: reorderFlexAction,
); );
widget.columnsState.addGroup(columnData.id, boardColumn);
return ConstrainedBox( return ConstrainedBox(
constraints: widget.groupConstraints, constraints: widget.groupConstraints,
child: boardColumn, child: boardColumn,
@ -356,71 +350,61 @@ class AppFlowyGroupContext {
} }
class AppFlowyBoardState extends DraggingStateStorage class AppFlowyBoardState extends DraggingStateStorage
with ReorderDragTargetIndexKeyStorage { with ReorderDragTargeKeys {
final Map<String, DraggingState> groupDragStates = {};
final Map<String, Map<String, GlobalObjectKey>> groupDragTargetKeys = {};
/// Quick access to the [AppFlowyBoardGroup], the [GlobalKey] is bind to the /// Quick access to the [AppFlowyBoardGroup], the [GlobalKey] is bind to the
/// AppFlowyBoardGroup's [ReorderFlex] widget. /// AppFlowyBoardGroup's [ReorderFlex] widget.
final Map<String, GlobalKey> groupReorderFlexKeys = {}; final Map<String, ReorderFlexActionImpl> reorderFlexActionMap = {};
final Map<String, DraggingState> groupDragStates = {};
final Map<String, Map<String, GlobalObjectKey>> groupDragDragTargets = {};
void addGroup(String groupId, AppFlowyBoardGroup groupWidget) {
groupReorderFlexKeys[groupId] = groupWidget.reorderFlexKey;
}
ReorderFlexState? getReorderFlexState({required String groupId}) {
final flexGlobalKey = groupReorderFlexKeys[groupId];
if (flexGlobalKey == null) return null;
if (flexGlobalKey.currentState is! ReorderFlexState) return null;
final state = flexGlobalKey.currentState as ReorderFlexState;
return state;
}
ReorderFlex? getReorderFlex({required String groupId}) {
final flexGlobalKey = groupReorderFlexKeys[groupId];
if (flexGlobalKey == null) return null;
if (flexGlobalKey.currentWidget is! ReorderFlex) return null;
final widget = flexGlobalKey.currentWidget as ReorderFlex;
return widget;
}
@override @override
DraggingState? read(String reorderFlexId) { DraggingState? readState(String reorderFlexId) {
return groupDragStates[reorderFlexId]; return groupDragStates[reorderFlexId];
} }
@override @override
void write(String reorderFlexId, DraggingState state) { void insertState(String reorderFlexId, DraggingState state) {
Log.trace('$reorderFlexId Write dragging state: $state'); Log.trace('$reorderFlexId Write dragging state: $state');
groupDragStates[reorderFlexId] = state; groupDragStates[reorderFlexId] = state;
} }
@override @override
void remove(String reorderFlexId) { void removeState(String reorderFlexId) {
groupDragStates.remove(reorderFlexId); groupDragStates.remove(reorderFlexId);
} }
@override @override
void addKey( void insertDragTarget(
String reorderFlexId, String reorderFlexId,
String key, String key,
GlobalObjectKey<State<StatefulWidget>> value, GlobalObjectKey<State<StatefulWidget>> value,
) { ) {
Map<String, GlobalObjectKey>? group = groupDragDragTargets[reorderFlexId]; Map<String, GlobalObjectKey>? group = groupDragTargetKeys[reorderFlexId];
if (group == null) { if (group == null) {
group = {}; group = {};
groupDragDragTargets[reorderFlexId] = group; groupDragTargetKeys[reorderFlexId] = group;
} }
group[key] = value; group[key] = value;
} }
@override @override
GlobalObjectKey<State<StatefulWidget>>? readKey( GlobalObjectKey<State<StatefulWidget>>? getDragTarget(
String reorderFlexId, String key) { String reorderFlexId,
Map<String, GlobalObjectKey>? group = groupDragDragTargets[reorderFlexId]; String key,
) {
Map<String, GlobalObjectKey>? group = groupDragTargetKeys[reorderFlexId];
if (group != null) { if (group != null) {
return group[key]; return group[key];
} else { } else {
return null; return null;
} }
} }
@override
void removeDragTarget(String reorderFlexId) {
groupDragTargetKeys.remove(reorderFlexId);
}
} }
class ReorderFlexActionImpl extends ReorderFlexAction {}

View File

@ -90,21 +90,21 @@ class AppFlowyBoardGroup extends StatefulWidget {
final DraggingStateStorage? dragStateStorage; final DraggingStateStorage? dragStateStorage;
final ReorderDragTargetIndexKeyStorage? dragTargetIndexKeyStorage; final ReorderDragTargeKeys? dragTargetKeys;
final GlobalObjectKey reorderFlexKey; final ReorderFlexAction? reorderFlexAction;
const AppFlowyBoardGroup({ const AppFlowyBoardGroup({
Key? key, Key? key,
required this.reorderFlexKey,
this.headerBuilder, this.headerBuilder,
this.footerBuilder, this.footerBuilder,
required this.cardBuilder, required this.cardBuilder,
required this.onReorder, required this.onReorder,
required this.dataSource, required this.dataSource,
required this.phantomController, required this.phantomController,
this.reorderFlexAction,
this.dragStateStorage, this.dragStateStorage,
this.dragTargetIndexKeyStorage, this.dragTargetKeys,
this.scrollController, this.scrollController,
this.onDragStarted, this.onDragStarted,
this.onDragEnded, this.onDragEnded,
@ -146,9 +146,9 @@ class _AppFlowyBoardGroupState extends State<AppFlowyBoardGroup> {
); );
Widget reorderFlex = ReorderFlex( Widget reorderFlex = ReorderFlex(
key: widget.reorderFlexKey, key: ValueKey(widget.groupId),
dragStateStorage: widget.dragStateStorage, dragStateStorage: widget.dragStateStorage,
dragTargetIndexKeyStorage: widget.dragTargetIndexKeyStorage, dragTargetKeys: widget.dragTargetKeys,
scrollController: widget.scrollController, scrollController: widget.scrollController,
config: widget.config, config: widget.config,
onDragStarted: (index) { onDragStarted: (index) {
@ -168,6 +168,7 @@ class _AppFlowyBoardGroupState extends State<AppFlowyBoardGroup> {
}, },
dataSource: widget.dataSource, dataSource: widget.dataSource,
interceptor: interceptor, interceptor: interceptor,
reorderFlexAction: widget.reorderFlexAction,
children: children, children: children,
); );

View File

@ -41,7 +41,7 @@ class AppFlowyGroupController extends ChangeNotifier with EquatableMixin {
void updateGroupName(String newName) { void updateGroupName(String newName) {
if (groupData.headerData.groupName != newName) { if (groupData.headerData.groupName != newName) {
groupData.headerData.groupName = newName; groupData.headerData.groupName = newName;
notifyListeners(); _notify();
} }
} }
@ -56,7 +56,7 @@ class AppFlowyGroupController extends ChangeNotifier with EquatableMixin {
Log.debug('[$AppFlowyGroupController] $groupData remove item at $index'); Log.debug('[$AppFlowyGroupController] $groupData remove item at $index');
final item = groupData._items.removeAt(index); final item = groupData._items.removeAt(index);
if (notify) { if (notify) {
notifyListeners(); _notify();
} }
return item; return item;
} }
@ -81,7 +81,7 @@ class AppFlowyGroupController extends ChangeNotifier with EquatableMixin {
'[$AppFlowyGroupController] $groupData move item from $fromIndex to $toIndex'); '[$AppFlowyGroupController] $groupData move item from $fromIndex to $toIndex');
final item = groupData._items.removeAt(fromIndex); final item = groupData._items.removeAt(fromIndex);
groupData._items.insert(toIndex, item); groupData._items.insert(toIndex, item);
notifyListeners(); _notify();
return true; return true;
} }
@ -102,7 +102,7 @@ class AppFlowyGroupController extends ChangeNotifier with EquatableMixin {
groupData._items.add(item); groupData._items.add(item);
} }
if (notify) notifyListeners(); if (notify) _notify();
return true; return true;
} }
} }
@ -112,7 +112,7 @@ class AppFlowyGroupController extends ChangeNotifier with EquatableMixin {
return false; return false;
} else { } else {
groupData._items.add(item); groupData._items.add(item);
if (notify) notifyListeners(); if (notify) _notify();
return true; return true;
} }
} }
@ -135,7 +135,7 @@ class AppFlowyGroupController extends ChangeNotifier with EquatableMixin {
'[$AppFlowyGroupController] $groupData replace $removedItem with $newItem at $index'); '[$AppFlowyGroupController] $groupData replace $removedItem with $newItem at $index');
} }
notifyListeners(); _notify();
} }
void replaceOrInsertItem(AppFlowyGroupItem newItem) { void replaceOrInsertItem(AppFlowyGroupItem newItem) {
@ -143,10 +143,10 @@ class AppFlowyGroupController extends ChangeNotifier with EquatableMixin {
if (index != -1) { if (index != -1) {
groupData._items.removeAt(index); groupData._items.removeAt(index);
groupData._items.insert(index, newItem); groupData._items.insert(index, newItem);
notifyListeners(); _notify();
} else { } else {
groupData._items.add(newItem); groupData._items.add(newItem);
notifyListeners(); _notify();
} }
} }
@ -154,6 +154,10 @@ class AppFlowyGroupController extends ChangeNotifier with EquatableMixin {
return groupData._items.indexWhere((element) => element.id == item.id) != return groupData._items.indexWhere((element) => element.id == item.id) !=
-1; -1;
} }
void _notify() {
notifyListeners();
}
} }
/// [AppFlowyGroupData] represents the data of each group of the Board. /// [AppFlowyGroupData] represents the data of each group of the Board.

View File

@ -69,9 +69,9 @@ class FlexDragTargetData extends DragTargetData {
} }
abstract class DraggingStateStorage { abstract class DraggingStateStorage {
void write(String reorderFlexId, DraggingState state); void insertState(String reorderFlexId, DraggingState state);
void remove(String reorderFlexId); void removeState(String reorderFlexId);
DraggingState? read(String reorderFlexId); DraggingState? readState(String reorderFlexId);
} }
class DraggingState { class DraggingState {

View File

@ -73,11 +73,15 @@ class ReorderDragTarget<T extends DragTargetData> extends StatefulWidget {
final ReorderFlexDraggableTargetBuilder? draggableTargetBuilder; final ReorderFlexDraggableTargetBuilder? draggableTargetBuilder;
final AnimationController insertAnimationController; final AnimationController insertAnimationController;
final AnimationController deleteAnimationController; final AnimationController deleteAnimationController;
final bool useMoveAnimation; final bool useMoveAnimation;
final bool draggable; final bool draggable;
final double draggingOpacity;
const ReorderDragTarget({ const ReorderDragTarget({
Key? key, Key? key,
required this.child, required this.child,
@ -94,6 +98,7 @@ class ReorderDragTarget<T extends DragTargetData> extends StatefulWidget {
this.onAccept, this.onAccept,
this.onLeave, this.onLeave,
this.draggableTargetBuilder, this.draggableTargetBuilder,
this.draggingOpacity = 0.3,
}) : super(key: key); }) : super(key: key);
@override @override
@ -164,6 +169,7 @@ class _ReorderDragTargetState<T extends DragTargetData>
feedback: feedbackBuilder, feedback: feedbackBuilder,
childWhenDragging: IgnorePointerWidget( childWhenDragging: IgnorePointerWidget(
useIntrinsicSize: !widget.useMoveAnimation, useIntrinsicSize: !widget.useMoveAnimation,
opacity: widget.draggingOpacity,
child: widget.child, child: widget.child,
), ),
onDragStarted: () { onDragStarted: () {
@ -195,7 +201,10 @@ class _ReorderDragTargetState<T extends DragTargetData>
} }
Widget _buildDraggableFeedback( Widget _buildDraggableFeedback(
BuildContext context, BoxConstraints constraints, Widget child) { BuildContext context,
BoxConstraints constraints,
Widget child,
) {
return Transform( return Transform(
transform: Matrix4.rotationZ(0), transform: Matrix4.rotationZ(0),
alignment: FractionalOffset.topLeft, alignment: FractionalOffset.topLeft,
@ -205,7 +214,7 @@ class _ReorderDragTargetState<T extends DragTargetData>
clipBehavior: Clip.hardEdge, clipBehavior: Clip.hardEdge,
child: ConstrainedBox( child: ConstrainedBox(
constraints: constraints, constraints: constraints,
child: Opacity(opacity: 0.3, child: child), child: Opacity(opacity: widget.draggingOpacity, child: child),
), ),
), ),
); );
@ -274,8 +283,11 @@ class DragTargetAnimation {
class IgnorePointerWidget extends StatelessWidget { class IgnorePointerWidget extends StatelessWidget {
final Widget? child; final Widget? child;
final bool useIntrinsicSize; final bool useIntrinsicSize;
final double opacity;
const IgnorePointerWidget({ const IgnorePointerWidget({
required this.child, required this.child,
required this.opacity,
this.useIntrinsicSize = false, this.useIntrinsicSize = false,
Key? key, Key? key,
}) : super(key: key); }) : super(key: key);
@ -286,11 +298,10 @@ class IgnorePointerWidget extends StatelessWidget {
? child ? child
: SizedBox(width: 0.0, height: 0.0, child: child); : SizedBox(width: 0.0, height: 0.0, child: child);
final opacity = useIntrinsicSize ? 0.3 : 0.0;
return IgnorePointer( return IgnorePointer(
ignoring: true, ignoring: true,
child: Opacity( child: Opacity(
opacity: opacity, opacity: useIntrinsicSize ? opacity : 0.0,
child: sizedChild, child: sizedChild,
), ),
); );
@ -300,8 +311,10 @@ class IgnorePointerWidget extends StatelessWidget {
class AbsorbPointerWidget extends StatelessWidget { class AbsorbPointerWidget extends StatelessWidget {
final Widget? child; final Widget? child;
final bool useIntrinsicSize; final bool useIntrinsicSize;
final double opacity;
const AbsorbPointerWidget({ const AbsorbPointerWidget({
required this.child, required this.child,
required this.opacity,
this.useIntrinsicSize = false, this.useIntrinsicSize = false,
Key? key, Key? key,
}) : super(key: key); }) : super(key: key);
@ -312,10 +325,9 @@ class AbsorbPointerWidget extends StatelessWidget {
? child ? child
: SizedBox(width: 0.0, height: 0.0, child: child); : SizedBox(width: 0.0, height: 0.0, child: child);
final opacity = useIntrinsicSize ? 0.3 : 0.0;
return AbsorbPointer( return AbsorbPointer(
child: Opacity( child: Opacity(
opacity: opacity, opacity: useIntrinsicSize ? opacity : 0.0,
child: sizedChild, child: sizedChild,
), ),
); );
@ -494,6 +506,7 @@ class _FakeDragTargetState<T extends DragTargetData>
sizeFactor: widget.deleteAnimationController, sizeFactor: widget.deleteAnimationController,
axis: Axis.vertical, axis: Axis.vertical,
child: AbsorbPointerWidget( child: AbsorbPointerWidget(
opacity: 0.3,
child: widget.child, child: widget.child,
), ),
); );
@ -503,6 +516,7 @@ class _FakeDragTargetState<T extends DragTargetData>
axis: Axis.vertical, axis: Axis.vertical,
child: AbsorbPointerWidget( child: AbsorbPointerWidget(
useIntrinsicSize: true, useIntrinsicSize: true,
opacity: 0.3,
child: widget.child, child: widget.child,
), ),
); );

View File

@ -81,7 +81,7 @@ class OverlappingDragTargetInterceptor extends DragTargetInterceptor {
delegate.cancel(); delegate.cancel();
} else { } else {
// Ignore the event if the dragTarget overlaps with the other column's dragTargets. // Ignore the event if the dragTarget overlaps with the other column's dragTargets.
final columnKeys = columnsState.groupDragDragTargets[dragTargetId]; final columnKeys = columnsState.groupDragTargetKeys[dragTargetId];
if (columnKeys != null) { if (columnKeys != null) {
final keys = columnKeys.values.toList(); final keys = columnKeys.values.toList();
if (dragTargetData.isOverlapWithWidgets(keys)) { if (dragTargetData.isOverlapWithWidgets(keys)) {
@ -102,8 +102,7 @@ class OverlappingDragTargetInterceptor extends DragTargetInterceptor {
delegate.dragTargetDidMoveToReorderFlex( delegate.dragTargetDidMoveToReorderFlex(
dragTargetId, dragTargetData, index); dragTargetId, dragTargetData, index);
columnsState columnsState.reorderFlexActionMap[dragTargetId]
.getReorderFlexState(groupId: dragTargetId)
?.resetDragTargetIndex(index); ?.resetDragTargetIndex(index);
} }
}); });

View File

@ -31,9 +31,32 @@ abstract class ReoderFlexItem {
String get id; String get id;
} }
abstract class ReorderDragTargetIndexKeyStorage { /// Cache each dragTarget's key.
void addKey(String reorderFlexId, String key, GlobalObjectKey value); /// For the moment, the key is used to locate the render object that will
GlobalObjectKey? readKey(String reorderFlexId, String key); /// be passed in the [ScrollPosition]'s [ensureVisible] function.
///
abstract class ReorderDragTargeKeys {
void insertDragTarget(
String reorderFlexId,
String key,
GlobalObjectKey value,
);
GlobalObjectKey? getDragTarget(
String reorderFlexId,
String key,
);
void removeDragTarget(String reorderFlexId);
}
abstract class ReorderFlexAction {
void Function(void Function(BuildContext)?)? _scrollToBottom;
void Function(void Function(BuildContext)?) get scrollToBottom =>
_scrollToBottom!;
void Function(int)? _resetDragTargetIndex;
void Function(int) get resetDragTargetIndex => _resetDragTargetIndex!;
} }
class ReorderFlexConfig { class ReorderFlexConfig {
@ -78,9 +101,12 @@ class ReorderFlex extends StatefulWidget {
final DragTargetInterceptor? interceptor; final DragTargetInterceptor? interceptor;
/// Save the [DraggingState] if the current [ReorderFlex] get reinitialize.
final DraggingStateStorage? dragStateStorage; final DraggingStateStorage? dragStateStorage;
final ReorderDragTargetIndexKeyStorage? dragTargetIndexKeyStorage; final ReorderDragTargeKeys? dragTargetKeys;
final ReorderFlexAction? reorderFlexAction;
final bool reorderable; final bool reorderable;
@ -93,10 +119,11 @@ class ReorderFlex extends StatefulWidget {
required this.onReorder, required this.onReorder,
this.reorderable = true, this.reorderable = true,
this.dragStateStorage, this.dragStateStorage,
this.dragTargetIndexKeyStorage, this.dragTargetKeys,
this.onDragStarted, this.onDragStarted,
this.onDragEnded, this.onDragEnded,
this.interceptor, this.interceptor,
this.reorderFlexAction,
this.direction = Axis.vertical, this.direction = Axis.vertical,
}) : assert(children.every((Widget w) => w.key != null), }) : assert(children.every((Widget w) => w.key != null),
'All child must have a key.'), 'All child must have a key.'),
@ -109,7 +136,7 @@ class ReorderFlex extends StatefulWidget {
} }
class ReorderFlexState extends State<ReorderFlex> class ReorderFlexState extends State<ReorderFlex>
with ReorderFlexMinxi, TickerProviderStateMixin<ReorderFlex> { with ReorderFlexMixin, TickerProviderStateMixin<ReorderFlex> {
/// Controls scrolls and measures scroll progress. /// Controls scrolls and measures scroll progress.
late ScrollController _scrollController; late ScrollController _scrollController;
@ -131,11 +158,11 @@ class ReorderFlexState extends State<ReorderFlex>
void initState() { void initState() {
_notifier = ReorderFlexNotifier(); _notifier = ReorderFlexNotifier();
final flexId = widget.reorderFlexId; final flexId = widget.reorderFlexId;
dragState = widget.dragStateStorage?.read(flexId) ?? dragState = widget.dragStateStorage?.readState(flexId) ??
DraggingState(widget.reorderFlexId); DraggingState(widget.reorderFlexId);
Log.trace('[DragTarget] init dragState: $dragState'); Log.trace('[DragTarget] init dragState: $dragState');
widget.dragStateStorage?.remove(flexId); widget.dragStateStorage?.removeState(flexId);
_animation = DragTargetAnimation( _animation = DragTargetAnimation(
reorderAnimationDuration: widget.config.reorderAnimationDuration, reorderAnimationDuration: widget.config.reorderAnimationDuration,
@ -148,6 +175,14 @@ class ReorderFlexState extends State<ReorderFlex>
vsync: this, vsync: this,
); );
widget.reorderFlexAction?._scrollToBottom = (fn) {
scrollToBottom(fn);
};
widget.reorderFlexAction?._resetDragTargetIndex = (index) {
resetDragTargetIndex(index);
};
super.initState(); super.initState();
} }
@ -184,7 +219,7 @@ class ReorderFlexState extends State<ReorderFlex>
final indexKey = GlobalObjectKey(child.key!); final indexKey = GlobalObjectKey(child.key!);
// Save the index key for quick access // Save the index key for quick access
widget.dragTargetIndexKeyStorage?.addKey( widget.dragTargetKeys?.insertDragTarget(
widget.reorderFlexId, widget.reorderFlexId,
item.id, item.id,
indexKey, indexKey,
@ -236,8 +271,12 @@ class ReorderFlexState extends State<ReorderFlex>
/// [childIndex]: the index of the child in a list /// [childIndex]: the index of the child in a list
Widget _wrap(Widget child, int childIndex, GlobalObjectKey indexKey) { Widget _wrap(Widget child, int childIndex, GlobalObjectKey indexKey) {
return Builder(builder: (context) { return Builder(builder: (context) {
final ReorderDragTarget dragTarget = final ReorderDragTarget dragTarget = _buildDragTarget(
_buildDragTarget(context, child, childIndex, indexKey); context,
child,
childIndex,
indexKey,
);
int shiftedIndex = childIndex; int shiftedIndex = childIndex;
if (dragState.isOverlapWithPhantom()) { if (dragState.isOverlapWithPhantom()) {
@ -342,6 +381,15 @@ class ReorderFlexState extends State<ReorderFlex>
}); });
} }
static ReorderFlexState of(BuildContext context) {
if (context is StatefulElement && context.state is ReorderFlexState) {
return context.state as ReorderFlexState;
}
final ReorderFlexState? result =
context.findAncestorStateOfType<ReorderFlexState>();
return result!;
}
ReorderDragTarget _buildDragTarget( ReorderDragTarget _buildDragTarget(
BuildContext builderContext, BuildContext builderContext,
Widget child, Widget child,
@ -364,7 +412,7 @@ class ReorderFlexState extends State<ReorderFlex>
"[DragTarget] Group:[${widget.dataSource.identifier}] start dragging item at $draggingIndex"); "[DragTarget] Group:[${widget.dataSource.identifier}] start dragging item at $draggingIndex");
_startDragging(draggingWidget, draggingIndex, size); _startDragging(draggingWidget, draggingIndex, size);
widget.onDragStarted?.call(draggingIndex); widget.onDragStarted?.call(draggingIndex);
widget.dragStateStorage?.remove(widget.reorderFlexId); widget.dragStateStorage?.removeState(widget.reorderFlexId);
}, },
onDragMoved: (dragTargetData, offset) { onDragMoved: (dragTargetData, offset) {
dragTargetData.dragTargetOffset = offset; dragTargetData.dragTargetOffset = offset;
@ -435,6 +483,7 @@ class ReorderFlexState extends State<ReorderFlex>
draggableTargetBuilder: widget.interceptor?.draggableTargetBuilder, draggableTargetBuilder: widget.interceptor?.draggableTargetBuilder,
useMoveAnimation: widget.config.useMoveAnimation, useMoveAnimation: widget.config.useMoveAnimation,
draggable: widget.reorderable, draggable: widget.reorderable,
draggingOpacity: widget.config.draggingWidgetOpacity,
child: child, child: child,
); );
} }
@ -487,7 +536,7 @@ class ReorderFlexState extends State<ReorderFlex>
} }
dragState.setStartDraggingIndex(dragTargetIndex); dragState.setStartDraggingIndex(dragTargetIndex);
widget.dragStateStorage?.write( widget.dragStateStorage?.insertState(
widget.reorderFlexId, widget.reorderFlexId,
dragState, dragState,
); );
@ -581,46 +630,46 @@ class ReorderFlexState extends State<ReorderFlex>
} }
} }
void scrollToBottom(VoidCallback? completed) { void scrollToBottom(void Function(BuildContext)? completed) {
if (_scrolling) { if (_scrolling) {
completed?.call(); completed?.call(context);
return; return;
} }
if (widget.dataSource.items.isNotEmpty) { if (widget.dataSource.items.isNotEmpty) {
final item = widget.dataSource.items.last; final item = widget.dataSource.items.last;
final indexKey = widget.dragTargetIndexKeyStorage?.readKey( final dragTargetKey = widget.dragTargetKeys?.getDragTarget(
widget.reorderFlexId, widget.reorderFlexId,
item.id, item.id,
); );
if (indexKey == null) { if (dragTargetKey == null) {
completed?.call(); completed?.call(context);
return; return;
} }
final indexContext = indexKey.currentContext; final dragTargetContext = dragTargetKey.currentContext;
if (indexContext == null || _scrollController.hasClients == false) { if (dragTargetContext == null || _scrollController.hasClients == false) {
completed?.call(); completed?.call(context);
return; return;
} }
final renderObject = indexContext.findRenderObject(); final dragTargetRenderObject = dragTargetContext.findRenderObject();
if (renderObject != null) { if (dragTargetRenderObject != null) {
_scrolling = true; _scrolling = true;
_scrollController.position _scrollController.position
.ensureVisible( .ensureVisible(
renderObject, dragTargetRenderObject,
alignment: 0.5, alignment: 0.5,
duration: const Duration(milliseconds: 120), duration: const Duration(milliseconds: 120),
) )
.then((value) { .then((value) {
setState(() { setState(() {
_scrolling = false; _scrolling = false;
completed?.call(); completed?.call(context);
}); });
}); });
} else { } else {
completed?.call(); completed?.call(context);
} }
} }
} }

View File

@ -3,7 +3,7 @@ import 'package:flutter/widgets.dart';
import '../transitions.dart'; import '../transitions.dart';
import 'drag_target.dart'; import 'drag_target.dart';
mixin ReorderFlexMinxi { mixin ReorderFlexMixin {
@protected @protected
Widget makeAppearingWidget( Widget makeAppearingWidget(
Widget child, Widget child,

View File

@ -1,6 +1,6 @@
name: appflowy_board name: appflowy_board
description: AppFlowyBoard is a board-style widget that consists of multi-groups. It supports drag and drop between different groups. description: AppFlowyBoard is a board-style widget that consists of multi-groups. It supports drag and drop between different groups.
version: 0.0.7 version: 0.0.8
homepage: https://github.com/AppFlowy-IO/AppFlowy homepage: https://github.com/AppFlowy-IO/AppFlowy
repository: https://github.com/AppFlowy-IO/AppFlowy/tree/main/frontend/app_flowy/packages/appflowy_board repository: https://github.com/AppFlowy-IO/AppFlowy/tree/main/frontend/app_flowy/packages/appflowy_board

View File

@ -1444,9 +1444,9 @@ checksum = "eabb4a44450da02c90444cf74558da904edde8fb4e9035a9a6a4e15445af0bd7"
[[package]] [[package]]
name = "hashbrown" name = "hashbrown"
version = "0.11.2" version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
[[package]] [[package]]
name = "heck" name = "heck"
@ -1610,9 +1610,9 @@ checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683"
[[package]] [[package]]
name = "indexmap" name = "indexmap"
version = "1.8.1" version = "1.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f647032dfaa1f8b6dc29bd3edb7bbef4861b8b8007ebb118d6db284fd59f6ee" checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e"
dependencies = [ dependencies = [
"autocfg", "autocfg",
"hashbrown", "hashbrown",

View File

@ -34,7 +34,7 @@ rayon = "1.5.2"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = {version = "1.0"} serde_json = {version = "1.0"}
serde_repr = "0.1" serde_repr = "0.1"
indexmap = {version = "1.8.1", features = ["serde"]} indexmap = {version = "1.9.1", features = ["serde"]}
fancy-regex = "0.10.0" fancy-regex = "0.10.0"
regex = "1.5.6" regex = "1.5.6"
url = { version = "2"} url = { version = "2"}

View File

@ -98,6 +98,7 @@ pub struct MoveGroupPayloadPB {
pub to_group_id: String, pub to_group_id: String,
} }
#[derive(Debug)]
pub struct MoveGroupParams { pub struct MoveGroupParams {
pub view_id: String, pub view_id: String,
pub from_group_id: String, pub from_group_id: String,

View File

@ -368,6 +368,7 @@ impl GridRevisionEditor {
Ok(row_pb) Ok(row_pb)
} }
#[tracing::instrument(level = "trace", skip_all, err)]
pub async fn move_group(&self, params: MoveGroupParams) -> FlowyResult<()> { pub async fn move_group(&self, params: MoveGroupParams) -> FlowyResult<()> {
let _ = self.view_manager.move_group(params).await?; let _ = self.view_manager.move_group(params).await?;
Ok(()) Ok(())

View File

@ -173,6 +173,7 @@ impl GridViewRevisionEditor {
Ok(groups.into_iter().map(GroupPB::from).collect()) Ok(groups.into_iter().map(GroupPB::from).collect())
} }
#[tracing::instrument(level = "trace", skip(self), err)]
pub(crate) async fn move_group(&self, params: MoveGroupParams) -> FlowyResult<()> { pub(crate) async fn move_group(&self, params: MoveGroupParams) -> FlowyResult<()> {
let _ = self let _ = self
.group_controller .group_controller
@ -180,7 +181,7 @@ impl GridViewRevisionEditor {
.await .await
.move_group(&params.from_group_id, &params.to_group_id)?; .move_group(&params.from_group_id, &params.to_group_id)?;
match self.group_controller.read().await.get_group(&params.from_group_id) { match self.group_controller.read().await.get_group(&params.from_group_id) {
None => {} None => tracing::warn!("Can not find the group with id: {}", params.from_group_id),
Some((index, group)) => { Some((index, group)) => {
let inserted_group = InsertedGroupPB { let inserted_group = InsertedGroupPB {
group: GroupPB::from(group), group: GroupPB::from(group),
@ -228,7 +229,11 @@ impl GridViewRevisionEditor {
let _ = self let _ = self
.modify(|pad| { .modify(|pad| {
let configuration = default_group_configuration(&field_rev); let configuration = default_group_configuration(&field_rev);
let changeset = pad.insert_group(&params.field_id, &params.field_type_rev, configuration)?; let changeset = pad.insert_or_update_group_configuration(
&params.field_id,
&params.field_type_rev,
configuration,
)?;
Ok(changeset) Ok(changeset)
}) })
.await?; .await?;
@ -496,10 +501,11 @@ impl GroupConfigurationWriter for GroupConfigurationWriterImpl {
let field_id = field_id.to_owned(); let field_id = field_id.to_owned();
wrap_future(async move { wrap_future(async move {
let changeset = view_pad let changeset = view_pad.write().await.insert_or_update_group_configuration(
.write() &field_id,
.await &field_type,
.insert_group(&field_id, &field_type, group_configuration)?; group_configuration,
)?;
if let Some(changeset) = changeset { if let Some(changeset) = changeset {
let _ = apply_change(&user_id, rev_manager, changeset).await?; let _ = apply_change(&user_id, rev_manager, changeset).await?;

View File

@ -1,5 +1,5 @@
use crate::entities::{GroupPB, GroupViewChangesetPB}; use crate::entities::{GroupPB, GroupViewChangesetPB};
use crate::services::group::{default_group_configuration, GeneratedGroup, Group}; use crate::services::group::{default_group_configuration, make_default_group, GeneratedGroup, Group};
use flowy_error::{FlowyError, FlowyResult}; use flowy_error::{FlowyError, FlowyResult};
use flowy_grid_data_model::revision::{ use flowy_grid_data_model::revision::{
FieldRevision, FieldTypeRevision, GroupConfigurationContentSerde, GroupConfigurationRevision, GroupRevision, FieldRevision, FieldTypeRevision, GroupConfigurationContentSerde, GroupConfigurationRevision, GroupRevision,
@ -29,10 +29,7 @@ impl<T> std::fmt::Display for GroupContext<T> {
self.groups_map.iter().for_each(|(_, group)| { self.groups_map.iter().for_each(|(_, group)| {
let _ = f.write_fmt(format_args!("Group:{} has {} rows \n", group.id, group.rows.len())); let _ = f.write_fmt(format_args!("Group:{} has {} rows \n", group.id, group.rows.len()));
}); });
let _ = f.write_fmt(format_args!(
"Default group has {} rows \n",
self.default_group.rows.len()
));
Ok(()) Ok(())
} }
} }
@ -44,7 +41,7 @@ pub struct GroupContext<C> {
field_rev: Arc<FieldRevision>, field_rev: Arc<FieldRevision>,
groups_map: IndexMap<String, Group>, groups_map: IndexMap<String, Group>,
/// default_group is used to store the rows that don't belong to any groups. /// default_group is used to store the rows that don't belong to any groups.
default_group: Group, // default_group: Group,
writer: Arc<dyn GroupConfigurationWriter>, writer: Arc<dyn GroupConfigurationWriter>,
} }
@ -59,16 +56,6 @@ where
reader: Arc<dyn GroupConfigurationReader>, reader: Arc<dyn GroupConfigurationReader>,
writer: Arc<dyn GroupConfigurationWriter>, writer: Arc<dyn GroupConfigurationWriter>,
) -> FlowyResult<Self> { ) -> FlowyResult<Self> {
let default_group_id = format!("{}_default_group", view_id);
let default_group = Group {
id: default_group_id,
field_id: field_rev.id.clone(),
name: format!("No {}", field_rev.name),
is_default: true,
is_visible: true,
rows: vec![],
filter_content: "".to_string(),
};
let configuration = match reader.get_configuration().await { let configuration = match reader.get_configuration().await {
None => { None => {
let default_configuration = default_group_configuration(&field_rev); let default_configuration = default_group_configuration(&field_rev);
@ -80,24 +67,22 @@ where
Some(configuration) => configuration, Some(configuration) => configuration,
}; };
// let configuration = C::from_configuration_content(&configuration_rev.content)?;
Ok(Self { Ok(Self {
view_id, view_id,
field_rev, field_rev,
groups_map: IndexMap::new(), groups_map: IndexMap::new(),
default_group,
writer, writer,
configuration, configuration,
configuration_content: PhantomData, configuration_content: PhantomData,
}) })
} }
pub(crate) fn get_default_group(&self) -> &Group { pub(crate) fn get_default_group(&self) -> Option<&Group> {
&self.default_group self.groups_map.get(&self.field_rev.id)
} }
pub(crate) fn get_mut_default_group(&mut self) -> &mut Group { pub(crate) fn get_mut_default_group(&mut self) -> Option<&mut Group> {
&mut self.default_group self.groups_map.get_mut(&self.field_rev.id)
} }
/// Returns the groups without the default group /// Returns the groups without the default group
@ -122,8 +107,6 @@ where
self.groups_map.iter_mut().for_each(|(_, group)| { self.groups_map.iter_mut().for_each(|(_, group)| {
each(group); each(group);
}); });
each(&mut self.default_group);
} }
pub(crate) fn move_group(&mut self, from_id: &str, to_id: &str) -> FlowyResult<()> { pub(crate) fn move_group(&mut self, from_id: &str, to_id: &str) -> FlowyResult<()> {
@ -131,18 +114,23 @@ where
let to_index = self.groups_map.get_index_of(to_id); let to_index = self.groups_map.get_index_of(to_id);
match (from_index, to_index) { match (from_index, to_index) {
(Some(from_index), Some(to_index)) => { (Some(from_index), Some(to_index)) => {
self.groups_map.swap_indices(from_index, to_index); self.groups_map.move_index(from_index, to_index);
self.mut_configuration(|configuration| { self.mut_configuration(|configuration| {
let from_index = configuration.groups.iter().position(|group| group.id == from_id); let from_index = configuration.groups.iter().position(|group| group.id == from_id);
let to_index = configuration.groups.iter().position(|group| group.id == to_id); let to_index = configuration.groups.iter().position(|group| group.id == to_id);
if let (Some(from), Some(to)) = (from_index, to_index) { tracing::info!("Configuration groups: {:?} ", configuration.groups);
configuration.groups.swap(from, to); if let (Some(from), Some(to)) = &(from_index, to_index) {
tracing::trace!("Move group from index:{:?} to index:{:?}", from_index, to_index);
let group = configuration.groups.remove(*from);
configuration.groups.insert(*to, group);
} }
true
from_index.is_some() && to_index.is_some()
})?; })?;
Ok(()) Ok(())
} }
_ => Err(FlowyError::out_of_bounds()), _ => Err(FlowyError::record_not_found().context("Moving group failed. Groups are not exist")),
} }
} }
@ -150,7 +138,6 @@ where
pub(crate) fn init_groups( pub(crate) fn init_groups(
&mut self, &mut self,
generated_groups: Vec<GeneratedGroup>, generated_groups: Vec<GeneratedGroup>,
reset: bool,
) -> FlowyResult<Option<GroupViewChangesetPB>> { ) -> FlowyResult<Option<GroupViewChangesetPB>> {
let mut new_groups = vec![]; let mut new_groups = vec![];
let mut filter_content_map = HashMap::new(); let mut filter_content_map = HashMap::new();
@ -159,16 +146,17 @@ where
new_groups.push(generate_group.group_rev); new_groups.push(generate_group.group_rev);
}); });
let mut old_groups = self.configuration.groups.clone();
if !old_groups.iter().any(|group| group.id == self.field_rev.id) {
old_groups.push(make_default_group(&self.field_rev));
}
let MergeGroupResult { let MergeGroupResult {
mut all_group_revs, mut all_group_revs,
new_group_revs, new_group_revs,
updated_group_revs: _, updated_group_revs: _,
deleted_group_revs, deleted_group_revs,
} = if reset { } = merge_groups(old_groups, new_groups);
merge_groups(&[], new_groups)
} else {
merge_groups(&self.configuration.groups, new_groups)
};
let deleted_group_ids = deleted_group_revs let deleted_group_ids = deleted_group_revs
.into_iter() .into_iter()
@ -197,31 +185,23 @@ where
Some(pos) => { Some(pos) => {
let mut old_group = configuration.groups.remove(pos); let mut old_group = configuration.groups.remove(pos);
group_rev.update_with_other(&old_group); group_rev.update_with_other(&old_group);
is_changed = is_group_changed(group_rev, &old_group);
// Take the GroupRevision if the name has changed old_group.name = group_rev.name.clone();
if is_group_changed(group_rev, &old_group) { configuration.groups.insert(pos, old_group);
old_group.name = group_rev.name.clone();
is_changed = true;
configuration.groups.insert(pos, old_group);
}
} }
} }
} }
is_changed is_changed
})?; })?;
// The len of the filter_content_map should equal to the len of the all_group_revs
debug_assert_eq!(filter_content_map.len(), all_group_revs.len());
all_group_revs.into_iter().for_each(|group_rev| { all_group_revs.into_iter().for_each(|group_rev| {
if let Some(filter_content) = filter_content_map.get(&group_rev.id) { let filter_content = filter_content_map
let group = Group::new( .get(&group_rev.id)
group_rev.id, .cloned()
self.field_rev.id.clone(), .unwrap_or_else(|| "".to_owned());
group_rev.name, let group = Group::new(group_rev.id, self.field_rev.id.clone(), group_rev.name, filter_content);
filter_content.clone(), self.groups_map.insert(group.id.clone(), group);
);
self.groups_map.insert(group.id.clone(), group);
}
}); });
let new_groups = new_group_revs let new_groups = new_group_revs
@ -269,6 +249,7 @@ where
Ok(()) Ok(())
} }
#[tracing::instrument(level = "trace", skip_all, err)]
pub fn save_configuration(&self) -> FlowyResult<()> { pub fn save_configuration(&self) -> FlowyResult<()> {
let configuration = (&*self.configuration).clone(); let configuration = (&*self.configuration).clone();
let writer = self.writer.clone(); let writer = self.writer.clone();
@ -311,13 +292,14 @@ where
} }
} }
fn merge_groups(old_groups: &[GroupRevision], new_groups: Vec<GroupRevision>) -> MergeGroupResult { fn merge_groups(old_groups: Vec<GroupRevision>, new_groups: Vec<GroupRevision>) -> MergeGroupResult {
let mut merge_result = MergeGroupResult::new(); let mut merge_result = MergeGroupResult::new();
if old_groups.is_empty() { // if old_groups.is_empty() {
merge_result.all_group_revs = new_groups.clone(); // merge_result.all_group_revs.extend(new_groups.clone());
merge_result.new_group_revs = new_groups; // merge_result.all_group_revs.push(default_group);
return merge_result; // merge_result.new_group_revs = new_groups;
} // return merge_result;
// }
// group_map is a helper map is used to filter out the new groups. // group_map is a helper map is used to filter out the new groups.
let mut new_group_map: IndexMap<String, GroupRevision> = IndexMap::new(); let mut new_group_map: IndexMap<String, GroupRevision> = IndexMap::new();
@ -329,19 +311,20 @@ fn merge_groups(old_groups: &[GroupRevision], new_groups: Vec<GroupRevision>) ->
for old in old_groups { for old in old_groups {
if let Some(new) = new_group_map.remove(&old.id) { if let Some(new) = new_group_map.remove(&old.id) {
merge_result.all_group_revs.push(new.clone()); merge_result.all_group_revs.push(new.clone());
if is_group_changed(&new, old) { if is_group_changed(&new, &old) {
merge_result.updated_group_revs.push(new); merge_result.updated_group_revs.push(new);
} }
} else { } else {
merge_result.deleted_group_revs.push(old.clone()); merge_result.all_group_revs.push(old);
} }
} }
// Find out the new groups // Find out the new groups
new_group_map.reverse();
let new_groups = new_group_map.into_values(); let new_groups = new_group_map.into_values();
for (_, group) in new_groups.into_iter().enumerate() { for (_, group) in new_groups.into_iter().enumerate() {
merge_result.all_group_revs.push(group.clone()); merge_result.all_group_revs.insert(0, group.clone());
merge_result.new_group_revs.push(group); merge_result.new_group_revs.insert(0, group);
} }
merge_result merge_result
} }

View File

@ -88,7 +88,7 @@ where
pub async fn new(field_rev: &Arc<FieldRevision>, mut configuration: GroupContext<C>) -> FlowyResult<Self> { pub async fn new(field_rev: &Arc<FieldRevision>, mut configuration: GroupContext<C>) -> FlowyResult<Self> {
let type_option = field_rev.get_type_option::<T>(field_rev.ty); let type_option = field_rev.get_type_option::<T>(field_rev.ty);
let groups = G::generate_groups(&field_rev.id, &configuration, &type_option); let groups = G::generate_groups(&field_rev.id, &configuration, &type_option);
let _ = configuration.init_groups(groups, true)?; let _ = configuration.init_groups(groups)?;
Ok(Self { Ok(Self {
field_id: field_rev.id.clone(), field_id: field_rev.id.clone(),
@ -105,8 +105,8 @@ where
&mut self, &mut self,
row_rev: &RowRevision, row_rev: &RowRevision,
other_group_changesets: &[GroupChangesetPB], other_group_changesets: &[GroupChangesetPB],
) -> GroupChangesetPB { ) -> Option<GroupChangesetPB> {
let default_group = self.group_ctx.get_mut_default_group(); let default_group = self.group_ctx.get_mut_default_group()?;
// [other_group_inserted_row] contains all the inserted rows except the default group. // [other_group_inserted_row] contains all the inserted rows except the default group.
let other_group_inserted_row = other_group_changesets let other_group_inserted_row = other_group_changesets
@ -163,7 +163,7 @@ where
} }
default_group.rows.retain(|row| !deleted_row_ids.contains(&row.id)); default_group.rows.retain(|row| !deleted_row_ids.contains(&row.id));
changeset.deleted_rows.extend(deleted_row_ids); changeset.deleted_rows.extend(deleted_row_ids);
changeset Some(changeset)
} }
} }
@ -182,11 +182,14 @@ where
fn groups(&self) -> Vec<Group> { fn groups(&self) -> Vec<Group> {
if self.use_default_group() { if self.use_default_group() {
let mut groups: Vec<Group> = self.group_ctx.groups().into_iter().cloned().collect();
groups.push(self.group_ctx.get_default_group().clone());
groups
} else {
self.group_ctx.groups().into_iter().cloned().collect() self.group_ctx.groups().into_iter().cloned().collect()
} else {
self.group_ctx
.groups()
.into_iter()
.filter(|group| group.id != self.field_id)
.cloned()
.collect::<Vec<_>>()
} }
} }
@ -216,17 +219,18 @@ where
} }
} }
if grouped_rows.is_empty() { if !grouped_rows.is_empty() {
self.group_ctx.get_mut_default_group().add_row(row_rev.into());
} else {
for group_row in grouped_rows { for group_row in grouped_rows {
if let Some(group) = self.group_ctx.get_mut_group(&group_row.group_id) { if let Some(group) = self.group_ctx.get_mut_group(&group_row.group_id) {
group.add_row(group_row.row); group.add_row(group_row.row);
} }
} }
continue;
} }
} else { }
self.group_ctx.get_mut_default_group().add_row(row_rev.into()); match self.group_ctx.get_mut_default_group() {
None => {}
Some(default_group) => default_group.add_row(row_rev.into()),
} }
} }
@ -247,10 +251,11 @@ where
let cell_bytes = decode_any_cell_data(cell_rev.data.clone(), field_rev).1; let cell_bytes = decode_any_cell_data(cell_rev.data.clone(), field_rev).1;
let cell_data = cell_bytes.parser::<P>()?; let cell_data = cell_bytes.parser::<P>()?;
let mut changesets = self.add_row_if_match(row_rev, &cell_data); let mut changesets = self.add_row_if_match(row_rev, &cell_data);
let default_group_changeset = self.update_default_group(row_rev, &changesets); if let Some(default_group_changeset) = self.update_default_group(row_rev, &changesets) {
tracing::trace!("default_group_changeset: {}", default_group_changeset); tracing::trace!("default_group_changeset: {}", default_group_changeset);
if !default_group_changeset.is_empty() { if !default_group_changeset.is_empty() {
changesets.push(default_group_changeset); changesets.push(default_group_changeset);
}
} }
Ok(changesets) Ok(changesets)
} else { } else {
@ -268,12 +273,13 @@ where
let cell_bytes = decode_any_cell_data(cell_rev.data.clone(), field_rev).1; let cell_bytes = decode_any_cell_data(cell_rev.data.clone(), field_rev).1;
let cell_data = cell_bytes.parser::<P>()?; let cell_data = cell_bytes.parser::<P>()?;
Ok(self.remove_row_if_match(row_rev, &cell_data)) Ok(self.remove_row_if_match(row_rev, &cell_data))
} else { } else if let Some(group) = self.group_ctx.get_default_group() {
let group = self.group_ctx.get_default_group();
Ok(vec![GroupChangesetPB::delete( Ok(vec![GroupChangesetPB::delete(
group.id.clone(), group.id.clone(),
vec![row_rev.id.clone()], vec![row_rev.id.clone()],
)]) )])
} else {
Ok(vec![])
} }
} }
@ -297,7 +303,7 @@ where
fn did_update_field(&mut self, field_rev: &FieldRevision) -> FlowyResult<Option<GroupViewChangesetPB>> { fn did_update_field(&mut self, field_rev: &FieldRevision) -> FlowyResult<Option<GroupViewChangesetPB>> {
let type_option = field_rev.get_type_option::<T>(field_rev.ty); let type_option = field_rev.get_type_option::<T>(field_rev.ty);
let groups = G::generate_groups(&field_rev.id, &self.group_ctx, &type_option); let groups = G::generate_groups(&field_rev.id, &self.group_ctx, &type_option);
let changeset = self.group_ctx.init_groups(groups, false)?; let changeset = self.group_ctx.init_groups(groups)?;
Ok(changeset) Ok(changeset)
} }
} }

View File

@ -9,16 +9,17 @@ pub struct Group {
pub is_visible: bool, pub is_visible: bool,
pub(crate) rows: Vec<RowPB>, pub(crate) rows: Vec<RowPB>,
/// [content] is used to determine which group the cell belongs to. /// [filter_content] is used to determine which group the cell belongs to.
pub filter_content: String, pub filter_content: String,
} }
impl Group { impl Group {
pub fn new(id: String, field_id: String, name: String, filter_content: String) -> Self { pub fn new(id: String, field_id: String, name: String, filter_content: String) -> Self {
let is_default = id == field_id;
Self { Self {
id, id,
field_id, field_id,
is_default: false, is_default,
is_visible: true, is_visible: true,
name, name,
rows: vec![], rows: vec![],

View File

@ -8,8 +8,8 @@ use crate::services::group::{
use flowy_error::FlowyResult; use flowy_error::FlowyResult;
use flowy_grid_data_model::revision::{ use flowy_grid_data_model::revision::{
CheckboxGroupConfigurationRevision, DateGroupConfigurationRevision, FieldRevision, GroupConfigurationRevision, CheckboxGroupConfigurationRevision, DateGroupConfigurationRevision, FieldRevision, GroupConfigurationRevision,
LayoutRevision, NumberGroupConfigurationRevision, RowRevision, SelectOptionGroupConfigurationRevision, GroupRevision, LayoutRevision, NumberGroupConfigurationRevision, RowRevision,
TextGroupConfigurationRevision, UrlGroupConfigurationRevision, SelectOptionGroupConfigurationRevision, TextGroupConfigurationRevision, UrlGroupConfigurationRevision,
}; };
use std::sync::Arc; use std::sync::Arc;
@ -79,7 +79,7 @@ pub fn default_group_configuration(field_rev: &FieldRevision) -> GroupConfigurat
let field_id = field_rev.id.clone(); let field_id = field_rev.id.clone();
let field_type_rev = field_rev.ty; let field_type_rev = field_rev.ty;
let field_type: FieldType = field_rev.ty.into(); let field_type: FieldType = field_rev.ty.into();
match field_type { let mut group_configuration_rev = match field_type {
FieldType::RichText => { FieldType::RichText => {
GroupConfigurationRevision::new(field_id, field_type_rev, TextGroupConfigurationRevision::default()) GroupConfigurationRevision::new(field_id, field_type_rev, TextGroupConfigurationRevision::default())
.unwrap() .unwrap()
@ -112,5 +112,23 @@ pub fn default_group_configuration(field_rev: &FieldRevision) -> GroupConfigurat
FieldType::URL => { FieldType::URL => {
GroupConfigurationRevision::new(field_id, field_type_rev, UrlGroupConfigurationRevision::default()).unwrap() GroupConfigurationRevision::new(field_id, field_type_rev, UrlGroupConfigurationRevision::default()).unwrap()
} }
};
// Append the no `status` group
let default_group_rev = GroupRevision {
id: field_rev.id.clone(),
name: format!("No {}", field_rev.name),
visible: true,
};
group_configuration_rev.groups.push(default_group_rev);
group_configuration_rev
}
pub fn make_default_group(field_rev: &FieldRevision) -> GroupRevision {
GroupRevision {
id: field_rev.id.clone(),
name: format!("No {}", field_rev.name),
visible: true,
} }
} }

View File

@ -370,6 +370,28 @@ async fn group_move_group_test() {
test.run_scripts(scripts).await; test.run_scripts(scripts).await;
} }
#[tokio::test]
async fn group_default_move_group_test() {
let mut test = GridGroupTest::new().await;
let group_0 = test.group_at_index(0).await;
let group_3 = test.group_at_index(3).await;
let scripts = vec![
MoveGroup {
from_group_index: 3,
to_group_index: 0,
},
AssertGroup {
group_index: 0,
expected_group: group_3,
},
AssertGroup {
group_index: 1,
expected_group: group_0,
},
];
test.run_scripts(scripts).await;
}
#[tokio::test] #[tokio::test]
async fn group_insert_single_select_option_test() { async fn group_insert_single_select_option_test() {
let mut test = GridGroupTest::new().await; let mut test = GridGroupTest::new().await;
@ -402,7 +424,7 @@ async fn group_group_by_other_field() {
group_index: 1, group_index: 1,
row_count: 2, row_count: 2,
}, },
AssertGroupCount(4), AssertGroupCount(5),
]; ];
test.run_scripts(scripts).await; test.run_scripts(scripts).await;
} }

8
shared-lib/Cargo.lock generated
View File

@ -650,9 +650,9 @@ dependencies = [
[[package]] [[package]]
name = "hashbrown" name = "hashbrown"
version = "0.11.2" version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
[[package]] [[package]]
name = "heck" name = "heck"
@ -732,9 +732,9 @@ dependencies = [
[[package]] [[package]]
name = "indexmap" name = "indexmap"
version = "1.8.1" version = "1.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f647032dfaa1f8b6dc29bd3edb7bbef4861b8b8007ebb118d6db284fd59f6ee" checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e"
dependencies = [ dependencies = [
"autocfg", "autocfg",
"hashbrown", "hashbrown",

View File

@ -12,7 +12,7 @@ serde_json = {version = "1.0"}
serde_repr = "0.1" serde_repr = "0.1"
nanoid = "0.4.0" nanoid = "0.4.0"
flowy-error-code = { path = "../flowy-error-code"} flowy-error-code = { path = "../flowy-error-code"}
indexmap = {version = "1.8.1", features = ["serde"]} indexmap = {version = "1.9.1", features = ["serde"]}
tracing = { version = "0.1", features = ["log"] } tracing = { version = "0.1", features = ["log"] }
[build-dependencies] [build-dependencies]

View File

@ -128,14 +128,6 @@ impl GroupRevision {
} }
} }
pub fn default_group(id: String, group_name: String) -> Self {
Self {
id,
name: group_name,
visible: true,
}
}
pub fn update_with_other(&mut self, other: &GroupRevision) { pub fn update_with_other(&mut self, other: &GroupRevision) {
self.visible = other.visible self.visible = other.visible
} }

View File

@ -66,7 +66,7 @@ impl GridViewRevisionPad {
} }
#[tracing::instrument(level = "trace", skip_all, err)] #[tracing::instrument(level = "trace", skip_all, err)]
pub fn insert_group( pub fn insert_or_update_group_configuration(
&mut self, &mut self,
field_id: &str, field_id: &str,
field_type: &FieldTypeRevision, field_type: &FieldTypeRevision,