feat: hidden kanban groups (#3907)

* feat: hide/unhide ui

* chore: implement collapsible side bar and adjust group header (#2)

* refactor: hidden columns into own file

* chore: adjust new group button position

* fix: flowy icon buton secondary color bleed

* chore: some UI adjustments

* fix: some regressions

* chore: proper group is_visible fetching

* chore: use a bloc to manage hidden groups

* fix: hiding groups not working

* chore: implement hidden group popups

* chore: proper ungrouped item column management

* chore: remove ungrouped items button

* chore: flowy hover build

* fix: clean up code

* test: integration tests

* fix: not null promise on null value

* fix: hide and unhide multiple groups

* chore: i18n and code review

* chore: missed review

* fix: rust-lib-test

* fix: dont completely remove flowyiconhovercolor

* chore: apply suggest

* fix: number of rows inside hidden groups not updating properly

* fix: hidden groups disappearing after collapse

* fix: hidden group title alignment

* fix: insert newly unhidden groups into the correct position

* chore: adjust padding all around

* feat: reorder hidden groups

* chore: adjust padding

* chore: collapse hidden groups section persist

* chore: no status group at beginning

* fix: hiding groups when grouping with other types

* chore: disable rename groups that arent supported

* chore: update appflowy board ref

* chore: better naming

* test: fix tests

---------

Co-authored-by: Mathias Mogensen <mathias@appflowy.io>
This commit is contained in:
Richard Shiue 2023-11-13 16:14:31 +08:00 committed by GitHub
parent 7867f0366e
commit a63a7ea611
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
35 changed files with 1200 additions and 749 deletions

View File

@ -1,4 +1,5 @@
import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/plugins/database_view/board/presentation/widgets/board_column_header.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
import 'package:appflowy_board/appflowy_board.dart'; import 'package:appflowy_board/appflowy_board.dart';
import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flowy_infra_ui/style_widget/text.dart';
@ -32,10 +33,12 @@ void main() {
await tester.tap( await tester.tap(
find find
.descendant( .descendant(
of: find.byType(AppFlowyGroupHeader), of: find.byType(BoardColumnHeader),
matching: find.byType(FlowySvg), matching: find.byWidgetPredicate(
(widget) => widget is FlowySvg && widget.svg == FlowySvgs.add_s,
),
) )
.first, .at(1),
); );
await tester.pumpAndSettle(); await tester.pumpAndSettle();
@ -77,7 +80,7 @@ void main() {
of: find.byType(AppFlowyGroupFooter), of: find.byType(AppFlowyGroupFooter),
matching: find.byType(FlowySvg), matching: find.byType(FlowySvg),
) )
.first, .at(1),
); );
await tester.pumpAndSettle(); await tester.pumpAndSettle();

View File

@ -0,0 +1,98 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/plugins/database_view/board/presentation/widgets/board_column_header.dart';
import 'package:appflowy/plugins/database_view/board/presentation/widgets/board_hidden_groups.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import '../util/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('board hide groups test', () {
testWidgets('expand/collapse hidden groups', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
await tester.createNewPageWithName(layout: ViewLayoutPB.Board);
final collapseFinder = find.byFlowySvg(FlowySvgs.pull_left_outlined_s);
final expandFinder = find.byFlowySvg(FlowySvgs.hamburger_s_s);
// Is expanded by default
expect(collapseFinder, findsOneWidget);
expect(expandFinder, findsNothing);
// Collapse hidden groups
await tester.tap(collapseFinder);
await tester.pumpAndSettle();
// Is collapsed
expect(collapseFinder, findsNothing);
expect(expandFinder, findsOneWidget);
// Expand hidden groups
await tester.tap(expandFinder);
await tester.pumpAndSettle();
// Is expanded
expect(collapseFinder, findsOneWidget);
expect(expandFinder, findsNothing);
});
testWidgets('hide first group, and show it again', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
await tester.createNewPageWithName(layout: ViewLayoutPB.Board);
// Tap the options of the first group
final optionsFinder = find
.descendant(
of: find.byType(BoardColumnHeader),
matching: find.byFlowySvg(FlowySvgs.details_horizontal_s),
)
.first;
await tester.tap(optionsFinder);
await tester.pumpAndSettle();
// Tap the hide option
await tester.tap(find.byFlowySvg(FlowySvgs.hide_s));
await tester.pumpAndSettle();
int shownGroups =
tester.widgetList(find.byType(BoardColumnHeader)).length;
// We still show Doing, Done, No Status
expect(shownGroups, 3);
final hiddenCardFinder = find.byType(HiddenGroupCard);
await tester.hoverOnWidget(hiddenCardFinder);
await tester.tap(find.byFlowySvg(FlowySvgs.show_m));
await tester.pumpAndSettle();
shownGroups = tester.widgetList(find.byType(BoardColumnHeader)).length;
expect(shownGroups, 4);
});
});
}
extension FlowySvgFinder on CommonFinders {
Finder byFlowySvg(FlowySvgData svg) => _FlowySvgFinder(svg);
}
class _FlowySvgFinder extends MatchFinder {
_FlowySvgFinder(this.svg);
final FlowySvgData svg;
@override
String get description => 'flowy_svg "$svg"';
@override
bool matches(Element candidate) {
final Widget widget = candidate.widget;
return widget is FlowySvg && widget.svg == svg;
}
}

View File

@ -6,6 +6,7 @@ import 'package:appflowy/plugins/database_view/application/field/field_info.dart
import 'package:appflowy/plugins/database_view/application/group/group_service.dart'; import 'package:appflowy/plugins/database_view/application/group/group_service.dart';
import 'package:appflowy/plugins/database_view/application/row/row_service.dart'; import 'package:appflowy/plugins/database_view/application/row/row_service.dart';
import 'package:appflowy_board/appflowy_board.dart'; import 'package:appflowy_board/appflowy_board.dart';
import 'package:collection/collection.dart';
import 'package:dartz/dartz.dart'; import 'package:dartz/dartz.dart';
import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
@ -14,6 +15,7 @@ import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:protobuf/protobuf.dart' hide FieldInfo;
import '../../application/field/field_controller.dart'; import '../../application/field/field_controller.dart';
import '../../application/row/row_cache.dart'; import '../../application/row/row_cache.dart';
@ -23,12 +25,13 @@ import 'group_controller.dart';
part 'board_bloc.freezed.dart'; part 'board_bloc.freezed.dart';
class BoardBloc extends Bloc<BoardEvent, BoardState> { class BoardBloc extends Bloc<BoardEvent, BoardState> {
late final GroupBackendService groupBackendSvc;
final DatabaseController databaseController; final DatabaseController databaseController;
late final AppFlowyBoardController boardController;
final LinkedHashMap<String, GroupController> groupControllers = final LinkedHashMap<String, GroupController> groupControllers =
LinkedHashMap(); LinkedHashMap();
GroupPB? ungroupedGroup; final List<GroupPB> groupList = [];
late final GroupBackendService groupBackendSvc;
late final AppFlowyBoardController boardController;
FieldController get fieldController => databaseController.fieldController; FieldController get fieldController => databaseController.fieldController;
String get viewId => databaseController.viewId; String get viewId => databaseController.viewId;
@ -82,10 +85,8 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
groupId: groupId, groupId: groupId,
startRowId: startRowId, startRowId: startRowId,
); );
result.fold(
(_) {}, result.fold((_) {}, (err) => Log.error(err));
(err) => Log.error(err),
);
}, },
createHeaderRow: (String groupId) async { createHeaderRow: (String groupId) async {
final result = await databaseController.createRow( final result = await databaseController.createRow(
@ -93,10 +94,7 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
fromBeginning: true, fromBeginning: true,
); );
result.fold( result.fold((_) {}, (err) => Log.error(err));
(_) {},
(err) => Log.error(err),
);
}, },
createGroup: (name) async { createGroup: (name) async {
final result = await groupBackendSvc.createGroup(name: name); final result = await groupBackendSvc.createGroup(name: name);
@ -115,6 +113,48 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
); );
_groupItemStartEditing(group, row, true); _groupItemStartEditing(group, row, true);
}, },
didReceiveGridUpdate: (DatabasePB grid) {
emit(state.copyWith(grid: Some(grid)));
},
didReceiveError: (FlowyError error) {
emit(state.copyWith(noneOrError: some(error)));
},
didReceiveGroups: (List<GroupPB> groups) {
final hiddenGroups = _filterHiddenGroups(hideUngrouped, groups);
emit(
state.copyWith(
hiddenGroups: hiddenGroups,
groupIds: groups.map((group) => group.groupId).toList(),
),
);
},
didUpdateLayoutSettings: (layoutSettings) {
final hiddenGroups = _filterHiddenGroups(hideUngrouped, groupList);
emit(
state.copyWith(
layoutSettings: layoutSettings,
hiddenGroups: hiddenGroups,
),
);
},
toggleGroupVisibility: (GroupPB group, bool isVisible) async {
await _toggleGroupVisibility(group, isVisible);
},
toggleHiddenSectionVisibility: (isVisible) async {
final newLayoutSettings = state.layoutSettings!;
newLayoutSettings.freeze();
final newLayoutSetting = newLayoutSettings.rebuild(
(message) => message.collapseHiddenGroups = isVisible,
);
await databaseController.updateLayoutSetting(
boardLayoutSetting: newLayoutSetting,
);
},
reorderGroup: (fromGroupId, toGroupId) async {
_reorderGroup(fromGroupId, toGroupId, emit);
},
startEditingRow: (group, row) { startEditingRow: (group, row) {
emit( emit(
state.copyWith( state.copyWith(
@ -140,22 +180,6 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
emit(state.copyWith(isEditingRow: false, editingRow: null)); emit(state.copyWith(isEditingRow: false, editingRow: null));
} }
}, },
didReceiveGridUpdate: (DatabasePB grid) {
emit(state.copyWith(grid: Some(grid)));
},
didReceiveError: (FlowyError error) {
emit(state.copyWith(noneOrError: some(error)));
},
didReceiveGroups: (List<GroupPB> groups) {
emit(
state.copyWith(
groupIds: groups.map((group) => group.groupId).toList(),
),
);
},
didUpdateLayoutSettings: (layoutSettings) {
emit(state.copyWith(layoutSettings: layoutSettings));
},
startEditingHeader: (String groupId) { startEditingHeader: (String groupId) {
emit( emit(
state.copyWith(isEditingHeader: true, editingHeaderId: groupId), state.copyWith(isEditingHeader: true, editingHeaderId: groupId),
@ -167,7 +191,6 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
groupId: groupId, groupId: groupId,
name: groupName, name: groupName,
); );
emit(state.copyWith(isEditingHeader: false)); emit(state.copyWith(isEditingHeader: false));
}, },
); );
@ -178,13 +201,50 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
void _groupItemStartEditing(GroupPB group, RowMetaPB row, bool isEdit) { void _groupItemStartEditing(GroupPB group, RowMetaPB row, bool isEdit) {
final fieldInfo = fieldController.getField(group.fieldId); final fieldInfo = fieldController.getField(group.fieldId);
if (fieldInfo == null) { if (fieldInfo == null) {
Log.warn("fieldInfo should not be null"); return Log.warn("fieldInfo should not be null");
return;
} }
boardController.enableGroupDragging(!isEdit); boardController.enableGroupDragging(!isEdit);
} }
Future<void> _toggleGroupVisibility(GroupPB group, bool isVisible) async {
if (group.isDefault) {
final newLayoutSettings = state.layoutSettings!;
newLayoutSettings.freeze();
final newLayoutSetting = newLayoutSettings.rebuild(
(message) => message.hideUngroupedColumn = !isVisible,
);
return databaseController.updateLayoutSetting(
boardLayoutSetting: newLayoutSetting,
);
}
await groupBackendSvc.updateGroup(
fieldId: groupControllers.values.first.group.fieldId,
groupId: group.groupId,
visible: isVisible,
);
}
Future<void> _reorderGroup(
String fromGroupId,
String toGroupId,
Emitter<BoardState> emit,
) async {
final fromIndex = groupList.indexWhere((g) => g.groupId == fromGroupId);
final toIndex = groupList.indexWhere((g) => g.groupId == toGroupId);
final group = groupList.removeAt(fromIndex);
groupList.insert(toIndex, group);
add(BoardEvent.didReceiveGroups(groupList));
final result = await databaseController.moveGroup(
fromGroupId: fromGroupId,
toGroupId: toGroupId,
);
result.fold((l) => {}, (err) => Log.error(err));
}
@override @override
Future<void> close() async { Future<void> close() async {
for (final controller in groupControllers.values) { for (final controller in groupControllers.values) {
@ -193,40 +253,45 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
return super.close(); return super.close();
} }
bool get hideUngrouped =>
databaseController.databaseLayoutSetting?.board.hideUngroupedColumn ??
false;
FieldType? get groupingFieldType {
final fieldInfo = databaseController.fieldController.fieldInfos
.firstWhereOrNull((field) => field.isGroupField);
return fieldInfo?.fieldType;
}
void initializeGroups(List<GroupPB> groups) { void initializeGroups(List<GroupPB> groups) {
for (final controller in groupControllers.values) { for (final controller in groupControllers.values) {
controller.dispose(); controller.dispose();
} }
groupControllers.clear(); groupControllers.clear();
boardController.clear(); boardController.clear();
groupList.clear();
final ungroupedGroupIndex = groupList.addAll(groups);
groups.indexWhere((group) => group.groupId == group.fieldId);
if (ungroupedGroupIndex != -1) {
ungroupedGroup = groups[ungroupedGroupIndex];
final group = groups.removeAt(ungroupedGroupIndex);
if (!(state.layoutSettings?.hideUngroupedColumn ?? false)) {
groups.add(group);
}
}
boardController.addGroups( boardController.addGroups(
groups groups
.where((group) => fieldController.getField(group.fieldId) != null) .where(
.map((group) => initializeGroupData(group)) (group) =>
fieldController.getField(group.fieldId) != null &&
(group.isVisible || (group.isDefault && !hideUngrouped)),
)
.map((group) => _initializeGroupData(group))
.toList(), .toList(),
); );
for (final group in groups) { for (final group in groups) {
final controller = initializeGroupController(group); final controller = _initializeGroupController(group);
groupControllers[controller.group.groupId] = (controller); groupControllers[controller.group.groupId] = controller;
} }
} }
RowCache? getRowCache() { RowCache? getRowCache() => databaseController.rowCache;
return databaseController.rowCache;
}
void _startListening() { void _startListening() {
final onDatabaseChanged = DatabaseCallbacks( final onDatabaseChanged = DatabaseCallbacks(
@ -238,15 +303,22 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
); );
final onLayoutSettingsChanged = DatabaseLayoutSettingCallbacks( final onLayoutSettingsChanged = DatabaseLayoutSettingCallbacks(
onLayoutSettingsChanged: (layoutSettings) { onLayoutSettingsChanged: (layoutSettings) {
if (isClosed || !layoutSettings.hasBoard()) { if (isClosed) {
return; return;
} }
if (ungroupedGroup != null) { final index = groupList.indexWhere((element) => element.isDefault);
if (index != -1) {
if (layoutSettings.board.hideUngroupedColumn) { if (layoutSettings.board.hideUngroupedColumn) {
boardController.removeGroup(ungroupedGroup!.fieldId); boardController.removeGroup(groupList[index].fieldId);
} else { } else {
final newGroup = initializeGroupData(ungroupedGroup!); final newGroup = _initializeGroupData(groupList[index]);
boardController.addGroup(newGroup); final visibleGroups = [...groupList]
..retainWhere((g) => g.isVisible || g.isDefault);
final indexInVisibleGroups =
visibleGroups.indexWhere((g) => g.isDefault);
if (indexInVisibleGroups != -1) {
boardController.insertGroup(indexInVisibleGroups, newGroup);
}
} }
} }
add(BoardEvent.didUpdateLayoutSettings(layoutSettings.board)); add(BoardEvent.didUpdateLayoutSettings(layoutSettings.board));
@ -254,30 +326,72 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
); );
final onGroupChanged = GroupCallbacks( final onGroupChanged = GroupCallbacks(
onGroupByField: (groups) { onGroupByField: (groups) {
if (isClosed) return; if (isClosed) {
ungroupedGroup = null; return;
}
initializeGroups(groups); initializeGroups(groups);
add(BoardEvent.didReceiveGroups(groups)); add(BoardEvent.didReceiveGroups(groups));
}, },
onDeleteGroup: (groupIds) { onDeleteGroup: (groupIds) {
if (isClosed) return; if (isClosed) {
return;
}
boardController.removeGroups(groupIds); boardController.removeGroups(groupIds);
groupList.removeWhere((group) => groupIds.contains(group.groupId));
add(BoardEvent.didReceiveGroups(groupList));
}, },
onInsertGroup: (insertGroups) { onInsertGroup: (insertGroups) {
if (isClosed) return; if (isClosed) {
return;
}
final group = insertGroups.group; final group = insertGroups.group;
final newGroup = initializeGroupData(group); final newGroup = _initializeGroupData(group);
final controller = initializeGroupController(group); final controller = _initializeGroupController(group);
groupControllers[controller.group.groupId] = (controller); groupControllers[controller.group.groupId] = controller;
boardController.addGroup(newGroup); boardController.addGroup(newGroup);
groupList.insert(insertGroups.index, group);
add(BoardEvent.didReceiveGroups(groupList));
}, },
onUpdateGroup: (updatedGroups) { onUpdateGroup: (updatedGroups) {
if (isClosed) return; if (isClosed) {
return;
}
for (final group in updatedGroups) { for (final group in updatedGroups) {
// see if the column is already in the board
final index = groupList.indexWhere((g) => g.groupId == group.groupId);
if (index == -1) continue;
final columnController = final columnController =
boardController.getGroupController(group.groupId); boardController.getGroupController(group.groupId);
columnController?.updateGroupName(group.groupName); if (columnController != null) {
// remove the group or update its name
columnController.updateGroupName(group.groupName);
if (!group.isVisible) {
boardController.removeGroup(group.groupId);
} }
} else {
final newGroup = _initializeGroupData(group);
final visibleGroups = [...groupList]..retainWhere(
(g) =>
g.isVisible ||
g.isDefault && !hideUngrouped ||
g.groupId == group.groupId,
);
final indexInVisibleGroups =
visibleGroups.indexWhere((g) => g.groupId == group.groupId);
if (indexInVisibleGroups != -1) {
boardController.insertGroup(indexInVisibleGroups, newGroup);
}
}
groupList.removeAt(index);
groupList.insert(index, group);
}
add(BoardEvent.didReceiveGroups(groupList));
}, },
); );
@ -315,24 +429,35 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
); );
} }
GroupController initializeGroupController(GroupPB group) { GroupController _initializeGroupController(GroupPB group) {
final delegate = GroupControllerDelegateImpl( final delegate = GroupControllerDelegateImpl(
controller: boardController, controller: boardController,
fieldController: fieldController, fieldController: fieldController,
onNewColumnItem: (groupId, row, index) { onNewColumnItem: (groupId, row, index) =>
add(BoardEvent.didCreateRow(group, row, index)); add(BoardEvent.didCreateRow(group, row, index)),
},
); );
final controller = GroupController( final controller = GroupController(
viewId: state.viewId, viewId: state.viewId,
group: group, group: group,
delegate: delegate, delegate: delegate,
onGroupChanged: (newGroup) {
if (isClosed) return;
final index =
groupList.indexWhere((g) => g.groupId == newGroup.groupId);
if (index != -1) {
groupList.removeAt(index);
groupList.insert(index, newGroup);
add(BoardEvent.didReceiveGroups(groupList));
}
},
); );
controller.startListening();
return controller; return controller..startListening();
} }
AppFlowyGroupData initializeGroupData(GroupPB group) { AppFlowyGroupData _initializeGroupData(GroupPB group) {
return AppFlowyGroupData( return AppFlowyGroupData(
id: group.groupId, id: group.groupId,
name: group.groupName, name: group.groupName,
@ -365,6 +490,14 @@ class BoardEvent with _$BoardEvent {
RowMetaPB row, RowMetaPB row,
) = _StartEditRow; ) = _StartEditRow;
const factory BoardEvent.endEditingRow(RowId rowId) = _EndEditRow; const factory BoardEvent.endEditingRow(RowId rowId) = _EndEditRow;
const factory BoardEvent.toggleGroupVisibility(
GroupPB group,
bool isVisible,
) = _ToggleGroupVisibility;
const factory BoardEvent.toggleHiddenSectionVisibility(bool isVisible) =
_ToggleHiddenSectionVisibility;
const factory BoardEvent.reorderGroup(String fromGroupId, String toGroupId) =
_ReorderGroup;
const factory BoardEvent.didReceiveError(FlowyError error) = _DidReceiveError; const factory BoardEvent.didReceiveError(FlowyError error) = _DidReceiveError;
const factory BoardEvent.didReceiveGridUpdate( const factory BoardEvent.didReceiveGridUpdate(
DatabasePB grid, DatabasePB grid,
@ -383,12 +516,13 @@ class BoardState with _$BoardState {
required Option<DatabasePB> grid, required Option<DatabasePB> grid,
required List<String> groupIds, required List<String> groupIds,
required bool isEditingHeader, required bool isEditingHeader,
String? editingHeaderId,
required bool isEditingRow, required bool isEditingRow,
BoardEditingRow? editingRow,
required LoadingState loadingState, required LoadingState loadingState,
required Option<FlowyError> noneOrError, required Option<FlowyError> noneOrError,
required BoardLayoutSettingPB? layoutSettings, required BoardLayoutSettingPB? layoutSettings,
String? editingHeaderId,
BoardEditingRow? editingRow,
required List<GroupPB> hiddenGroups,
}) = _BoardState; }) = _BoardState;
factory BoardState.initial(String viewId) => BoardState( factory BoardState.initial(String viewId) => BoardState(
@ -400,6 +534,13 @@ class BoardState with _$BoardState {
noneOrError: none(), noneOrError: none(),
loadingState: const LoadingState.loading(), loadingState: const LoadingState.loading(),
layoutSettings: null, layoutSettings: null,
hiddenGroups: [],
);
}
List<GroupPB> _filterHiddenGroups(bool hideUngrouped, List<GroupPB> groups) {
return [...groups]..retainWhere(
(group) => !group.isVisible || group.isDefault && hideUngrouped,
); );
} }
@ -430,12 +571,16 @@ class GroupControllerDelegateImpl extends GroupControllerDelegate {
required this.onNewColumnItem, required this.onNewColumnItem,
}); });
@override
bool hasGroup(String groupId) {
return controller.groupIds.contains(groupId);
}
@override @override
void insertRow(GroupPB group, RowMetaPB row, int? index) { void insertRow(GroupPB group, RowMetaPB row, int? index) {
final fieldInfo = fieldController.getField(group.fieldId); final fieldInfo = fieldController.getField(group.fieldId);
if (fieldInfo == null) { if (fieldInfo == null) {
Log.warn("fieldInfo should not be null"); return Log.warn("fieldInfo should not be null");
return;
} }
if (index != null) { if (index != null) {
@ -454,17 +599,16 @@ class GroupControllerDelegateImpl extends GroupControllerDelegate {
} }
@override @override
void removeRow(GroupPB group, RowId rowId) { void removeRow(GroupPB group, RowId rowId) =>
controller.removeGroupItem(group.groupId, rowId.toString()); controller.removeGroupItem(group.groupId, rowId.toString());
}
@override @override
void updateRow(GroupPB group, RowMetaPB row) { void updateRow(GroupPB group, RowMetaPB row) {
final fieldInfo = fieldController.getField(group.fieldId); final fieldInfo = fieldController.getField(group.fieldId);
if (fieldInfo == null) { if (fieldInfo == null) {
Log.warn("fieldInfo should not be null"); return Log.warn("fieldInfo should not be null");
return;
} }
controller.updateGroupItem( controller.updateGroupItem(
group.groupId, group.groupId,
GroupItem( GroupItem(
@ -478,20 +622,17 @@ class GroupControllerDelegateImpl extends GroupControllerDelegate {
void addNewRow(GroupPB group, RowMetaPB row, int? index) { void addNewRow(GroupPB group, RowMetaPB row, int? index) {
final fieldInfo = fieldController.getField(group.fieldId); final fieldInfo = fieldController.getField(group.fieldId);
if (fieldInfo == null) { if (fieldInfo == null) {
Log.warn("fieldInfo should not be null"); return Log.warn("fieldInfo should not be null");
return;
} }
final item = GroupItem(
row: row, final item = GroupItem(row: row, fieldInfo: fieldInfo, draggable: false);
fieldInfo: fieldInfo,
draggable: false,
);
if (index != null) { if (index != null) {
controller.insertGroupItem(group.groupId, index, item); controller.insertGroupItem(group.groupId, index, item);
} else { } else {
controller.addGroupItem(group.groupId, item); controller.addGroupItem(group.groupId, item);
} }
onNewColumnItem(group.groupId, row, index); onNewColumnItem(group.groupId, row, index);
} }
} }
@ -509,27 +650,26 @@ class BoardEditingRow {
} }
class GroupData { class GroupData {
final GroupPB group;
final FieldInfo fieldInfo;
GroupData({ GroupData({
required this.group, required this.group,
required this.fieldInfo, required this.fieldInfo,
}); });
CheckboxGroup? asCheckboxGroup() { final GroupPB group;
if (fieldType != FieldType.Checkbox) return null; final FieldInfo fieldInfo;
return CheckboxGroup(group);
} CheckboxGroup? asCheckboxGroup() =>
fieldType == FieldType.Checkbox ? CheckboxGroup(group) : null;
FieldType get fieldType => fieldInfo.fieldType; FieldType get fieldType => fieldInfo.fieldType;
} }
class CheckboxGroup { class CheckboxGroup {
const CheckboxGroup(this.group);
final GroupPB group; final GroupPB group;
CheckboxGroup(this.group); // Hardcode value: "Yes" that equal to the value defined in Rust
// pub const CHECK: &str = "Yes";
// Hardcode value: "Yes" that equal to the value defined in Rust
// pub const CHECK: &str = "Yes";
bool get isCheck => group.groupId == "Yes"; bool get isCheck => group.groupId == "Yes";
} }

View File

@ -1,12 +0,0 @@
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
class BoardGroupService {
final String viewId;
FieldPB? groupField;
BoardGroupService(this.viewId);
void setGroupField(FieldPB field) {
groupField = field;
}
}

View File

@ -7,10 +7,12 @@ import 'dart:typed_data';
import 'package:appflowy/core/notification/grid_notification.dart'; import 'package:appflowy/core/notification/grid_notification.dart';
import 'package:flowy_infra/notifier.dart'; import 'package:flowy_infra/notifier.dart';
import 'package:dartz/dartz.dart'; import 'package:dartz/dartz.dart';
import 'package:protobuf/protobuf.dart';
typedef OnGroupError = void Function(FlowyError); typedef OnGroupError = void Function(FlowyError);
abstract class GroupControllerDelegate { abstract class GroupControllerDelegate {
bool hasGroup(String groupId);
void removeRow(GroupPB group, RowId rowId); void removeRow(GroupPB group, RowId rowId);
void insertRow(GroupPB group, RowMetaPB row, int? index); void insertRow(GroupPB group, RowMetaPB row, int? index);
void updateRow(GroupPB group, RowMetaPB row); void updateRow(GroupPB group, RowMetaPB row);
@ -18,14 +20,16 @@ abstract class GroupControllerDelegate {
} }
class GroupController { class GroupController {
final GroupPB group; GroupPB group;
final SingleGroupListener _listener; final SingleGroupListener _listener;
final GroupControllerDelegate delegate; final GroupControllerDelegate delegate;
final void Function(GroupPB group) onGroupChanged;
GroupController({ GroupController({
required String viewId, required String viewId,
required this.group, required this.group,
required this.delegate, required this.delegate,
required this.onGroupChanged,
}) : _listener = SingleGroupListener(group); }) : _listener = SingleGroupListener(group);
RowMetaPB? rowAtIndex(int index) { RowMetaPB? rowAtIndex(int index) {
@ -46,37 +50,52 @@ class GroupController {
onGroupChanged: (result) { onGroupChanged: (result) {
result.fold( result.fold(
(GroupRowsNotificationPB changeset) { (GroupRowsNotificationPB changeset) {
final newItems = [...group.rows];
final isGroupExist = delegate.hasGroup(group.groupId);
for (final deletedRow in changeset.deletedRows) { for (final deletedRow in changeset.deletedRows) {
group.rows.removeWhere((rowPB) => rowPB.id == deletedRow); newItems.removeWhere((rowPB) => rowPB.id == deletedRow);
if (isGroupExist) {
delegate.removeRow(group, deletedRow); delegate.removeRow(group, deletedRow);
} }
}
for (final insertedRow in changeset.insertedRows) { for (final insertedRow in changeset.insertedRows) {
final index = insertedRow.hasIndex() ? insertedRow.index : null; final index = insertedRow.hasIndex() ? insertedRow.index : null;
if (insertedRow.hasIndex() && if (insertedRow.hasIndex() &&
group.rows.length > insertedRow.index) { newItems.length > insertedRow.index) {
group.rows.insert(insertedRow.index, insertedRow.rowMeta); newItems.insert(insertedRow.index, insertedRow.rowMeta);
} else { } else {
group.rows.add(insertedRow.rowMeta); newItems.add(insertedRow.rowMeta);
} }
if (isGroupExist) {
if (insertedRow.isNew) { if (insertedRow.isNew) {
delegate.addNewRow(group, insertedRow.rowMeta, index); delegate.addNewRow(group, insertedRow.rowMeta, index);
} else { } else {
delegate.insertRow(group, insertedRow.rowMeta, index); delegate.insertRow(group, insertedRow.rowMeta, index);
} }
} }
}
for (final updatedRow in changeset.updatedRows) { for (final updatedRow in changeset.updatedRows) {
final index = group.rows.indexWhere( final index = newItems.indexWhere(
(rowPB) => rowPB.id == updatedRow.id, (rowPB) => rowPB.id == updatedRow.id,
); );
if (index != -1) { if (index != -1) {
group.rows[index] = updatedRow; newItems[index] = updatedRow;
if (isGroupExist) {
delegate.updateRow(group, updatedRow); delegate.updateRow(group, updatedRow);
} }
} }
}
group.freeze();
group = group.rebuild((group) {
group.rows.clear();
group.rows.addAll(newItems);
});
onGroupChanged(group);
}, },
(err) => Log.error(err), (err) => Log.error(err),
); );

View File

@ -1,112 +0,0 @@
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:bloc/bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'group_controller.dart';
part 'ungrouped_items_bloc.freezed.dart';
class UngroupedItemsBloc
extends Bloc<UngroupedItemsEvent, UngroupedItemsState> {
UngroupedItemsListener? listener;
UngroupedItemsBloc({required GroupPB group})
: super(UngroupedItemsState(ungroupedItems: group.rows)) {
on<UngroupedItemsEvent>(
(event, emit) {
event.when(
initial: () {
listener = UngroupedItemsListener(
initialGroup: group,
onGroupChanged: (ungroupedItems) {
if (isClosed) return;
add(
UngroupedItemsEvent.updateGroup(
ungroupedItems: ungroupedItems,
),
);
},
)..startListening();
},
updateGroup: (newItems) =>
emit(UngroupedItemsState(ungroupedItems: newItems)),
);
},
);
}
}
@freezed
class UngroupedItemsEvent with _$UngroupedItemsEvent {
const factory UngroupedItemsEvent.initial() = _Initial;
const factory UngroupedItemsEvent.updateGroup({
required List<RowMetaPB> ungroupedItems,
}) = _UpdateGroup;
}
@freezed
class UngroupedItemsState with _$UngroupedItemsState {
const factory UngroupedItemsState({
required List<RowMetaPB> ungroupedItems,
}) = _UngroupedItemsState;
}
class UngroupedItemsListener {
List<RowMetaPB> _ungroupedItems;
final SingleGroupListener _listener;
final void Function(List<RowMetaPB> items) onGroupChanged;
UngroupedItemsListener({
required GroupPB initialGroup,
required this.onGroupChanged,
}) : _ungroupedItems = List<RowMetaPB>.from(initialGroup.rows),
_listener = SingleGroupListener(initialGroup);
void startListening() {
_listener.start(
onGroupChanged: (result) {
result.fold(
(GroupRowsNotificationPB changeset) {
final newItems = List<RowMetaPB>.from(_ungroupedItems);
for (final deletedRow in changeset.deletedRows) {
newItems.removeWhere((rowPB) => rowPB.id == deletedRow);
}
for (final insertedRow in changeset.insertedRows) {
final index = newItems.indexWhere(
(rowPB) => rowPB.id == insertedRow.rowMeta.id,
);
if (index != -1) {
continue;
}
if (insertedRow.hasIndex() &&
newItems.length > insertedRow.index) {
newItems.insert(insertedRow.index, insertedRow.rowMeta);
} else {
newItems.add(insertedRow.rowMeta);
}
}
for (final updatedRow in changeset.updatedRows) {
final index = newItems.indexWhere(
(rowPB) => rowPB.id == updatedRow.id,
);
if (index != -1) {
newItems[index] = updatedRow;
}
}
onGroupChanged.call(newItems);
_ungroupedItems = newItems;
},
(err) => Log.error(err),
);
},
);
}
Future<void> dispose() async {
_listener.stop();
}
}

View File

@ -1,5 +1,3 @@
// ignore_for_file: unused_field
import 'dart:collection'; import 'dart:collection';
import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart';
@ -9,7 +7,7 @@ import 'package:appflowy/plugins/database_view/application/field/field_controlle
import 'package:appflowy/plugins/database_view/application/row/row_cache.dart'; import 'package:appflowy/plugins/database_view/application/row/row_cache.dart';
import 'package:appflowy/plugins/database_view/application/row/row_controller.dart'; import 'package:appflowy/plugins/database_view/application/row/row_controller.dart';
import 'package:appflowy/plugins/database_view/board/presentation/widgets/board_column_header.dart'; import 'package:appflowy/plugins/database_view/board/presentation/widgets/board_column_header.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart'; import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_type_extension.dart';
import 'package:appflowy/plugins/database_view/tar_bar/tab_bar_view.dart'; import 'package:appflowy/plugins/database_view/tar_bar/tab_bar_view.dart';
import 'package:appflowy/plugins/database_view/widgets/row/row_detail.dart'; import 'package:appflowy/plugins/database_view/widgets/row/row_detail.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
@ -30,7 +28,7 @@ import '../../widgets/row/cell_builder.dart';
import '../application/board_bloc.dart'; import '../application/board_bloc.dart';
import '../../widgets/card/card.dart'; import '../../widgets/card/card.dart';
import 'toolbar/board_setting_bar.dart'; import 'toolbar/board_setting_bar.dart';
import 'ungrouped_items_button.dart'; import 'widgets/board_hidden_groups.dart';
class BoardPageTabBarBuilderImpl implements DatabaseTabBarItemBuilder { class BoardPageTabBarBuilderImpl implements DatabaseTabBarItemBuilder {
@override @override
@ -39,46 +37,38 @@ class BoardPageTabBarBuilderImpl implements DatabaseTabBarItemBuilder {
ViewPB view, ViewPB view,
DatabaseController controller, DatabaseController controller,
bool shrinkWrap, bool shrinkWrap,
) { ) =>
return BoardPage( BoardPage(view: view, databaseController: controller);
key: _makeValueKey(controller),
view: view,
databaseController: controller,
);
}
@override @override
Widget settingBar(BuildContext context, DatabaseController controller) { Widget settingBar(BuildContext context, DatabaseController controller) =>
return BoardSettingBar( BoardSettingBar(
key: _makeValueKey(controller), key: _makeValueKey(controller),
databaseController: controller, databaseController: controller,
); );
}
@override @override
Widget settingBarExtension( Widget settingBarExtension(
BuildContext context, BuildContext context,
DatabaseController controller, DatabaseController controller,
) { ) =>
return SizedBox.fromSize(); const SizedBox.shrink();
}
ValueKey _makeValueKey(DatabaseController controller) { ValueKey _makeValueKey(DatabaseController controller) =>
return ValueKey(controller.viewId); ValueKey(controller.viewId);
}
} }
class BoardPage extends StatelessWidget { class BoardPage extends StatelessWidget {
final DatabaseController databaseController;
BoardPage({ BoardPage({
required this.view, required this.view,
required this.databaseController, required this.databaseController,
Key? key,
this.onEditStateChanged, this.onEditStateChanged,
}) : super(key: ValueKey(view.id)); }) : super(key: ValueKey(view.id));
final ViewPB view; final ViewPB view;
final DatabaseController databaseController;
/// Called when edit state changed /// Called when edit state changed
final VoidCallback? onEditStateChanged; final VoidCallback? onEditStateChanged;
@ -91,23 +81,18 @@ class BoardPage extends StatelessWidget {
)..add(const BoardEvent.initial()), )..add(const BoardEvent.initial()),
child: BlocBuilder<BoardBloc, BoardState>( child: BlocBuilder<BoardBloc, BoardState>(
buildWhen: (p, c) => p.loadingState != c.loadingState, buildWhen: (p, c) => p.loadingState != c.loadingState,
builder: (context, state) { builder: (context, state) => state.loadingState.map(
return state.loadingState.map( loading: (_) => const Center(
loading: (_) => child: CircularProgressIndicator.adaptive(),
const Center(child: CircularProgressIndicator.adaptive()),
finish: (result) {
return result.successOrFail.fold(
(_) => BoardContent(
onEditStateChanged: onEditStateChanged,
), ),
finish: (result) => result.successOrFail.fold(
(_) => BoardContent(onEditStateChanged: onEditStateChanged),
(err) => FlowyErrorPage.message( (err) => FlowyErrorPage.message(
err.toString(), err.toString(),
howToFix: LocaleKeys.errorDialog_howToFixFallback.tr(), howToFix: LocaleKeys.errorDialog_howToFixFallback.tr(),
), ),
); ),
}, ),
);
},
), ),
); );
} }
@ -126,12 +111,14 @@ class BoardContent extends StatefulWidget {
} }
class _BoardContentState extends State<BoardContent> { class _BoardContentState extends State<BoardContent> {
late AppFlowyBoardScrollController scrollManager;
late final ScrollController scrollController;
final renderHook = RowCardRenderHook<String>(); final renderHook = RowCardRenderHook<String>();
late final ScrollController scrollController;
late final AppFlowyBoardScrollController scrollManager;
final config = const AppFlowyBoardConfig( final config = const AppFlowyBoardConfig(
groupBackgroundColor: Color(0xffF7F8FC), groupBackgroundColor: Color(0xffF7F8FC),
headerPadding: EdgeInsets.symmetric(horizontal: 8),
cardPadding: EdgeInsets.symmetric(horizontal: 4, vertical: 3),
); );
@override @override
@ -162,22 +149,24 @@ class _BoardContentState extends State<BoardContent> {
}, },
child: BlocBuilder<BoardBloc, BoardState>( child: BlocBuilder<BoardBloc, BoardState>(
builder: (context, state) { builder: (context, state) {
final showCreateGroupButton =
context.read<BoardBloc>().groupingFieldType!.canCreateNewGroup;
return Padding( return Padding(
padding: GridSize.contentInsets, padding: const EdgeInsets.only(top: 8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
const VSpace(8.0),
if (state.layoutSettings?.hideUngroupedColumn ?? false)
_buildBoardHeader(context),
Expanded(
child: AppFlowyBoard( child: AppFlowyBoard(
boardScrollController: scrollManager, boardScrollController: scrollManager,
scrollController: scrollController, scrollController: scrollController,
controller: context.read<BoardBloc>().boardController, controller: context.read<BoardBloc>().boardController,
headerBuilder: (_, groupData) => groupConstraints: const BoxConstraints.tightFor(width: 300),
BlocProvider<BoardBloc>.value( config: const AppFlowyBoardConfig(
groupPadding: EdgeInsets.symmetric(horizontal: 4),
groupItemPadding: EdgeInsets.symmetric(horizontal: 4),
),
leading: HiddenGroupsColumn(margin: config.headerPadding),
trailing: showCreateGroupButton
? BoardTrailing(scrollController: scrollController)
: null,
headerBuilder: (_, groupData) => BlocProvider<BoardBloc>.value(
value: context.read<BoardBloc>(), value: context.read<BoardBloc>(),
child: BoardColumnHeader( child: BoardColumnHeader(
groupData: groupData, groupData: groupData,
@ -185,20 +174,11 @@ class _BoardContentState extends State<BoardContent> {
), ),
), ),
footerBuilder: _buildFooter, footerBuilder: _buildFooter,
trailing: BoardTrailing(scrollController: scrollController),
cardBuilder: (_, column, columnItem) => _buildCard( cardBuilder: (_, column, columnItem) => _buildCard(
context, context,
column, column,
columnItem, columnItem,
), ),
groupConstraints: const BoxConstraints.tightFor(width: 300),
config: AppFlowyBoardConfig(
groupBackgroundColor:
Theme.of(context).colorScheme.surfaceVariant,
),
),
)
],
), ),
); );
}, },
@ -206,19 +186,6 @@ class _BoardContentState extends State<BoardContent> {
); );
} }
Widget _buildBoardHeader(BuildContext context) {
return const Padding(
padding: EdgeInsets.only(bottom: 8.0),
child: SizedBox(
height: 24,
child: Align(
alignment: AlignmentDirectional.centerEnd,
child: UngroupedItemsButton(),
),
),
);
}
void _handleEditStateChanged(BoardState state, BuildContext context) { void _handleEditStateChanged(BoardState state, BuildContext context) {
if (state.isEditingRow && state.editingRow != null) { if (state.isEditingRow && state.editingRow != null) {
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
@ -231,25 +198,24 @@ class _BoardContentState extends State<BoardContent> {
Widget _buildFooter(BuildContext context, AppFlowyGroupData columnData) { Widget _buildFooter(BuildContext context, AppFlowyGroupData columnData) {
return AppFlowyGroupFooter( return AppFlowyGroupFooter(
height: 50,
margin: config.footerPadding,
icon: SizedBox( icon: SizedBox(
height: 20, height: 20,
width: 20, width: 20,
child: FlowySvg( child: FlowySvg(
FlowySvgs.add_s, FlowySvgs.add_s,
color: Theme.of(context).iconTheme.color, color: Theme.of(context).hintColor,
), ),
), ),
title: FlowyText.medium( title: FlowyText.medium(
LocaleKeys.board_column_createNewCard.tr(), LocaleKeys.board_column_createNewCard.tr(),
color: Theme.of(context).hintColor,
fontSize: 14, fontSize: 14,
), ),
height: 50, onAddButtonClick: () => context
margin: config.footerPadding, .read<BoardBloc>()
onAddButtonClick: () { .add(BoardEvent.createBottomRow(columnData.id)),
context.read<BoardBloc>().add(
BoardEvent.createBottomRow(columnData.id),
);
},
); );
} }
@ -307,15 +273,27 @@ class _BoardContentState extends State<BoardContent> {
} }
BoxDecoration _makeBoxDecoration(BuildContext context) { BoxDecoration _makeBoxDecoration(BuildContext context) {
final borderSide = BorderSide(
color: Theme.of(context).dividerColor,
width: 1.0,
);
final isLightMode = Theme.of(context).brightness == Brightness.light;
return BoxDecoration( return BoxDecoration(
color: Theme.of(context).colorScheme.surface, color: Theme.of(context).colorScheme.surface,
border: isLightMode ? Border.fromBorderSide(borderSide) : null,
borderRadius: const BorderRadius.all(Radius.circular(6)), borderRadius: const BorderRadius.all(Radius.circular(6)),
border: Border.fromBorderSide(
BorderSide(
color: Theme.of(context).dividerColor,
width: 1.4,
),
),
boxShadow: [
BoxShadow(
blurRadius: 4,
spreadRadius: 0,
color: const Color(0xFF1F2329).withOpacity(0.02),
),
BoxShadow(
blurRadius: 4,
spreadRadius: -2,
color: const Color(0xFF1F2329).withOpacity(0.02),
),
],
); );
} }
@ -343,40 +321,37 @@ class _BoardContentState extends State<BoardContent> {
FlowyOverlay.show( FlowyOverlay.show(
context: context, context: context,
builder: (BuildContext context) { builder: (_) => RowDetailPage(
return RowDetailPage(
cellBuilder: GridCellBuilder(cellCache: dataController.cellCache), cellBuilder: GridCellBuilder(cellCache: dataController.cellCache),
rowController: dataController, rowController: dataController,
); ),
},
); );
} }
} }
class BoardTrailing extends StatefulWidget { class BoardTrailing extends StatefulWidget {
const BoardTrailing({super.key, required this.scrollController});
final ScrollController scrollController; final ScrollController scrollController;
const BoardTrailing({required this.scrollController, super.key});
@override @override
State<BoardTrailing> createState() => _BoardTrailingState(); State<BoardTrailing> createState() => _BoardTrailingState();
} }
class _BoardTrailingState extends State<BoardTrailing> { class _BoardTrailingState extends State<BoardTrailing> {
bool isEditing = false; final TextEditingController _textController = TextEditingController();
late final TextEditingController _textController;
late final FocusNode _focusNode; late final FocusNode _focusNode;
bool isEditing = false;
void _cancelAddNewGroup() { void _cancelAddNewGroup() {
_textController.clear(); _textController.clear();
setState(() { setState(() => isEditing = false);
isEditing = false;
});
} }
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_textController = TextEditingController();
_focusNode = FocusNode( _focusNode = FocusNode(
onKeyEvent: (node, event) { onKeyEvent: (node, event) {
if (_focusNode.hasFocus && if (_focusNode.hasFocus &&
@ -406,7 +381,7 @@ class _BoardTrailingState extends State<BoardTrailing> {
}); });
return Padding( return Padding(
padding: const EdgeInsets.only(left: 8.0), padding: const EdgeInsets.only(left: 8.0, top: 12),
child: Align( child: Align(
alignment: AlignmentDirectional.topStart, alignment: AlignmentDirectional.topStart,
child: AnimatedSwitcher( child: AnimatedSwitcher(
@ -448,9 +423,7 @@ class _BoardTrailingState extends State<BoardTrailing> {
width: 26, width: 26,
icon: const FlowySvg(FlowySvgs.add_s), icon: const FlowySvg(FlowySvgs.add_s),
iconColorOnHover: Theme.of(context).colorScheme.onSurface, iconColorOnHover: Theme.of(context).colorScheme.onSurface,
onPressed: () => setState(() { onPressed: () => setState(() => isEditing = true),
isEditing = true;
}),
), ),
), ),
), ),

View File

@ -1,237 +0,0 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database_view/application/cell/cell_service.dart';
import 'package:appflowy/plugins/database_view/application/field/field_info.dart';
import 'package:appflowy/plugins/database_view/application/row/row_cache.dart';
import 'package:appflowy/plugins/database_view/application/row/row_controller.dart';
import 'package:appflowy/plugins/database_view/board/application/board_bloc.dart';
import 'package:appflowy/plugins/database_view/board/application/ungrouped_items_bloc.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart';
import 'package:appflowy/plugins/database_view/widgets/card/card_cell_builder.dart';
import 'package:appflowy/plugins/database_view/widgets/card/cells/card_cell.dart';
import 'package:appflowy/plugins/database_view/widgets/row/cell_builder.dart';
import 'package:appflowy/plugins/database_view/widgets/row/cells/text_cell/text_cell_bloc.dart';
import 'package:appflowy/plugins/database_view/widgets/row/row_detail.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/row_entities.pb.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:collection/collection.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/size.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class UngroupedItemsButton extends StatefulWidget {
const UngroupedItemsButton({super.key});
@override
State<UngroupedItemsButton> createState() => _UnscheduledEventsButtonState();
}
class _UnscheduledEventsButtonState extends State<UngroupedItemsButton> {
late final PopoverController _popoverController;
@override
void initState() {
super.initState();
_popoverController = PopoverController();
}
@override
Widget build(BuildContext context) {
return BlocBuilder<BoardBloc, BoardState>(
builder: (context, boardState) {
final ungroupedGroup = context.watch<BoardBloc>().ungroupedGroup;
final databaseController = context.read<BoardBloc>().databaseController;
final primaryField = databaseController.fieldController.fieldInfos
.firstWhereOrNull((element) => element.isPrimary)!;
if (ungroupedGroup == null) {
return const SizedBox.shrink();
}
return BlocProvider<UngroupedItemsBloc>(
create: (_) => UngroupedItemsBloc(group: ungroupedGroup)
..add(const UngroupedItemsEvent.initial()),
child: BlocBuilder<UngroupedItemsBloc, UngroupedItemsState>(
builder: (context, state) {
return AppFlowyPopover(
direction: PopoverDirection.bottomWithCenterAligned,
triggerActions: PopoverTriggerFlags.none,
controller: _popoverController,
offset: const Offset(0, 8),
constraints:
const BoxConstraints(maxWidth: 282, maxHeight: 600),
child: FlowyTooltip(
message: LocaleKeys.board_ungroupedButtonTooltip.tr(),
child: OutlinedButton(
style: OutlinedButton.styleFrom(
shape: RoundedRectangleBorder(
side: BorderSide(
color: Theme.of(context).dividerColor,
width: 1,
),
borderRadius: Corners.s6Border,
),
side: BorderSide(
color: Theme.of(context).dividerColor,
width: 1,
),
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 4,
),
visualDensity: VisualDensity.compact,
),
onPressed: () {
if (state.ungroupedItems.isNotEmpty) {
_popoverController.show();
}
},
child: FlowyText.regular(
"${LocaleKeys.board_ungroupedButtonText.tr()} (${state.ungroupedItems.length})",
fontSize: 10,
),
),
),
popupBuilder: (context) {
return UngroupedItemList(
viewId: databaseController.viewId,
primaryField: primaryField,
rowCache: databaseController.rowCache,
ungroupedItems: state.ungroupedItems,
);
},
);
},
),
);
},
);
}
}
class UngroupedItemList extends StatelessWidget {
final String viewId;
final FieldInfo primaryField;
final RowCache rowCache;
final List<RowMetaPB> ungroupedItems;
const UngroupedItemList({
required this.viewId,
required this.primaryField,
required this.ungroupedItems,
required this.rowCache,
super.key,
});
@override
Widget build(BuildContext context) {
final cells = <Widget>[
Padding(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4),
child: FlowyText.medium(
LocaleKeys.board_ungroupedItemsTitle.tr(),
fontSize: 10,
color: Theme.of(context).hintColor,
overflow: TextOverflow.ellipsis,
),
),
...ungroupedItems.map(
(item) {
final rowController = RowController(
rowMeta: item,
viewId: viewId,
rowCache: rowCache,
);
final renderHook = RowCardRenderHook<String>();
renderHook.addTextCellHook((cellData, _, __) {
return BlocBuilder<TextCellBloc, TextCellState>(
builder: (context, state) {
final text = cellData.isEmpty
? LocaleKeys.grid_row_titlePlaceholder.tr()
: cellData;
if (text.isEmpty) {
return const SizedBox.shrink();
}
return Align(
alignment: Alignment.centerLeft,
child: FlowyText.medium(
text,
textAlign: TextAlign.left,
fontSize: 11,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
);
},
);
});
return UngroupedItem(
cellContext: rowCache.loadCells(item)[primaryField.id]!,
primaryField: primaryField,
rowController: rowController,
cellBuilder: CardCellBuilder<String>(rowController.cellCache),
renderHook: renderHook,
onPressed: () {
FlowyOverlay.show(
context: context,
builder: (BuildContext context) {
return RowDetailPage(
cellBuilder:
GridCellBuilder(cellCache: rowController.cellCache),
rowController: rowController,
);
},
);
PopoverContainer.of(context).close();
},
);
},
)
];
return ListView.separated(
itemBuilder: (context, index) => cells[index],
itemCount: cells.length,
separatorBuilder: (context, index) =>
VSpace(GridSize.typeOptionSeparatorHeight),
shrinkWrap: true,
);
}
}
class UngroupedItem extends StatelessWidget {
final DatabaseCellContext cellContext;
final FieldInfo primaryField;
final RowController rowController;
final CardCellBuilder cellBuilder;
final RowCardRenderHook<String> renderHook;
final VoidCallback onPressed;
const UngroupedItem({
super.key,
required this.cellContext,
required this.onPressed,
required this.cellBuilder,
required this.rowController,
required this.primaryField,
required this.renderHook,
});
@override
Widget build(BuildContext context) {
return SizedBox(
height: 26,
child: FlowyButton(
margin: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0),
text: cellBuilder.buildCell(
cellContext: cellContext,
renderHook: renderHook,
hasNotes: false,
),
onTap: onPressed,
),
);
}
}

View File

@ -1,14 +1,15 @@
import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database_view/board/application/board_bloc.dart'; import 'package:appflowy/plugins/database_view/board/application/board_bloc.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_type_extension.dart'; import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_type_extension.dart';
import 'package:appflowy/plugins/database_view/widgets/card/define.dart'; import 'package:appflowy/plugins/database_view/widgets/card/define.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/group.pb.dart';
import 'package:appflowy_board/appflowy_board.dart'; import 'package:appflowy_board/appflowy_board.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/style_widget/hover.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
@ -18,11 +19,11 @@ class BoardColumnHeader extends StatefulWidget {
const BoardColumnHeader({ const BoardColumnHeader({
super.key, super.key,
required this.groupData, required this.groupData,
this.margin, required this.margin,
}); });
final AppFlowyGroupData groupData; final AppFlowyGroupData groupData;
final EdgeInsets? margin; final EdgeInsets margin;
@override @override
State<BoardColumnHeader> createState() => _BoardColumnHeaderState(); State<BoardColumnHeader> createState() => _BoardColumnHeaderState();
@ -74,7 +75,7 @@ class _BoardColumnHeaderState extends State<BoardColumnHeader> {
child: FlowyText.medium( child: FlowyText.medium(
widget.groupData.headerData.groupName, widget.groupData.headerData.groupName,
fontSize: 14, fontSize: 14,
overflow: TextOverflow.clip, overflow: TextOverflow.ellipsis,
), ),
); );
@ -84,22 +85,16 @@ class _BoardColumnHeaderState extends State<BoardColumnHeader> {
fit: FlexFit.tight, fit: FlexFit.tight,
child: FlowyTooltip( child: FlowyTooltip(
message: LocaleKeys.board_column_renameGroupTooltip.tr(), message: LocaleKeys.board_column_renameGroupTooltip.tr(),
child: FlowyHover( child: MouseRegion(
style: HoverStyle( cursor: SystemMouseCursors.click,
hoverColor: Colors.transparent,
foregroundColorOnHover:
AFThemeExtension.of(context).textColor,
),
child: GestureDetector( child: GestureDetector(
onTap: () => context.read<BoardBloc>().add( onTap: () => context
BoardEvent.startEditingHeader( .read<BoardBloc>()
widget.groupData.id, .add(BoardEvent.startEditingHeader(widget.groupData.id)),
),
),
child: FlowyText.medium( child: FlowyText.medium(
widget.groupData.headerData.groupName, widget.groupData.headerData.groupName,
fontSize: 14, fontSize: 14,
overflow: TextOverflow.clip, overflow: TextOverflow.ellipsis,
), ),
), ),
), ),
@ -112,22 +107,31 @@ class _BoardColumnHeaderState extends State<BoardColumnHeader> {
title = _buildTextField(context); title = _buildTextField(context);
} }
return AppFlowyGroupHeader( return Padding(
title: title, padding: widget.margin,
icon: _buildHeaderIcon(boardCustomData), child: SizedBox(
addIcon: SizedBox( height: 50,
height: 20, child: Row(
children: [
_buildHeaderIcon(boardCustomData),
title,
const HSpace(6),
_groupOptionsButton(context),
const HSpace(4),
FlowyTooltip(
message: LocaleKeys.board_column_addToColumnTopTooltip.tr(),
child: FlowyIconButton(
width: 20, width: 20,
child: FlowySvg( icon: const FlowySvg(FlowySvgs.add_s),
FlowySvgs.add_s, iconColorOnHover: Theme.of(context).colorScheme.onSurface,
color: Theme.of(context).iconTheme.color, onPressed: () => context
),
),
onAddButtonClick: () => context
.read<BoardBloc>() .read<BoardBloc>()
.add(BoardEvent.createHeaderRow(widget.groupData.id)), .add(BoardEvent.createHeaderRow(widget.groupData.id)),
height: 50, ),
margin: widget.margin ?? EdgeInsets.zero, ),
],
),
),
); );
}, },
); );
@ -154,7 +158,6 @@ class _BoardColumnHeaderState extends State<BoardColumnHeader> {
filled: true, filled: true,
fillColor: Theme.of(context).colorScheme.surface, fillColor: Theme.of(context).colorScheme.surface,
hoverColor: Colors.transparent, hoverColor: Colors.transparent,
// Magic number 4 makes the textField take up the same space as FlowyText
contentPadding: EdgeInsets.symmetric( contentPadding: EdgeInsets.symmetric(
vertical: CardSizes.cardCellVPadding + 4, vertical: CardSizes.cardCellVPadding + 4,
horizontal: 8, horizontal: 8,
@ -181,45 +184,93 @@ class _BoardColumnHeaderState extends State<BoardColumnHeader> {
); );
} }
void _saveEdit() { void _saveEdit() => context
context.read<BoardBloc>().add( .read<BoardBloc>()
BoardEvent.endEditingHeader( .add(BoardEvent.endEditingHeader(widget.groupData.id, _controller.text));
widget.groupData.id,
_controller.text,
),
);
}
}
Widget? _buildHeaderIcon(GroupData customData) { Widget _buildHeaderIcon(GroupData customData) =>
Widget? widget;
switch (customData.fieldType) { switch (customData.fieldType) {
case FieldType.Checkbox: FieldType.Checkbox => FlowySvg(
final group = customData.asCheckboxGroup()!; customData.asCheckboxGroup()!.isCheck
widget = FlowySvg( ? FlowySvgs.check_filled_s
group.isCheck ? FlowySvgs.check_filled_s : FlowySvgs.uncheck_s, : FlowySvgs.uncheck_s,
blendMode: BlendMode.dst, blendMode: BlendMode.dst,
); ),
break; _ => const SizedBox.shrink(),
case FieldType.DateTime: };
case FieldType.LastEditedTime:
case FieldType.CreatedTime:
case FieldType.MultiSelect:
case FieldType.Number:
case FieldType.RichText:
case FieldType.SingleSelect:
case FieldType.URL:
case FieldType.Checklist:
break;
}
if (widget != null) { Widget _groupOptionsButton(BuildContext context) {
widget = SizedBox( return AppFlowyPopover(
clickHandler: PopoverClickHandler.gestureDetector,
margin: const EdgeInsets.fromLTRB(8, 8, 8, 4),
constraints: BoxConstraints.loose(const Size(168, 300)),
direction: PopoverDirection.bottomWithLeftAligned,
child: FlowyIconButton(
width: 20, width: 20,
height: 20, icon: const FlowySvg(FlowySvgs.details_horizontal_s),
child: widget, iconColorOnHover: Theme.of(context).colorScheme.onSurface,
),
popupBuilder: (popoverContext) {
final customGroupData = widget.groupData.customData as GroupData;
final menuItems = GroupOptions.values.toList();
if (!customGroupData.fieldType.canEditHeader) {
menuItems.remove(GroupOptions.rename);
}
return Column(
mainAxisSize: MainAxisSize.min,
children: [
...menuItems.map(
(action) => SizedBox(
height: GridSize.popoverItemHeight,
child: Padding(
padding: const EdgeInsets.only(bottom: 4.0),
child: FlowyButton(
leftIcon: FlowySvg(action.icon),
text: FlowyText.medium(
action.text,
overflow: TextOverflow.ellipsis,
),
onTap: () {
action.call(context, customGroupData.group);
PopoverContainer.of(popoverContext).close();
},
),
),
),
)
],
);
},
); );
} }
}
return widget;
enum GroupOptions {
rename,
hide;
void call(BuildContext context, GroupPB group) {
switch (this) {
case rename:
context
.read<BoardBloc>()
.add(BoardEvent.startEditingHeader(group.groupId));
break;
case hide:
context
.read<BoardBloc>()
.add(BoardEvent.toggleGroupVisibility(group, false));
break;
}
}
FlowySvgData get icon => switch (this) {
rename => FlowySvgs.edit_s,
hide => FlowySvgs.hide_s,
};
String get text => switch (this) {
rename => LocaleKeys.board_column_renameColumn.tr(),
hide => LocaleKeys.board_column_hideColumn.tr(),
};
} }

View File

@ -0,0 +1,483 @@
import 'dart:io';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database_view/application/cell/cell_service.dart';
import 'package:appflowy/plugins/database_view/application/database_controller.dart';
import 'package:appflowy/plugins/database_view/application/field/field_info.dart';
import 'package:appflowy/plugins/database_view/application/row/row_cache.dart';
import 'package:appflowy/plugins/database_view/application/row/row_controller.dart';
import 'package:appflowy/plugins/database_view/board/application/board_bloc.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart';
import 'package:appflowy/plugins/database_view/widgets/card/card_cell_builder.dart';
import 'package:appflowy/plugins/database_view/widgets/card/cells/card_cell.dart';
import 'package:appflowy/plugins/database_view/widgets/row/cell_builder.dart';
import 'package:appflowy/plugins/database_view/widgets/row/cells/text_cell/text_cell_bloc.dart';
import 'package:appflowy/plugins/database_view/widgets/row/row_detail.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:collection/collection.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/style_widget/hover.dart';
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class HiddenGroupsColumn extends StatelessWidget {
final EdgeInsets margin;
const HiddenGroupsColumn({super.key, required this.margin});
@override
Widget build(BuildContext context) {
final databaseController = context.read<BoardBloc>().databaseController;
return BlocSelector<BoardBloc, BoardState, BoardLayoutSettingPB?>(
selector: (state) => state.layoutSettings,
builder: (context, layoutSettings) {
if (layoutSettings == null) {
return const SizedBox.shrink();
}
final isCollapsed = layoutSettings.collapseHiddenGroups;
return AnimatedSize(
alignment: AlignmentDirectional.topStart,
curve: Curves.easeOut,
duration: const Duration(milliseconds: 150),
child: isCollapsed
? SizedBox(
height: 50,
child: Padding(
padding: const EdgeInsets.only(left: 40, right: 8),
child: Center(
child: _collapseExpandIcon(context, isCollapsed),
),
),
)
: SizedBox(
width: 260,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
height: 50,
child: Padding(
padding: EdgeInsets.only(
left: 40 + margin.left,
right: margin.right,
),
child: Row(
children: [
Expanded(
child: FlowyText.medium(
LocaleKeys
.board_hiddenGroupSection_sectionTitle
.tr(),
fontSize: 14,
overflow: TextOverflow.ellipsis,
color: Theme.of(context).hintColor,
),
),
_collapseExpandIcon(context, isCollapsed),
],
),
),
),
Expanded(
child: HiddenGroupList(
databaseController: databaseController,
),
),
],
),
),
);
},
);
}
Widget _collapseExpandIcon(BuildContext context, bool isCollapsed) {
return FlowyTooltip(
message: isCollapsed
? LocaleKeys.board_hiddenGroupSection_expandTooltip.tr()
: LocaleKeys.board_hiddenGroupSection_collapseTooltip.tr(),
child: FlowyIconButton(
width: 20,
height: 20,
iconColorOnHover: Theme.of(context).colorScheme.onSurface,
onPressed: () => context
.read<BoardBloc>()
.add(BoardEvent.toggleHiddenSectionVisibility(!isCollapsed)),
icon: FlowySvg(
isCollapsed
? FlowySvgs.hamburger_s_s
: FlowySvgs.pull_left_outlined_s,
),
),
);
}
}
class HiddenGroupList extends StatelessWidget {
const HiddenGroupList({
super.key,
required this.databaseController,
});
final DatabaseController databaseController;
@override
Widget build(BuildContext context) {
final bloc = context.read<BoardBloc>();
return BlocBuilder<BoardBloc, BoardState>(
builder: (_, state) => ReorderableListView.builder(
proxyDecorator: (child, index, animation) => Material(
color: Colors.transparent,
child: Stack(
children: [
child,
MouseRegion(
cursor: Platform.isWindows
? SystemMouseCursors.click
: SystemMouseCursors.grabbing,
child: const SizedBox.expand(),
),
],
),
),
buildDefaultDragHandles: false,
itemCount: state.hiddenGroups.length,
itemBuilder: (_, index) => Padding(
padding: const EdgeInsets.only(bottom: 4),
key: ValueKey("hiddenGroup${state.hiddenGroups[index].groupId}"),
child: HiddenGroupCard(
group: state.hiddenGroups[index],
index: index,
bloc: bloc,
),
),
onReorder: (oldIndex, newIndex) {
if (oldIndex < newIndex) {
newIndex--;
}
final fromGroupId = state.hiddenGroups[oldIndex].groupId;
final toGroupId = state.hiddenGroups[newIndex].groupId;
bloc.add(BoardEvent.reorderGroup(fromGroupId, toGroupId));
},
),
);
}
}
class HiddenGroupCard extends StatefulWidget {
const HiddenGroupCard({
super.key,
required this.group,
required this.index,
required this.bloc,
});
final GroupPB group;
final BoardBloc bloc;
final int index;
@override
State<HiddenGroupCard> createState() => _HiddenGroupCardState();
}
class _HiddenGroupCardState extends State<HiddenGroupCard> {
final PopoverController _popoverController = PopoverController();
@override
Widget build(BuildContext context) {
final databaseController = widget.bloc.databaseController;
final primaryField = databaseController.fieldController.fieldInfos
.firstWhereOrNull((element) => element.isPrimary)!;
return Padding(
padding: const EdgeInsets.only(left: 26),
child: AppFlowyPopover(
controller: _popoverController,
direction: PopoverDirection.bottomWithCenterAligned,
triggerActions: PopoverTriggerFlags.none,
constraints: const BoxConstraints(maxWidth: 234, maxHeight: 300),
popupBuilder: (popoverContext) => HiddenGroupPopupItemList(
bloc: widget.bloc,
viewId: databaseController.viewId,
groupId: widget.group.groupId,
primaryField: primaryField,
rowCache: databaseController.rowCache,
),
child: HiddenGroupButtonContent(
popoverController: _popoverController,
groupId: widget.group.groupId,
index: widget.index,
bloc: widget.bloc,
),
),
);
}
}
class HiddenGroupButtonContent extends StatelessWidget {
final String groupId;
final int index;
final BoardBloc bloc;
const HiddenGroupButtonContent({
super.key,
required this.popoverController,
required this.groupId,
required this.index,
required this.bloc,
});
final PopoverController popoverController;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(right: 8.0),
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: popoverController.show,
child: FlowyHover(
builder: (context, isHovering) {
return BlocProvider<BoardBloc>.value(
value: bloc,
child: BlocBuilder<BoardBloc, BoardState>(
builder: (context, state) {
final group = state.hiddenGroups.firstWhereOrNull(
(g) => g.groupId == groupId,
);
if (group == null) {
return const SizedBox.shrink();
}
return SizedBox(
height: 30,
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 4,
vertical: 3,
),
child: Row(
children: [
HiddenGroupCardActions(
isVisible: isHovering,
index: index,
),
const HSpace(4),
FlowyText.medium(
group.groupName,
overflow: TextOverflow.ellipsis,
),
const HSpace(6),
Expanded(
child: FlowyText.medium(
group.rows.length.toString(),
overflow: TextOverflow.ellipsis,
color: Theme.of(context).hintColor,
),
),
if (isHovering) ...[
FlowyIconButton(
width: 20,
icon: FlowySvg(
FlowySvgs.show_m,
color: Theme.of(context).hintColor,
),
onPressed: () => context.read<BoardBloc>().add(
BoardEvent.toggleGroupVisibility(
group,
true,
),
),
),
],
],
),
),
);
},
),
);
},
),
),
);
}
}
class HiddenGroupCardActions extends StatelessWidget {
final bool isVisible;
final int index;
const HiddenGroupCardActions({
super.key,
required this.isVisible,
required this.index,
});
@override
Widget build(BuildContext context) {
return ReorderableDragStartListener(
index: index,
enabled: isVisible,
child: MouseRegion(
cursor: SystemMouseCursors.grab,
child: SizedBox(
height: 14,
width: 14,
child: isVisible
? FlowySvg(
FlowySvgs.drag_element_s,
color: Theme.of(context).hintColor,
)
: const SizedBox.shrink(),
),
),
);
}
}
class HiddenGroupPopupItemList extends StatelessWidget {
const HiddenGroupPopupItemList({
required this.bloc,
required this.groupId,
required this.viewId,
required this.primaryField,
required this.rowCache,
super.key,
});
final BoardBloc bloc;
final String groupId;
final String viewId;
final FieldInfo primaryField;
final RowCache rowCache;
@override
Widget build(BuildContext context) {
return BlocProvider.value(
value: bloc,
child: BlocBuilder<BoardBloc, BoardState>(
builder: (context, state) {
final group = state.hiddenGroups.firstWhereOrNull(
(g) => g.groupId == groupId,
);
if (group == null) {
return const SizedBox.shrink();
}
final cells = <Widget>[
Padding(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4),
child: FlowyText.medium(
group.groupName,
fontSize: 10,
color: Theme.of(context).hintColor,
overflow: TextOverflow.ellipsis,
),
),
...group.rows.map(
(item) {
final rowController = RowController(
rowMeta: item,
viewId: viewId,
rowCache: rowCache,
);
final renderHook = RowCardRenderHook<String>();
renderHook.addTextCellHook((cellData, _, __) {
return BlocBuilder<TextCellBloc, TextCellState>(
builder: (context, state) {
final text = cellData.isEmpty
? LocaleKeys.grid_row_titlePlaceholder.tr()
: cellData;
if (text.isEmpty) {
return const SizedBox.shrink();
}
return Align(
alignment: Alignment.centerLeft,
child: FlowyText.medium(
text,
textAlign: TextAlign.left,
fontSize: 11,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
);
},
);
});
return HiddenGroupPopupItem(
cellContext: rowCache.loadCells(item)[primaryField.id]!,
primaryField: primaryField,
rowController: rowController,
cellBuilder: CardCellBuilder<String>(rowController.cellCache),
renderHook: renderHook,
onPressed: () {
FlowyOverlay.show(
context: context,
builder: (BuildContext context) {
return RowDetailPage(
cellBuilder: GridCellBuilder(
cellCache: rowController.cellCache,
),
rowController: rowController,
);
},
);
PopoverContainer.of(context).close();
},
);
},
)
];
return ListView.separated(
itemBuilder: (context, index) => cells[index],
itemCount: cells.length,
separatorBuilder: (context, index) =>
VSpace(GridSize.typeOptionSeparatorHeight),
shrinkWrap: true,
);
},
),
);
}
}
class HiddenGroupPopupItem extends StatelessWidget {
const HiddenGroupPopupItem({
super.key,
required this.cellContext,
required this.onPressed,
required this.cellBuilder,
required this.rowController,
required this.primaryField,
required this.renderHook,
});
final DatabaseCellContext cellContext;
final FieldInfo primaryField;
final RowController rowController;
final CardCellBuilder cellBuilder;
final RowCardRenderHook<String> renderHook;
final VoidCallback onPressed;
@override
Widget build(BuildContext context) {
return SizedBox(
height: 26,
child: FlowyButton(
margin: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0),
text: cellBuilder.buildCell(
cellContext: cellContext,
renderHook: renderHook,
hasNotes: !cellContext.rowMeta.isDocumentEmpty,
),
onTap: onPressed,
),
);
}
}

View File

@ -21,7 +21,7 @@ class GridHeaderBloc extends Bloc<GridHeaderEvent, GridHeaderState> {
on<GridHeaderEvent>( on<GridHeaderEvent>(
(event, emit) async { (event, emit) async {
await event.map( await event.map(
initial: (_InitialHeader value) async { initial: (_InitialHeader value) {
_startListening(); _startListening();
add( add(
GridHeaderEvent.didReceiveFieldUpdate(fieldController.fieldInfos), GridHeaderEvent.didReceiveFieldUpdate(fieldController.fieldInfos),
@ -65,7 +65,7 @@ class GridHeaderBloc extends Bloc<GridHeaderEvent, GridHeaderState> {
result.fold((l) {}, (err) => Log.error(err)); result.fold((l) {}, (err) => Log.error(err));
} }
Future<void> _startListening() async { void _startListening() {
fieldController.addListener( fieldController.addListener(
onReceiveFields: (fields) => onReceiveFields: (fields) =>
add(GridHeaderEvent.didReceiveFieldUpdate(fields)), add(GridHeaderEvent.didReceiveFieldUpdate(fields)),

View File

@ -6,7 +6,7 @@ class GridSize {
static double get scrollBarSize => 8 * scale; static double get scrollBarSize => 8 * scale;
static double get headerHeight => 40 * scale; static double get headerHeight => 40 * scale;
static double get footerHeight => 40 * scale; static double get footerHeight => 40 * scale;
static double get leadingHeaderPadding => 50 * scale; static double get leadingHeaderPadding => 40 * scale;
static double get trailHeaderPadding => 140 * scale; static double get trailHeaderPadding => 140 * scale;
static double get headerContainerPadding => 0 * scale; static double get headerContainerPadding => 0 * scale;
static double get cellHPadding => 10 * scale; static double get cellHPadding => 10 * scale;

View File

@ -59,4 +59,10 @@ extension FieldTypeListExtension on FieldType {
FieldType.SingleSelect => true, FieldType.SingleSelect => true,
_ => false, _ => false,
}; };
bool get canCreateNewGroup => switch (this) {
FieldType.MultiSelect => true,
FieldType.SingleSelect => true,
_ => false,
};
} }

View File

@ -10,7 +10,6 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import '../application/database_controller.dart'; import '../application/database_controller.dart';
import '../grid/presentation/layout/sizes.dart';
import 'tab_bar_header.dart'; import 'tab_bar_header.dart';
abstract class DatabaseTabBarItemBuilder { abstract class DatabaseTabBarItemBuilder {
@ -95,13 +94,11 @@ class _DatabaseTabBarViewState extends State<DatabaseTabBarView> {
if (value) { if (value) {
return const SizedBox.shrink(); return const SizedBox.shrink();
} }
return SizedBox( return const SizedBox(
height: 30, height: 30,
child: Padding( child: Padding(
padding: EdgeInsets.symmetric( padding: EdgeInsets.symmetric(horizontal: 40),
horizontal: GridSize.leadingHeaderPadding, child: TabBarHeader(),
),
child: const TabBarHeader(),
), ),
); );
}, },

View File

@ -239,6 +239,7 @@ class PageManager {
shrinkWrap: false, shrinkWrap: false,
); );
// TODO(Xazin): Board should fill up full width
return Padding( return Padding(
padding: builder.contentPadding, padding: builder.contentPadding,
child: pluginWidget, child: pluginWidget,

View File

@ -70,7 +70,7 @@ class FlowyIconButton extends StatelessWidget {
shape: shape:
RoundedRectangleBorder(borderRadius: radius ?? Corners.s6Border), RoundedRectangleBorder(borderRadius: radius ?? Corners.s6Border),
fillColor: fillColor, fillColor: fillColor,
hoverColor: hoverColor, hoverColor: Colors.transparent,
focusColor: Colors.transparent, focusColor: Colors.transparent,
splashColor: Colors.transparent, splashColor: Colors.transparent,
highlightColor: Colors.transparent, highlightColor: Colors.transparent,
@ -79,7 +79,6 @@ class FlowyIconButton extends StatelessWidget {
child: FlowyHover( child: FlowyHover(
isSelected: isSelected != null ? () => isSelected! : null, isSelected: isSelected != null ? () => isSelected! : null,
style: HoverStyle( style: HoverStyle(
// hoverColor is set in both [HoverStyle] and [RawMaterialButton] to avoid the conflicts between two layers
hoverColor: hoverColor, hoverColor: hoverColor,
foregroundColorOnHover: foregroundColorOnHover:
iconColorOnHover ?? Theme.of(context).iconTheme.color, iconColorOnHover ?? Theme.of(context).iconTheme.color,
@ -88,9 +87,7 @@ class FlowyIconButton extends StatelessWidget {
resetHoverOnRebuild: false, resetHoverOnRebuild: false,
child: Padding( child: Padding(
padding: iconPadding, padding: iconPadding,
child: Center( child: Center(child: child),
child: child,
),
), ),
), ),
), ),
@ -100,11 +97,9 @@ class FlowyIconButton extends StatelessWidget {
} }
class FlowyDropdownButton extends StatelessWidget { class FlowyDropdownButton extends StatelessWidget {
const FlowyDropdownButton({super.key, this.onPressed});
final VoidCallback? onPressed; final VoidCallback? onPressed;
const FlowyDropdownButton({
Key? key,
this.onPressed,
}) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {

View File

@ -45,11 +45,11 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
path: "." path: "."
ref: "1a329c2" ref: "2de4fe0"
resolved-ref: "1a329c21921c0d19871bea3237b7d80fe131f2ed" resolved-ref: "2de4fe0b0245dcdf2c2bf43410661c28acbcc687"
url: "https://github.com/AppFlowy-IO/appflowy-board.git" url: "https://github.com/AppFlowy-IO/appflowy-board.git"
source: git source: git
version: "0.1.0" version: "0.1.1"
appflowy_editor: appflowy_editor:
dependency: "direct main" dependency: "direct main"
description: description:

View File

@ -43,7 +43,7 @@ dependencies:
# path: packages/appflowy_board # path: packages/appflowy_board
git: git:
url: https://github.com/AppFlowy-IO/appflowy-board.git url: https://github.com/AppFlowy-IO/appflowy-board.git
ref: 1a329c2 ref: 2de4fe0
appflowy_editor: appflowy_editor:
git: git:
url: https://github.com/AppFlowy-IO/appflowy-editor.git url: https://github.com/AppFlowy-IO/appflowy-editor.git

View File

@ -107,8 +107,8 @@ void main() {
final groups = final groups =
boardBloc.groupControllers.values.map((e) => e.group).toList(); boardBloc.groupControllers.values.map((e) => e.group).toList();
assert(groups[0].groupName == "B"); assert(groups[0].groupName == "No ${multiSelectField.name}");
assert(groups[1].groupName == "A"); assert(groups[1].groupName == "B");
assert(groups[2].groupName == "No ${multiSelectField.name}"); assert(groups[2].groupName == "A");
}); });
} }

View File

@ -0,0 +1,7 @@
<svg width="6" height="8" viewBox="0 0 6 8" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="Vector">
<path d="M0 0.799951C0 0.46858 0.268629 0.199951 0.6 0.199951H5.4C5.73137 0.199951 6 0.46858 6 0.799951C6 1.13132 5.73137 1.39995 5.4 1.39995H0.600001C0.26863 1.39995 0 1.13132 0 0.799951Z" fill="#8F959E"/>
<path d="M0 3.99995C0 3.66858 0.268629 3.39995 0.6 3.39995H5.4C5.73137 3.39995 6 3.66858 6 3.99995C6 4.33132 5.73137 4.59995 5.4 4.59995H0.600001C0.26863 4.59995 0 4.33132 0 3.99995Z" fill="#8F959E"/>
<path d="M0 7.19995C0 6.86858 0.268629 6.59995 0.6 6.59995H5.4C5.73137 6.59995 6 6.86858 6 7.19995C6 7.53132 5.73137 7.79995 5.4 7.79995H0.600001C0.26863 7.79995 0 7.53132 0 7.19995Z" fill="#8F959E"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 729 B

View File

@ -0,0 +1,6 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="icon_pull-left_outlined">
<path id="Union" d="M14.6515 9.58019H9.1031L10.7447 7.91716C10.9061 7.75372 10.9048 7.4864 10.7435 7.3229C10.5825 7.1597 10.3193 7.15862 10.1582 7.32177L7.66078 9.85165C7.62187 9.89109 7.60001 9.94456 7.60001 10.0003C7.60001 10.0561 7.62187 10.1095 7.66078 10.149L10.1566 12.6773C10.3184 12.8412 10.5808 12.8407 10.7426 12.6767C10.9046 12.5125 10.9053 12.2461 10.7432 12.082L9.10373 10.4213H14.6515C14.8808 10.4213 15.0667 10.233 15.0667 10.0007C15.0667 9.76848 14.8808 9.58019 14.6515 9.58019Z" fill="#8F959E" stroke="#8F959E" stroke-width="0.1" stroke-linecap="round"/>
<path id="Union_2" d="M5.19995 14.1126C5.19995 14.437 5.42384 14.7 5.69995 14.7C5.97606 14.7 6.19995 14.437 6.19995 14.1126V5.88753C6.19995 5.56307 5.97606 5.30005 5.69995 5.30005C5.42384 5.30005 5.19995 5.56307 5.19995 5.88753V14.1126Z" fill="#8F959E"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 968 B

View File

@ -619,9 +619,9 @@
"createInlineMathEquation": "Create equation", "createInlineMathEquation": "Create equation",
"fonts": "Fonts", "fonts": "Fonts",
"toggleList": "Toggle list", "toggleList": "Toggle list",
"quoteList":"Quote list", "quoteList": "Quote list",
"numberedList":"Numbered list", "numberedList": "Numbered list",
"bulletedList":"Bulleted list", "bulletedList": "Bulleted list",
"todoList": "Todo List", "todoList": "Todo List",
"callout": "Callout", "callout": "Callout",
"cover": { "cover": {
@ -772,7 +772,15 @@
"column": { "column": {
"createNewCard": "New", "createNewCard": "New",
"renameGroupTooltip": "Press to rename group", "renameGroupTooltip": "Press to rename group",
"createNewColumn": "Add a new group" "createNewColumn": "Add a new group",
"addToColumnTopTooltip": "Add a new card at the top",
"renameColumn": "Rename",
"hideColumn": "Hide"
},
"hiddenGroupSection": {
"sectionTitle": "Hidden Groups",
"collapseTooltip": "Hide the hidden groups",
"expandTooltip": "View the hidden groups"
}, },
"menuName": "Board", "menuName": "Board",
"showUngrouped": "Show ungrouped items", "showUngrouped": "Show ungrouped items",

View File

@ -786,7 +786,8 @@ async fn hide_group_event_test() {
assert!(error.is_none()); assert!(error.is_none());
let groups = test.get_groups(&board_view.id).await; let groups = test.get_groups(&board_view.id).await;
assert_eq!(groups.len(), 3); assert_eq!(groups.len(), 4);
assert_eq!(groups[0].is_visible, false);
} }
// Update the database layout type from grid to board // Update the database layout type from grid to board

View File

@ -6,12 +6,16 @@ use crate::services::setting::BoardLayoutSetting;
pub struct BoardLayoutSettingPB { pub struct BoardLayoutSettingPB {
#[pb(index = 1)] #[pb(index = 1)]
pub hide_ungrouped_column: bool, pub hide_ungrouped_column: bool,
#[pb(index = 2)]
pub collapse_hidden_groups: bool,
} }
impl From<BoardLayoutSetting> for BoardLayoutSettingPB { impl From<BoardLayoutSetting> for BoardLayoutSettingPB {
fn from(setting: BoardLayoutSetting) -> Self { fn from(setting: BoardLayoutSetting) -> Self {
Self { Self {
hide_ungrouped_column: setting.hide_ungrouped_column, hide_ungrouped_column: setting.hide_ungrouped_column,
collapse_hidden_groups: setting.collapse_hidden_groups,
} }
} }
} }
@ -20,6 +24,7 @@ impl From<BoardLayoutSettingPB> for BoardLayoutSetting {
fn from(setting: BoardLayoutSettingPB) -> Self { fn from(setting: BoardLayoutSettingPB) -> Self {
Self { Self {
hide_ungrouped_column: setting.hide_ungrouped_column, hide_ungrouped_column: setting.hide_ungrouped_column,
collapse_hidden_groups: setting.collapse_hidden_groups,
} }
} }
} }

View File

@ -312,7 +312,6 @@ impl DatabaseViewEditor {
.as_ref()? .as_ref()?
.get_all_groups() .get_all_groups()
.into_iter() .into_iter()
.filter(|group| group.is_visible)
.map(|group_data| GroupPB::from(group_data.clone())) .map(|group_data| GroupPB::from(group_data.clone()))
.collect::<Vec<_>>(); .collect::<Vec<_>>();
tracing::trace!("Number of groups: {}", groups.len()); tracing::trace!("Number of groups: {}", groups.len());
@ -398,12 +397,16 @@ impl DatabaseViewEditor {
pub async fn v_update_group(&self, changeset: GroupChangesets) -> FlowyResult<()> { pub async fn v_update_group(&self, changeset: GroupChangesets) -> FlowyResult<()> {
let mut type_option_data = TypeOptionData::new(); let mut type_option_data = TypeOptionData::new();
let old_field = if let Some(controller) = self.group_controller.write().await.as_mut() { let (old_field, updated_groups) = if let Some(controller) =
self.group_controller.write().await.as_mut()
{
let old_field = self.delegate.get_field(controller.field_id()); let old_field = self.delegate.get_field(controller.field_id());
type_option_data.extend(controller.apply_group_changeset(&changeset).await?); let (updated_groups, new_type_option) = controller.apply_group_changeset(&changeset).await?;
old_field type_option_data.extend(new_type_option);
(old_field, updated_groups)
} else { } else {
None (None, vec![])
}; };
if let Some(old_field) = old_field { if let Some(old_field) = old_field {
@ -413,6 +416,12 @@ impl DatabaseViewEditor {
.update_field(&self.view_id, type_option_data, old_field) .update_field(&self.view_id, type_option_data, old_field)
.await?; .await?;
} }
let notification = GroupChangesPB {
view_id: self.view_id.clone(),
update_groups: updated_groups,
..Default::default()
};
notify_did_update_num_of_groups(&self.view_id, notification).await;
} }
Ok(()) Ok(())

View File

@ -168,7 +168,7 @@ pub trait GroupControllerOperation: Send + Sync {
async fn apply_group_changeset( async fn apply_group_changeset(
&mut self, &mut self,
changesets: &GroupChangesets, changesets: &GroupChangesets,
) -> FlowyResult<TypeOptionData>; ) -> FlowyResult<(Vec<GroupPB>, TypeOptionData)>;
} }
#[derive(Debug)] #[derive(Debug)]

View File

@ -173,6 +173,7 @@ where
self.field.id.clone(), self.field.id.clone(),
group.name.clone(), group.name.clone(),
group.id.clone(), group.id.clone(),
group.visible,
); );
self.group_by_id.insert(group.id.clone(), group_data); self.group_by_id.insert(group.id.clone(), group_data);
let (index, group_data) = self.get_group(&group.id).unwrap(); let (index, group_data) = self.get_group(&group.id).unwrap();
@ -338,7 +339,13 @@ where
.get(&group.id) .get(&group.id)
.cloned() .cloned()
.unwrap_or_else(|| "".to_owned()); .unwrap_or_else(|| "".to_owned());
let group = GroupData::new(group.id, self.field.id.clone(), group.name, filter_content); let group = GroupData::new(
group.id,
self.field.id.clone(),
group.name,
filter_content,
group.visible,
);
self.group_by_id.insert(group.id.clone(), group); self.group_by_id.insert(group.id.clone(), group);
}); });
@ -351,6 +358,7 @@ where
self.field.id.clone(), self.field.id.clone(),
group_rev.name, group_rev.name,
filter_content.clone(), filter_content.clone(),
group_rev.visible,
); );
Some(GroupPB::from(group)) Some(GroupPB::from(group))
}) })

View File

@ -11,7 +11,8 @@ use serde::Serialize;
use flowy_error::FlowyResult; use flowy_error::FlowyResult;
use crate::entities::{ use crate::entities::{
FieldType, GroupChangesPB, GroupRowsNotificationPB, InsertedGroupPB, InsertedRowPB, RowMetaPB, FieldType, GroupChangesPB, GroupPB, GroupRowsNotificationPB, InsertedGroupPB, InsertedRowPB,
RowMetaPB,
}; };
use crate::services::cell::{get_cell_protobuf, CellProtobufBlobParser}; use crate::services::cell::{get_cell_protobuf, CellProtobufBlobParser};
use crate::services::field::{default_type_option_data_from_type, TypeOption, TypeOptionCellData}; use crate::services::field::{default_type_option_data_from_type, TypeOption, TypeOptionCellData};
@ -45,10 +46,12 @@ pub trait GroupOperationInterceptor {
type GroupTypeOption: TypeOption; type GroupTypeOption: TypeOption;
async fn type_option_from_group_changeset( async fn type_option_from_group_changeset(
&self, &self,
changeset: &GroupChangeset, _changeset: &GroupChangeset,
type_option: &Self::GroupTypeOption, _type_option: &Self::GroupTypeOption,
view_id: &str, _view_id: &str,
) -> Option<TypeOptionData>; ) -> Option<TypeOptionData> {
None
}
} }
/// C: represents the group configuration that impl [GroupConfigurationSerde] /// C: represents the group configuration that impl [GroupConfigurationSerde]
@ -396,7 +399,7 @@ where
async fn apply_group_changeset( async fn apply_group_changeset(
&mut self, &mut self,
changeset: &GroupChangesets, changeset: &GroupChangesets,
) -> FlowyResult<TypeOptionData> { ) -> FlowyResult<(Vec<GroupPB>, TypeOptionData)> {
for group_changeset in changeset.changesets.iter() { for group_changeset in changeset.changesets.iter() {
self.context.update_group(group_changeset)?; self.context.update_group(group_changeset)?;
} }
@ -410,7 +413,16 @@ where
type_option_data.extend(new_type_option_data); type_option_data.extend(new_type_option_data);
} }
} }
Ok(type_option_data) let updated_groups = changeset
.changesets
.iter()
.filter_map(|changeset| {
self
.get_group(&changeset.group_id)
.map(|(_, group)| GroupPB::from(group))
})
.collect::<Vec<_>>();
Ok((updated_groups, type_option_data))
} }
} }

View File

@ -1,5 +1,5 @@
use async_trait::async_trait; use async_trait::async_trait;
use collab_database::fields::{Field, TypeOptionData}; use collab_database::fields::Field;
use collab_database::rows::{new_cell_builder, Cell, Cells, Row, RowDetail}; use collab_database::rows::{new_cell_builder, Cell, Cells, Row, RowDetail};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -12,8 +12,8 @@ use crate::services::group::action::GroupCustomize;
use crate::services::group::configuration::GroupContext; use crate::services::group::configuration::GroupContext;
use crate::services::group::controller::{BaseGroupController, GroupController}; use crate::services::group::controller::{BaseGroupController, GroupController};
use crate::services::group::{ use crate::services::group::{
move_group_row, GeneratedGroupConfig, GeneratedGroups, Group, GroupChangeset, move_group_row, GeneratedGroupConfig, GeneratedGroups, Group, GroupOperationInterceptor,
GroupOperationInterceptor, GroupsBuilder, MoveGroupRowContext, GroupsBuilder, MoveGroupRowContext,
}; };
#[derive(Default, Serialize, Deserialize)] #[derive(Default, Serialize, Deserialize)]
@ -190,12 +190,4 @@ pub struct CheckboxGroupOperationInterceptorImpl {}
#[async_trait] #[async_trait]
impl GroupOperationInterceptor for CheckboxGroupOperationInterceptorImpl { impl GroupOperationInterceptor for CheckboxGroupOperationInterceptorImpl {
type GroupTypeOption = CheckboxTypeOption; type GroupTypeOption = CheckboxTypeOption;
async fn type_option_from_group_changeset(
&self,
_changeset: &GroupChangeset,
_type_option: &Self::GroupTypeOption,
_view_id: &str,
) -> Option<TypeOptionData> {
todo!()
}
} }

View File

@ -7,7 +7,7 @@ use chrono::{
}; };
use chrono_tz::Tz; use chrono_tz::Tz;
use collab_database::database::timestamp; use collab_database::database::timestamp;
use collab_database::fields::{Field, TypeOptionData}; use collab_database::fields::Field;
use collab_database::rows::{new_cell_builder, Cell, Cells, Row, RowDetail}; use collab_database::rows::{new_cell_builder, Cell, Cells, Row, RowDetail};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_repr::{Deserialize_repr, Serialize_repr}; use serde_repr::{Deserialize_repr, Serialize_repr};
@ -24,7 +24,7 @@ use crate::services::group::configuration::GroupContext;
use crate::services::group::controller::{BaseGroupController, GroupController}; use crate::services::group::controller::{BaseGroupController, GroupController};
use crate::services::group::{ use crate::services::group::{
make_no_status_group, move_group_row, GeneratedGroupConfig, GeneratedGroups, Group, make_no_status_group, move_group_row, GeneratedGroupConfig, GeneratedGroups, Group,
GroupChangeset, GroupOperationInterceptor, GroupsBuilder, MoveGroupRowContext, GroupOperationInterceptor, GroupsBuilder, MoveGroupRowContext,
}; };
pub trait GroupConfigurationContentSerde: Sized + Send + Sync { pub trait GroupConfigurationContentSerde: Sized + Send + Sync {
@ -458,14 +458,6 @@ pub struct DateGroupOperationInterceptorImpl {}
#[async_trait] #[async_trait]
impl GroupOperationInterceptor for DateGroupOperationInterceptorImpl { impl GroupOperationInterceptor for DateGroupOperationInterceptorImpl {
type GroupTypeOption = DateTypeOption; type GroupTypeOption = DateTypeOption;
async fn type_option_from_group_changeset(
&self,
_changeset: &GroupChangeset,
_type_option: &Self::GroupTypeOption,
_view_id: &str,
) -> Option<TypeOptionData> {
todo!()
}
} }
#[cfg(test)] #[cfg(test)]

View File

@ -6,7 +6,9 @@ use collab_database::rows::{Cells, Row, RowDetail};
use flowy_error::FlowyResult; use flowy_error::FlowyResult;
use crate::entities::{GroupChangesPB, GroupRowsNotificationPB, InsertedGroupPB, InsertedRowPB}; use crate::entities::{
GroupChangesPB, GroupPB, GroupRowsNotificationPB, InsertedGroupPB, InsertedRowPB,
};
use crate::services::group::action::{ use crate::services::group::action::{
DidMoveGroupRowResult, DidUpdateGroupRowResult, GroupControllerOperation, DidMoveGroupRowResult, DidUpdateGroupRowResult, GroupControllerOperation,
}; };
@ -30,6 +32,7 @@ impl DefaultGroupController {
field.id.clone(), field.id.clone(),
"".to_owned(), "".to_owned(),
"".to_owned(), "".to_owned(),
true,
); );
Self { Self {
field_id: field.id.clone(), field_id: field.id.clone(),
@ -129,8 +132,8 @@ impl GroupControllerOperation for DefaultGroupController {
async fn apply_group_changeset( async fn apply_group_changeset(
&mut self, &mut self,
_changeset: &GroupChangesets, _changeset: &GroupChangesets,
) -> FlowyResult<TypeOptionData> { ) -> FlowyResult<(Vec<GroupPB>, TypeOptionData)> {
Ok(TypeOptionData::default()) Ok((Vec::new(), TypeOptionData::default()))
} }
} }

View File

@ -1,7 +1,7 @@
use std::sync::Arc; use std::sync::Arc;
use async_trait::async_trait; use async_trait::async_trait;
use collab_database::fields::{Field, TypeOptionData}; use collab_database::fields::Field;
use collab_database::rows::{new_cell_builder, Cell, Cells, Row, RowDetail}; use collab_database::rows::{new_cell_builder, Cell, Cells, Row, RowDetail};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -17,8 +17,7 @@ use crate::services::group::configuration::GroupContext;
use crate::services::group::controller::{BaseGroupController, GroupController}; use crate::services::group::controller::{BaseGroupController, GroupController};
use crate::services::group::{ use crate::services::group::{
make_no_status_group, move_group_row, GeneratedGroupConfig, GeneratedGroups, Group, make_no_status_group, move_group_row, GeneratedGroupConfig, GeneratedGroups, Group,
GroupChangeset, GroupOperationInterceptor, GroupTypeOptionCellOperation, GroupsBuilder, GroupOperationInterceptor, GroupTypeOptionCellOperation, GroupsBuilder, MoveGroupRowContext,
MoveGroupRowContext,
}; };
#[derive(Default, Serialize, Deserialize)] #[derive(Default, Serialize, Deserialize)]
@ -250,12 +249,4 @@ pub struct URLGroupOperationInterceptorImpl {
#[async_trait::async_trait] #[async_trait::async_trait]
impl GroupOperationInterceptor for URLGroupOperationInterceptorImpl { impl GroupOperationInterceptor for URLGroupOperationInterceptorImpl {
type GroupTypeOption = URLTypeOption; type GroupTypeOption = URLTypeOption;
async fn type_option_from_group_changeset(
&self,
_changeset: &GroupChangeset,
_type_option: &Self::GroupTypeOption,
_view_id: &str,
) -> Option<TypeOptionData> {
todo!()
}
} }

View File

@ -148,13 +148,19 @@ pub struct GroupData {
} }
impl GroupData { impl GroupData {
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,
is_visible: bool,
) -> Self {
let is_default = id == field_id; let is_default = id == field_id;
Self { Self {
id, id,
field_id, field_id,
is_default, is_default,
is_visible: true, is_visible,
name, name,
rows: vec![], rows: vec![],
filter_content, filter_content,

View File

@ -93,6 +93,7 @@ pub const DEFAULT_SHOW_WEEK_NUMBERS: bool = true;
#[derive(Debug, Clone, Default)] #[derive(Debug, Clone, Default)]
pub struct BoardLayoutSetting { pub struct BoardLayoutSetting {
pub hide_ungrouped_column: bool, pub hide_ungrouped_column: bool,
pub collapse_hidden_groups: bool,
} }
impl BoardLayoutSetting { impl BoardLayoutSetting {
@ -107,6 +108,9 @@ impl From<LayoutSetting> for BoardLayoutSetting {
hide_ungrouped_column: setting hide_ungrouped_column: setting
.get_bool_value("hide_ungrouped_column") .get_bool_value("hide_ungrouped_column")
.unwrap_or_default(), .unwrap_or_default(),
collapse_hidden_groups: setting
.get_bool_value("collapse_hidden_groups")
.unwrap_or_default(),
} }
} }
} }
@ -115,6 +119,7 @@ impl From<BoardLayoutSetting> for LayoutSetting {
fn from(setting: BoardLayoutSetting) -> Self { fn from(setting: BoardLayoutSetting) -> Self {
LayoutSettingBuilder::new() LayoutSettingBuilder::new()
.insert_bool_value("hide_ungrouped_column", setting.hide_ungrouped_column) .insert_bool_value("hide_ungrouped_column", setting.hide_ungrouped_column)
.insert_bool_value("collapse_hidden_groups", setting.collapse_hidden_groups)
.build() .build()
} }
} }

View File

@ -11,6 +11,7 @@ async fn board_layout_setting_test() {
let default_board_setting = BoardLayoutSetting::new(); let default_board_setting = BoardLayoutSetting::new();
let new_board_setting = BoardLayoutSetting { let new_board_setting = BoardLayoutSetting {
hide_ungrouped_column: true, hide_ungrouped_column: true,
..default_board_setting
}; };
let scripts = vec![ let scripts = vec![
AssertBoardLayoutSetting { AssertBoardLayoutSetting {