mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
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:
parent
7867f0366e
commit
a63a7ea611
@ -1,4 +1,5 @@
|
||||
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_board/appflowy_board.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||
@ -32,10 +33,12 @@ void main() {
|
||||
await tester.tap(
|
||||
find
|
||||
.descendant(
|
||||
of: find.byType(AppFlowyGroupHeader),
|
||||
matching: find.byType(FlowySvg),
|
||||
of: find.byType(BoardColumnHeader),
|
||||
matching: find.byWidgetPredicate(
|
||||
(widget) => widget is FlowySvg && widget.svg == FlowySvgs.add_s,
|
||||
),
|
||||
)
|
||||
.first,
|
||||
.at(1),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
@ -77,7 +80,7 @@ void main() {
|
||||
of: find.byType(AppFlowyGroupFooter),
|
||||
matching: find.byType(FlowySvg),
|
||||
)
|
||||
.first,
|
||||
.at(1),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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/row/row_service.dart';
|
||||
import 'package:appflowy_board/appflowy_board.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:dartz/dartz.dart';
|
||||
import 'package:appflowy_backend/log.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_bloc/flutter_bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:protobuf/protobuf.dart' hide FieldInfo;
|
||||
|
||||
import '../../application/field/field_controller.dart';
|
||||
import '../../application/row/row_cache.dart';
|
||||
@ -23,12 +25,13 @@ import 'group_controller.dart';
|
||||
part 'board_bloc.freezed.dart';
|
||||
|
||||
class BoardBloc extends Bloc<BoardEvent, BoardState> {
|
||||
late final GroupBackendService groupBackendSvc;
|
||||
final DatabaseController databaseController;
|
||||
late final AppFlowyBoardController boardController;
|
||||
final LinkedHashMap<String, GroupController> groupControllers =
|
||||
LinkedHashMap();
|
||||
GroupPB? ungroupedGroup;
|
||||
final List<GroupPB> groupList = [];
|
||||
|
||||
late final GroupBackendService groupBackendSvc;
|
||||
late final AppFlowyBoardController boardController;
|
||||
|
||||
FieldController get fieldController => databaseController.fieldController;
|
||||
String get viewId => databaseController.viewId;
|
||||
@ -82,10 +85,8 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
|
||||
groupId: groupId,
|
||||
startRowId: startRowId,
|
||||
);
|
||||
result.fold(
|
||||
(_) {},
|
||||
(err) => Log.error(err),
|
||||
);
|
||||
|
||||
result.fold((_) {}, (err) => Log.error(err));
|
||||
},
|
||||
createHeaderRow: (String groupId) async {
|
||||
final result = await databaseController.createRow(
|
||||
@ -93,10 +94,7 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
|
||||
fromBeginning: true,
|
||||
);
|
||||
|
||||
result.fold(
|
||||
(_) {},
|
||||
(err) => Log.error(err),
|
||||
);
|
||||
result.fold((_) {}, (err) => Log.error(err));
|
||||
},
|
||||
createGroup: (name) async {
|
||||
final result = await groupBackendSvc.createGroup(name: name);
|
||||
@ -115,6 +113,48 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
|
||||
);
|
||||
_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) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
@ -140,22 +180,6 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
|
||||
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) {
|
||||
emit(
|
||||
state.copyWith(isEditingHeader: true, editingHeaderId: groupId),
|
||||
@ -167,7 +191,6 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
|
||||
groupId: groupId,
|
||||
name: groupName,
|
||||
);
|
||||
|
||||
emit(state.copyWith(isEditingHeader: false));
|
||||
},
|
||||
);
|
||||
@ -178,13 +201,50 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
|
||||
void _groupItemStartEditing(GroupPB group, RowMetaPB row, bool isEdit) {
|
||||
final fieldInfo = fieldController.getField(group.fieldId);
|
||||
if (fieldInfo == null) {
|
||||
Log.warn("fieldInfo should not be null");
|
||||
return;
|
||||
return Log.warn("fieldInfo should not be null");
|
||||
}
|
||||
|
||||
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
|
||||
Future<void> close() async {
|
||||
for (final controller in groupControllers.values) {
|
||||
@ -193,40 +253,45 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
|
||||
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) {
|
||||
for (final controller in groupControllers.values) {
|
||||
controller.dispose();
|
||||
}
|
||||
|
||||
groupControllers.clear();
|
||||
boardController.clear();
|
||||
|
||||
final ungroupedGroupIndex =
|
||||
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);
|
||||
}
|
||||
}
|
||||
groupList.clear();
|
||||
groupList.addAll(groups);
|
||||
|
||||
boardController.addGroups(
|
||||
groups
|
||||
.where((group) => fieldController.getField(group.fieldId) != null)
|
||||
.map((group) => initializeGroupData(group))
|
||||
.where(
|
||||
(group) =>
|
||||
fieldController.getField(group.fieldId) != null &&
|
||||
(group.isVisible || (group.isDefault && !hideUngrouped)),
|
||||
)
|
||||
.map((group) => _initializeGroupData(group))
|
||||
.toList(),
|
||||
);
|
||||
|
||||
for (final group in groups) {
|
||||
final controller = initializeGroupController(group);
|
||||
groupControllers[controller.group.groupId] = (controller);
|
||||
final controller = _initializeGroupController(group);
|
||||
groupControllers[controller.group.groupId] = controller;
|
||||
}
|
||||
}
|
||||
|
||||
RowCache? getRowCache() {
|
||||
return databaseController.rowCache;
|
||||
}
|
||||
RowCache? getRowCache() => databaseController.rowCache;
|
||||
|
||||
void _startListening() {
|
||||
final onDatabaseChanged = DatabaseCallbacks(
|
||||
@ -238,15 +303,22 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
|
||||
);
|
||||
final onLayoutSettingsChanged = DatabaseLayoutSettingCallbacks(
|
||||
onLayoutSettingsChanged: (layoutSettings) {
|
||||
if (isClosed || !layoutSettings.hasBoard()) {
|
||||
if (isClosed) {
|
||||
return;
|
||||
}
|
||||
if (ungroupedGroup != null) {
|
||||
final index = groupList.indexWhere((element) => element.isDefault);
|
||||
if (index != -1) {
|
||||
if (layoutSettings.board.hideUngroupedColumn) {
|
||||
boardController.removeGroup(ungroupedGroup!.fieldId);
|
||||
boardController.removeGroup(groupList[index].fieldId);
|
||||
} else {
|
||||
final newGroup = initializeGroupData(ungroupedGroup!);
|
||||
boardController.addGroup(newGroup);
|
||||
final newGroup = _initializeGroupData(groupList[index]);
|
||||
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));
|
||||
@ -254,30 +326,72 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
|
||||
);
|
||||
final onGroupChanged = GroupCallbacks(
|
||||
onGroupByField: (groups) {
|
||||
if (isClosed) return;
|
||||
ungroupedGroup = null;
|
||||
if (isClosed) {
|
||||
return;
|
||||
}
|
||||
|
||||
initializeGroups(groups);
|
||||
add(BoardEvent.didReceiveGroups(groups));
|
||||
},
|
||||
onDeleteGroup: (groupIds) {
|
||||
if (isClosed) return;
|
||||
if (isClosed) {
|
||||
return;
|
||||
}
|
||||
|
||||
boardController.removeGroups(groupIds);
|
||||
groupList.removeWhere((group) => groupIds.contains(group.groupId));
|
||||
add(BoardEvent.didReceiveGroups(groupList));
|
||||
},
|
||||
onInsertGroup: (insertGroups) {
|
||||
if (isClosed) return;
|
||||
if (isClosed) {
|
||||
return;
|
||||
}
|
||||
|
||||
final group = insertGroups.group;
|
||||
final newGroup = initializeGroupData(group);
|
||||
final controller = initializeGroupController(group);
|
||||
groupControllers[controller.group.groupId] = (controller);
|
||||
final newGroup = _initializeGroupData(group);
|
||||
final controller = _initializeGroupController(group);
|
||||
groupControllers[controller.group.groupId] = controller;
|
||||
boardController.addGroup(newGroup);
|
||||
groupList.insert(insertGroups.index, group);
|
||||
add(BoardEvent.didReceiveGroups(groupList));
|
||||
},
|
||||
onUpdateGroup: (updatedGroups) {
|
||||
if (isClosed) return;
|
||||
if (isClosed) {
|
||||
return;
|
||||
}
|
||||
|
||||
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 =
|
||||
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(
|
||||
controller: boardController,
|
||||
fieldController: fieldController,
|
||||
onNewColumnItem: (groupId, row, index) {
|
||||
add(BoardEvent.didCreateRow(group, row, index));
|
||||
},
|
||||
onNewColumnItem: (groupId, row, index) =>
|
||||
add(BoardEvent.didCreateRow(group, row, index)),
|
||||
);
|
||||
|
||||
final controller = GroupController(
|
||||
viewId: state.viewId,
|
||||
group: group,
|
||||
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(
|
||||
id: group.groupId,
|
||||
name: group.groupName,
|
||||
@ -365,6 +490,14 @@ class BoardEvent with _$BoardEvent {
|
||||
RowMetaPB row,
|
||||
) = _StartEditRow;
|
||||
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.didReceiveGridUpdate(
|
||||
DatabasePB grid,
|
||||
@ -383,12 +516,13 @@ class BoardState with _$BoardState {
|
||||
required Option<DatabasePB> grid,
|
||||
required List<String> groupIds,
|
||||
required bool isEditingHeader,
|
||||
String? editingHeaderId,
|
||||
required bool isEditingRow,
|
||||
BoardEditingRow? editingRow,
|
||||
required LoadingState loadingState,
|
||||
required Option<FlowyError> noneOrError,
|
||||
required BoardLayoutSettingPB? layoutSettings,
|
||||
String? editingHeaderId,
|
||||
BoardEditingRow? editingRow,
|
||||
required List<GroupPB> hiddenGroups,
|
||||
}) = _BoardState;
|
||||
|
||||
factory BoardState.initial(String viewId) => BoardState(
|
||||
@ -400,9 +534,16 @@ class BoardState with _$BoardState {
|
||||
noneOrError: none(),
|
||||
loadingState: const LoadingState.loading(),
|
||||
layoutSettings: null,
|
||||
hiddenGroups: [],
|
||||
);
|
||||
}
|
||||
|
||||
List<GroupPB> _filterHiddenGroups(bool hideUngrouped, List<GroupPB> groups) {
|
||||
return [...groups]..retainWhere(
|
||||
(group) => !group.isVisible || group.isDefault && hideUngrouped,
|
||||
);
|
||||
}
|
||||
|
||||
class GroupItem extends AppFlowyGroupItem {
|
||||
final RowMetaPB row;
|
||||
final FieldInfo fieldInfo;
|
||||
@ -430,12 +571,16 @@ class GroupControllerDelegateImpl extends GroupControllerDelegate {
|
||||
required this.onNewColumnItem,
|
||||
});
|
||||
|
||||
@override
|
||||
bool hasGroup(String groupId) {
|
||||
return controller.groupIds.contains(groupId);
|
||||
}
|
||||
|
||||
@override
|
||||
void insertRow(GroupPB group, RowMetaPB row, int? index) {
|
||||
final fieldInfo = fieldController.getField(group.fieldId);
|
||||
if (fieldInfo == null) {
|
||||
Log.warn("fieldInfo should not be null");
|
||||
return;
|
||||
return Log.warn("fieldInfo should not be null");
|
||||
}
|
||||
|
||||
if (index != null) {
|
||||
@ -454,17 +599,16 @@ class GroupControllerDelegateImpl extends GroupControllerDelegate {
|
||||
}
|
||||
|
||||
@override
|
||||
void removeRow(GroupPB group, RowId rowId) {
|
||||
controller.removeGroupItem(group.groupId, rowId.toString());
|
||||
}
|
||||
void removeRow(GroupPB group, RowId rowId) =>
|
||||
controller.removeGroupItem(group.groupId, rowId.toString());
|
||||
|
||||
@override
|
||||
void updateRow(GroupPB group, RowMetaPB row) {
|
||||
final fieldInfo = fieldController.getField(group.fieldId);
|
||||
if (fieldInfo == null) {
|
||||
Log.warn("fieldInfo should not be null");
|
||||
return;
|
||||
return Log.warn("fieldInfo should not be null");
|
||||
}
|
||||
|
||||
controller.updateGroupItem(
|
||||
group.groupId,
|
||||
GroupItem(
|
||||
@ -478,20 +622,17 @@ class GroupControllerDelegateImpl extends GroupControllerDelegate {
|
||||
void addNewRow(GroupPB group, RowMetaPB row, int? index) {
|
||||
final fieldInfo = fieldController.getField(group.fieldId);
|
||||
if (fieldInfo == null) {
|
||||
Log.warn("fieldInfo should not be null");
|
||||
return;
|
||||
return Log.warn("fieldInfo should not be null");
|
||||
}
|
||||
final item = GroupItem(
|
||||
row: row,
|
||||
fieldInfo: fieldInfo,
|
||||
draggable: false,
|
||||
);
|
||||
|
||||
final item = GroupItem(row: row, fieldInfo: fieldInfo, draggable: false);
|
||||
|
||||
if (index != null) {
|
||||
controller.insertGroupItem(group.groupId, index, item);
|
||||
} else {
|
||||
controller.addGroupItem(group.groupId, item);
|
||||
}
|
||||
|
||||
onNewColumnItem(group.groupId, row, index);
|
||||
}
|
||||
}
|
||||
@ -509,27 +650,26 @@ class BoardEditingRow {
|
||||
}
|
||||
|
||||
class GroupData {
|
||||
final GroupPB group;
|
||||
final FieldInfo fieldInfo;
|
||||
GroupData({
|
||||
required this.group,
|
||||
required this.fieldInfo,
|
||||
});
|
||||
|
||||
CheckboxGroup? asCheckboxGroup() {
|
||||
if (fieldType != FieldType.Checkbox) return null;
|
||||
return CheckboxGroup(group);
|
||||
}
|
||||
final GroupPB group;
|
||||
final FieldInfo fieldInfo;
|
||||
|
||||
CheckboxGroup? asCheckboxGroup() =>
|
||||
fieldType == FieldType.Checkbox ? CheckboxGroup(group) : null;
|
||||
|
||||
FieldType get fieldType => fieldInfo.fieldType;
|
||||
}
|
||||
|
||||
class CheckboxGroup {
|
||||
const CheckboxGroup(this.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";
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -7,10 +7,12 @@ import 'dart:typed_data';
|
||||
import 'package:appflowy/core/notification/grid_notification.dart';
|
||||
import 'package:flowy_infra/notifier.dart';
|
||||
import 'package:dartz/dartz.dart';
|
||||
import 'package:protobuf/protobuf.dart';
|
||||
|
||||
typedef OnGroupError = void Function(FlowyError);
|
||||
|
||||
abstract class GroupControllerDelegate {
|
||||
bool hasGroup(String groupId);
|
||||
void removeRow(GroupPB group, RowId rowId);
|
||||
void insertRow(GroupPB group, RowMetaPB row, int? index);
|
||||
void updateRow(GroupPB group, RowMetaPB row);
|
||||
@ -18,14 +20,16 @@ abstract class GroupControllerDelegate {
|
||||
}
|
||||
|
||||
class GroupController {
|
||||
final GroupPB group;
|
||||
GroupPB group;
|
||||
final SingleGroupListener _listener;
|
||||
final GroupControllerDelegate delegate;
|
||||
final void Function(GroupPB group) onGroupChanged;
|
||||
|
||||
GroupController({
|
||||
required String viewId,
|
||||
required this.group,
|
||||
required this.delegate,
|
||||
required this.onGroupChanged,
|
||||
}) : _listener = SingleGroupListener(group);
|
||||
|
||||
RowMetaPB? rowAtIndex(int index) {
|
||||
@ -46,37 +50,52 @@ class GroupController {
|
||||
onGroupChanged: (result) {
|
||||
result.fold(
|
||||
(GroupRowsNotificationPB changeset) {
|
||||
final newItems = [...group.rows];
|
||||
final isGroupExist = delegate.hasGroup(group.groupId);
|
||||
for (final deletedRow in changeset.deletedRows) {
|
||||
group.rows.removeWhere((rowPB) => rowPB.id == deletedRow);
|
||||
delegate.removeRow(group, deletedRow);
|
||||
newItems.removeWhere((rowPB) => rowPB.id == deletedRow);
|
||||
if (isGroupExist) {
|
||||
delegate.removeRow(group, deletedRow);
|
||||
}
|
||||
}
|
||||
|
||||
for (final insertedRow in changeset.insertedRows) {
|
||||
final index = insertedRow.hasIndex() ? insertedRow.index : null;
|
||||
if (insertedRow.hasIndex() &&
|
||||
group.rows.length > insertedRow.index) {
|
||||
group.rows.insert(insertedRow.index, insertedRow.rowMeta);
|
||||
newItems.length > insertedRow.index) {
|
||||
newItems.insert(insertedRow.index, insertedRow.rowMeta);
|
||||
} else {
|
||||
group.rows.add(insertedRow.rowMeta);
|
||||
newItems.add(insertedRow.rowMeta);
|
||||
}
|
||||
|
||||
if (insertedRow.isNew) {
|
||||
delegate.addNewRow(group, insertedRow.rowMeta, index);
|
||||
} else {
|
||||
delegate.insertRow(group, insertedRow.rowMeta, index);
|
||||
if (isGroupExist) {
|
||||
if (insertedRow.isNew) {
|
||||
delegate.addNewRow(group, insertedRow.rowMeta, index);
|
||||
} else {
|
||||
delegate.insertRow(group, insertedRow.rowMeta, index);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (final updatedRow in changeset.updatedRows) {
|
||||
final index = group.rows.indexWhere(
|
||||
final index = newItems.indexWhere(
|
||||
(rowPB) => rowPB.id == updatedRow.id,
|
||||
);
|
||||
|
||||
if (index != -1) {
|
||||
group.rows[index] = updatedRow;
|
||||
delegate.updateRow(group, updatedRow);
|
||||
newItems[index] = updatedRow;
|
||||
if (isGroupExist) {
|
||||
delegate.updateRow(group, updatedRow);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
group.freeze();
|
||||
group = group.rebuild((group) {
|
||||
group.rows.clear();
|
||||
group.rows.addAll(newItems);
|
||||
});
|
||||
onGroupChanged(group);
|
||||
},
|
||||
(err) => Log.error(err),
|
||||
);
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
@ -1,5 +1,3 @@
|
||||
// ignore_for_file: unused_field
|
||||
|
||||
import 'dart:collection';
|
||||
|
||||
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_controller.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/widgets/row/row_detail.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 '../../widgets/card/card.dart';
|
||||
import 'toolbar/board_setting_bar.dart';
|
||||
import 'ungrouped_items_button.dart';
|
||||
import 'widgets/board_hidden_groups.dart';
|
||||
|
||||
class BoardPageTabBarBuilderImpl implements DatabaseTabBarItemBuilder {
|
||||
@override
|
||||
@ -39,46 +37,38 @@ class BoardPageTabBarBuilderImpl implements DatabaseTabBarItemBuilder {
|
||||
ViewPB view,
|
||||
DatabaseController controller,
|
||||
bool shrinkWrap,
|
||||
) {
|
||||
return BoardPage(
|
||||
key: _makeValueKey(controller),
|
||||
view: view,
|
||||
databaseController: controller,
|
||||
);
|
||||
}
|
||||
) =>
|
||||
BoardPage(view: view, databaseController: controller);
|
||||
|
||||
@override
|
||||
Widget settingBar(BuildContext context, DatabaseController controller) {
|
||||
return BoardSettingBar(
|
||||
key: _makeValueKey(controller),
|
||||
databaseController: controller,
|
||||
);
|
||||
}
|
||||
Widget settingBar(BuildContext context, DatabaseController controller) =>
|
||||
BoardSettingBar(
|
||||
key: _makeValueKey(controller),
|
||||
databaseController: controller,
|
||||
);
|
||||
|
||||
@override
|
||||
Widget settingBarExtension(
|
||||
BuildContext context,
|
||||
DatabaseController controller,
|
||||
) {
|
||||
return SizedBox.fromSize();
|
||||
}
|
||||
) =>
|
||||
const SizedBox.shrink();
|
||||
|
||||
ValueKey _makeValueKey(DatabaseController controller) {
|
||||
return ValueKey(controller.viewId);
|
||||
}
|
||||
ValueKey _makeValueKey(DatabaseController controller) =>
|
||||
ValueKey(controller.viewId);
|
||||
}
|
||||
|
||||
class BoardPage extends StatelessWidget {
|
||||
final DatabaseController databaseController;
|
||||
BoardPage({
|
||||
required this.view,
|
||||
required this.databaseController,
|
||||
Key? key,
|
||||
this.onEditStateChanged,
|
||||
}) : super(key: ValueKey(view.id));
|
||||
|
||||
final ViewPB view;
|
||||
|
||||
final DatabaseController databaseController;
|
||||
|
||||
/// Called when edit state changed
|
||||
final VoidCallback? onEditStateChanged;
|
||||
|
||||
@ -91,23 +81,18 @@ class BoardPage extends StatelessWidget {
|
||||
)..add(const BoardEvent.initial()),
|
||||
child: BlocBuilder<BoardBloc, BoardState>(
|
||||
buildWhen: (p, c) => p.loadingState != c.loadingState,
|
||||
builder: (context, state) {
|
||||
return state.loadingState.map(
|
||||
loading: (_) =>
|
||||
const Center(child: CircularProgressIndicator.adaptive()),
|
||||
finish: (result) {
|
||||
return result.successOrFail.fold(
|
||||
(_) => BoardContent(
|
||||
onEditStateChanged: onEditStateChanged,
|
||||
),
|
||||
(err) => FlowyErrorPage.message(
|
||||
err.toString(),
|
||||
howToFix: LocaleKeys.errorDialog_howToFixFallback.tr(),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
builder: (context, state) => state.loadingState.map(
|
||||
loading: (_) => const Center(
|
||||
child: CircularProgressIndicator.adaptive(),
|
||||
),
|
||||
finish: (result) => result.successOrFail.fold(
|
||||
(_) => BoardContent(onEditStateChanged: onEditStateChanged),
|
||||
(err) => FlowyErrorPage.message(
|
||||
err.toString(),
|
||||
howToFix: LocaleKeys.errorDialog_howToFixFallback.tr(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@ -126,12 +111,14 @@ class BoardContent extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _BoardContentState extends State<BoardContent> {
|
||||
late AppFlowyBoardScrollController scrollManager;
|
||||
late final ScrollController scrollController;
|
||||
final renderHook = RowCardRenderHook<String>();
|
||||
late final ScrollController scrollController;
|
||||
late final AppFlowyBoardScrollController scrollManager;
|
||||
|
||||
final config = const AppFlowyBoardConfig(
|
||||
groupBackgroundColor: Color(0xffF7F8FC),
|
||||
headerPadding: EdgeInsets.symmetric(horizontal: 8),
|
||||
cardPadding: EdgeInsets.symmetric(horizontal: 4, vertical: 3),
|
||||
);
|
||||
|
||||
@override
|
||||
@ -162,43 +149,36 @@ class _BoardContentState extends State<BoardContent> {
|
||||
},
|
||||
child: BlocBuilder<BoardBloc, BoardState>(
|
||||
builder: (context, state) {
|
||||
final showCreateGroupButton =
|
||||
context.read<BoardBloc>().groupingFieldType!.canCreateNewGroup;
|
||||
return Padding(
|
||||
padding: GridSize.contentInsets,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const VSpace(8.0),
|
||||
if (state.layoutSettings?.hideUngroupedColumn ?? false)
|
||||
_buildBoardHeader(context),
|
||||
Expanded(
|
||||
child: AppFlowyBoard(
|
||||
boardScrollController: scrollManager,
|
||||
scrollController: scrollController,
|
||||
controller: context.read<BoardBloc>().boardController,
|
||||
headerBuilder: (_, groupData) =>
|
||||
BlocProvider<BoardBloc>.value(
|
||||
value: context.read<BoardBloc>(),
|
||||
child: BoardColumnHeader(
|
||||
groupData: groupData,
|
||||
margin: config.headerPadding,
|
||||
),
|
||||
),
|
||||
footerBuilder: _buildFooter,
|
||||
trailing: BoardTrailing(scrollController: scrollController),
|
||||
cardBuilder: (_, column, columnItem) => _buildCard(
|
||||
context,
|
||||
column,
|
||||
columnItem,
|
||||
),
|
||||
groupConstraints: const BoxConstraints.tightFor(width: 300),
|
||||
config: AppFlowyBoardConfig(
|
||||
groupBackgroundColor:
|
||||
Theme.of(context).colorScheme.surfaceVariant,
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: AppFlowyBoard(
|
||||
boardScrollController: scrollManager,
|
||||
scrollController: scrollController,
|
||||
controller: context.read<BoardBloc>().boardController,
|
||||
groupConstraints: const BoxConstraints.tightFor(width: 300),
|
||||
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>(),
|
||||
child: BoardColumnHeader(
|
||||
groupData: groupData,
|
||||
margin: config.headerPadding,
|
||||
),
|
||||
),
|
||||
footerBuilder: _buildFooter,
|
||||
cardBuilder: (_, column, columnItem) => _buildCard(
|
||||
context,
|
||||
column,
|
||||
columnItem,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
@ -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) {
|
||||
if (state.isEditingRow && state.editingRow != null) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
@ -231,25 +198,24 @@ class _BoardContentState extends State<BoardContent> {
|
||||
|
||||
Widget _buildFooter(BuildContext context, AppFlowyGroupData columnData) {
|
||||
return AppFlowyGroupFooter(
|
||||
height: 50,
|
||||
margin: config.footerPadding,
|
||||
icon: SizedBox(
|
||||
height: 20,
|
||||
width: 20,
|
||||
child: FlowySvg(
|
||||
FlowySvgs.add_s,
|
||||
color: Theme.of(context).iconTheme.color,
|
||||
color: Theme.of(context).hintColor,
|
||||
),
|
||||
),
|
||||
title: FlowyText.medium(
|
||||
LocaleKeys.board_column_createNewCard.tr(),
|
||||
color: Theme.of(context).hintColor,
|
||||
fontSize: 14,
|
||||
),
|
||||
height: 50,
|
||||
margin: config.footerPadding,
|
||||
onAddButtonClick: () {
|
||||
context.read<BoardBloc>().add(
|
||||
BoardEvent.createBottomRow(columnData.id),
|
||||
);
|
||||
},
|
||||
onAddButtonClick: () => context
|
||||
.read<BoardBloc>()
|
||||
.add(BoardEvent.createBottomRow(columnData.id)),
|
||||
);
|
||||
}
|
||||
|
||||
@ -307,15 +273,27 @@ class _BoardContentState extends State<BoardContent> {
|
||||
}
|
||||
|
||||
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(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
border: isLightMode ? Border.fromBorderSide(borderSide) : null,
|
||||
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(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return RowDetailPage(
|
||||
cellBuilder: GridCellBuilder(cellCache: dataController.cellCache),
|
||||
rowController: dataController,
|
||||
);
|
||||
},
|
||||
builder: (_) => RowDetailPage(
|
||||
cellBuilder: GridCellBuilder(cellCache: dataController.cellCache),
|
||||
rowController: dataController,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class BoardTrailing extends StatefulWidget {
|
||||
const BoardTrailing({super.key, required this.scrollController});
|
||||
|
||||
final ScrollController scrollController;
|
||||
const BoardTrailing({required this.scrollController, super.key});
|
||||
|
||||
@override
|
||||
State<BoardTrailing> createState() => _BoardTrailingState();
|
||||
}
|
||||
|
||||
class _BoardTrailingState extends State<BoardTrailing> {
|
||||
bool isEditing = false;
|
||||
late final TextEditingController _textController;
|
||||
final TextEditingController _textController = TextEditingController();
|
||||
late final FocusNode _focusNode;
|
||||
|
||||
bool isEditing = false;
|
||||
|
||||
void _cancelAddNewGroup() {
|
||||
_textController.clear();
|
||||
setState(() {
|
||||
isEditing = false;
|
||||
});
|
||||
setState(() => isEditing = false);
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_textController = TextEditingController();
|
||||
_focusNode = FocusNode(
|
||||
onKeyEvent: (node, event) {
|
||||
if (_focusNode.hasFocus &&
|
||||
@ -406,7 +381,7 @@ class _BoardTrailingState extends State<BoardTrailing> {
|
||||
});
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(left: 8.0),
|
||||
padding: const EdgeInsets.only(left: 8.0, top: 12),
|
||||
child: Align(
|
||||
alignment: AlignmentDirectional.topStart,
|
||||
child: AnimatedSwitcher(
|
||||
@ -448,9 +423,7 @@ class _BoardTrailingState extends State<BoardTrailing> {
|
||||
width: 26,
|
||||
icon: const FlowySvg(FlowySvgs.add_s),
|
||||
iconColorOnHover: Theme.of(context).colorScheme.onSurface,
|
||||
onPressed: () => setState(() {
|
||||
isEditing = true;
|
||||
}),
|
||||
onPressed: () => setState(() => isEditing = true),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -1,14 +1,15 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.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/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/widgets/card/define.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_popover/appflowy_popover.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/theme_extension.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/hover.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text.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/services.dart';
|
||||
@ -18,11 +19,11 @@ class BoardColumnHeader extends StatefulWidget {
|
||||
const BoardColumnHeader({
|
||||
super.key,
|
||||
required this.groupData,
|
||||
this.margin,
|
||||
required this.margin,
|
||||
});
|
||||
|
||||
final AppFlowyGroupData groupData;
|
||||
final EdgeInsets? margin;
|
||||
final EdgeInsets margin;
|
||||
|
||||
@override
|
||||
State<BoardColumnHeader> createState() => _BoardColumnHeaderState();
|
||||
@ -74,7 +75,7 @@ class _BoardColumnHeaderState extends State<BoardColumnHeader> {
|
||||
child: FlowyText.medium(
|
||||
widget.groupData.headerData.groupName,
|
||||
fontSize: 14,
|
||||
overflow: TextOverflow.clip,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
);
|
||||
|
||||
@ -84,22 +85,16 @@ class _BoardColumnHeaderState extends State<BoardColumnHeader> {
|
||||
fit: FlexFit.tight,
|
||||
child: FlowyTooltip(
|
||||
message: LocaleKeys.board_column_renameGroupTooltip.tr(),
|
||||
child: FlowyHover(
|
||||
style: HoverStyle(
|
||||
hoverColor: Colors.transparent,
|
||||
foregroundColorOnHover:
|
||||
AFThemeExtension.of(context).textColor,
|
||||
),
|
||||
child: MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
child: GestureDetector(
|
||||
onTap: () => context.read<BoardBloc>().add(
|
||||
BoardEvent.startEditingHeader(
|
||||
widget.groupData.id,
|
||||
),
|
||||
),
|
||||
onTap: () => context
|
||||
.read<BoardBloc>()
|
||||
.add(BoardEvent.startEditingHeader(widget.groupData.id)),
|
||||
child: FlowyText.medium(
|
||||
widget.groupData.headerData.groupName,
|
||||
fontSize: 14,
|
||||
overflow: TextOverflow.clip,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -112,22 +107,31 @@ class _BoardColumnHeaderState extends State<BoardColumnHeader> {
|
||||
title = _buildTextField(context);
|
||||
}
|
||||
|
||||
return AppFlowyGroupHeader(
|
||||
title: title,
|
||||
icon: _buildHeaderIcon(boardCustomData),
|
||||
addIcon: SizedBox(
|
||||
height: 20,
|
||||
width: 20,
|
||||
child: FlowySvg(
|
||||
FlowySvgs.add_s,
|
||||
color: Theme.of(context).iconTheme.color,
|
||||
return Padding(
|
||||
padding: widget.margin,
|
||||
child: SizedBox(
|
||||
height: 50,
|
||||
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,
|
||||
icon: const FlowySvg(FlowySvgs.add_s),
|
||||
iconColorOnHover: Theme.of(context).colorScheme.onSurface,
|
||||
onPressed: () => context
|
||||
.read<BoardBloc>()
|
||||
.add(BoardEvent.createHeaderRow(widget.groupData.id)),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
onAddButtonClick: () => context
|
||||
.read<BoardBloc>()
|
||||
.add(BoardEvent.createHeaderRow(widget.groupData.id)),
|
||||
height: 50,
|
||||
margin: widget.margin ?? EdgeInsets.zero,
|
||||
);
|
||||
},
|
||||
);
|
||||
@ -154,7 +158,6 @@ class _BoardColumnHeaderState extends State<BoardColumnHeader> {
|
||||
filled: true,
|
||||
fillColor: Theme.of(context).colorScheme.surface,
|
||||
hoverColor: Colors.transparent,
|
||||
// Magic number 4 makes the textField take up the same space as FlowyText
|
||||
contentPadding: EdgeInsets.symmetric(
|
||||
vertical: CardSizes.cardCellVPadding + 4,
|
||||
horizontal: 8,
|
||||
@ -181,45 +184,93 @@ class _BoardColumnHeaderState extends State<BoardColumnHeader> {
|
||||
);
|
||||
}
|
||||
|
||||
void _saveEdit() {
|
||||
context.read<BoardBloc>().add(
|
||||
BoardEvent.endEditingHeader(
|
||||
widget.groupData.id,
|
||||
_controller.text,
|
||||
void _saveEdit() => context
|
||||
.read<BoardBloc>()
|
||||
.add(BoardEvent.endEditingHeader(widget.groupData.id, _controller.text));
|
||||
|
||||
Widget _buildHeaderIcon(GroupData customData) =>
|
||||
switch (customData.fieldType) {
|
||||
FieldType.Checkbox => FlowySvg(
|
||||
customData.asCheckboxGroup()!.isCheck
|
||||
? FlowySvgs.check_filled_s
|
||||
: FlowySvgs.uncheck_s,
|
||||
blendMode: BlendMode.dst,
|
||||
),
|
||||
_ => const SizedBox.shrink(),
|
||||
};
|
||||
|
||||
Widget _groupOptionsButton(BuildContext context) {
|
||||
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,
|
||||
icon: const FlowySvg(FlowySvgs.details_horizontal_s),
|
||||
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();
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Widget? _buildHeaderIcon(GroupData customData) {
|
||||
Widget? widget;
|
||||
switch (customData.fieldType) {
|
||||
case FieldType.Checkbox:
|
||||
final group = customData.asCheckboxGroup()!;
|
||||
widget = FlowySvg(
|
||||
group.isCheck ? FlowySvgs.check_filled_s : FlowySvgs.uncheck_s,
|
||||
blendMode: BlendMode.dst,
|
||||
);
|
||||
break;
|
||||
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 = SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: widget,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
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(),
|
||||
};
|
||||
}
|
||||
|
@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -21,7 +21,7 @@ class GridHeaderBloc extends Bloc<GridHeaderEvent, GridHeaderState> {
|
||||
on<GridHeaderEvent>(
|
||||
(event, emit) async {
|
||||
await event.map(
|
||||
initial: (_InitialHeader value) async {
|
||||
initial: (_InitialHeader value) {
|
||||
_startListening();
|
||||
add(
|
||||
GridHeaderEvent.didReceiveFieldUpdate(fieldController.fieldInfos),
|
||||
@ -65,7 +65,7 @@ class GridHeaderBloc extends Bloc<GridHeaderEvent, GridHeaderState> {
|
||||
result.fold((l) {}, (err) => Log.error(err));
|
||||
}
|
||||
|
||||
Future<void> _startListening() async {
|
||||
void _startListening() {
|
||||
fieldController.addListener(
|
||||
onReceiveFields: (fields) =>
|
||||
add(GridHeaderEvent.didReceiveFieldUpdate(fields)),
|
||||
|
@ -6,7 +6,7 @@ class GridSize {
|
||||
static double get scrollBarSize => 8 * scale;
|
||||
static double get headerHeight => 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 headerContainerPadding => 0 * scale;
|
||||
static double get cellHPadding => 10 * scale;
|
||||
|
@ -59,4 +59,10 @@ extension FieldTypeListExtension on FieldType {
|
||||
FieldType.SingleSelect => true,
|
||||
_ => false,
|
||||
};
|
||||
|
||||
bool get canCreateNewGroup => switch (this) {
|
||||
FieldType.MultiSelect => true,
|
||||
FieldType.SingleSelect => true,
|
||||
_ => false,
|
||||
};
|
||||
}
|
||||
|
@ -10,7 +10,6 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import '../application/database_controller.dart';
|
||||
import '../grid/presentation/layout/sizes.dart';
|
||||
import 'tab_bar_header.dart';
|
||||
|
||||
abstract class DatabaseTabBarItemBuilder {
|
||||
@ -95,13 +94,11 @@ class _DatabaseTabBarViewState extends State<DatabaseTabBarView> {
|
||||
if (value) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
return SizedBox(
|
||||
return const SizedBox(
|
||||
height: 30,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: GridSize.leadingHeaderPadding,
|
||||
),
|
||||
child: const TabBarHeader(),
|
||||
padding: EdgeInsets.symmetric(horizontal: 40),
|
||||
child: TabBarHeader(),
|
||||
),
|
||||
);
|
||||
},
|
||||
|
@ -239,6 +239,7 @@ class PageManager {
|
||||
shrinkWrap: false,
|
||||
);
|
||||
|
||||
// TODO(Xazin): Board should fill up full width
|
||||
return Padding(
|
||||
padding: builder.contentPadding,
|
||||
child: pluginWidget,
|
||||
|
@ -70,7 +70,7 @@ class FlowyIconButton extends StatelessWidget {
|
||||
shape:
|
||||
RoundedRectangleBorder(borderRadius: radius ?? Corners.s6Border),
|
||||
fillColor: fillColor,
|
||||
hoverColor: hoverColor,
|
||||
hoverColor: Colors.transparent,
|
||||
focusColor: Colors.transparent,
|
||||
splashColor: Colors.transparent,
|
||||
highlightColor: Colors.transparent,
|
||||
@ -79,7 +79,6 @@ class FlowyIconButton extends StatelessWidget {
|
||||
child: FlowyHover(
|
||||
isSelected: isSelected != null ? () => isSelected! : null,
|
||||
style: HoverStyle(
|
||||
// hoverColor is set in both [HoverStyle] and [RawMaterialButton] to avoid the conflicts between two layers
|
||||
hoverColor: hoverColor,
|
||||
foregroundColorOnHover:
|
||||
iconColorOnHover ?? Theme.of(context).iconTheme.color,
|
||||
@ -88,9 +87,7 @@ class FlowyIconButton extends StatelessWidget {
|
||||
resetHoverOnRebuild: false,
|
||||
child: Padding(
|
||||
padding: iconPadding,
|
||||
child: Center(
|
||||
child: child,
|
||||
),
|
||||
child: Center(child: child),
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -100,11 +97,9 @@ class FlowyIconButton extends StatelessWidget {
|
||||
}
|
||||
|
||||
class FlowyDropdownButton extends StatelessWidget {
|
||||
const FlowyDropdownButton({super.key, this.onPressed});
|
||||
|
||||
final VoidCallback? onPressed;
|
||||
const FlowyDropdownButton({
|
||||
Key? key,
|
||||
this.onPressed,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
@ -45,11 +45,11 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "."
|
||||
ref: "1a329c2"
|
||||
resolved-ref: "1a329c21921c0d19871bea3237b7d80fe131f2ed"
|
||||
ref: "2de4fe0"
|
||||
resolved-ref: "2de4fe0b0245dcdf2c2bf43410661c28acbcc687"
|
||||
url: "https://github.com/AppFlowy-IO/appflowy-board.git"
|
||||
source: git
|
||||
version: "0.1.0"
|
||||
version: "0.1.1"
|
||||
appflowy_editor:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -43,7 +43,7 @@ dependencies:
|
||||
# path: packages/appflowy_board
|
||||
git:
|
||||
url: https://github.com/AppFlowy-IO/appflowy-board.git
|
||||
ref: 1a329c2
|
||||
ref: 2de4fe0
|
||||
appflowy_editor:
|
||||
git:
|
||||
url: https://github.com/AppFlowy-IO/appflowy-editor.git
|
||||
|
@ -107,8 +107,8 @@ void main() {
|
||||
|
||||
final groups =
|
||||
boardBloc.groupControllers.values.map((e) => e.group).toList();
|
||||
assert(groups[0].groupName == "B");
|
||||
assert(groups[1].groupName == "A");
|
||||
assert(groups[2].groupName == "No ${multiSelectField.name}");
|
||||
assert(groups[0].groupName == "No ${multiSelectField.name}");
|
||||
assert(groups[1].groupName == "B");
|
||||
assert(groups[2].groupName == "A");
|
||||
});
|
||||
}
|
||||
|
7
frontend/resources/flowy_icons/16x/hamburger_s.svg
Normal file
7
frontend/resources/flowy_icons/16x/hamburger_s.svg
Normal 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 |
@ -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 |
@ -619,9 +619,9 @@
|
||||
"createInlineMathEquation": "Create equation",
|
||||
"fonts": "Fonts",
|
||||
"toggleList": "Toggle list",
|
||||
"quoteList":"Quote list",
|
||||
"numberedList":"Numbered list",
|
||||
"bulletedList":"Bulleted list",
|
||||
"quoteList": "Quote list",
|
||||
"numberedList": "Numbered list",
|
||||
"bulletedList": "Bulleted list",
|
||||
"todoList": "Todo List",
|
||||
"callout": "Callout",
|
||||
"cover": {
|
||||
@ -772,7 +772,15 @@
|
||||
"column": {
|
||||
"createNewCard": "New",
|
||||
"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",
|
||||
"showUngrouped": "Show ungrouped items",
|
||||
@ -1054,4 +1062,4 @@
|
||||
"cardDetails": {
|
||||
"notesPlaceholder": "Enter a / to insert a block, or start typing"
|
||||
}
|
||||
}
|
||||
}
|
@ -786,7 +786,8 @@ async fn hide_group_event_test() {
|
||||
assert!(error.is_none());
|
||||
|
||||
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
|
||||
|
@ -6,12 +6,16 @@ use crate::services::setting::BoardLayoutSetting;
|
||||
pub struct BoardLayoutSettingPB {
|
||||
#[pb(index = 1)]
|
||||
pub hide_ungrouped_column: bool,
|
||||
|
||||
#[pb(index = 2)]
|
||||
pub collapse_hidden_groups: bool,
|
||||
}
|
||||
|
||||
impl From<BoardLayoutSetting> for BoardLayoutSettingPB {
|
||||
fn from(setting: BoardLayoutSetting) -> Self {
|
||||
Self {
|
||||
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 {
|
||||
Self {
|
||||
hide_ungrouped_column: setting.hide_ungrouped_column,
|
||||
collapse_hidden_groups: setting.collapse_hidden_groups,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -312,7 +312,6 @@ impl DatabaseViewEditor {
|
||||
.as_ref()?
|
||||
.get_all_groups()
|
||||
.into_iter()
|
||||
.filter(|group| group.is_visible)
|
||||
.map(|group_data| GroupPB::from(group_data.clone()))
|
||||
.collect::<Vec<_>>();
|
||||
tracing::trace!("Number of groups: {}", groups.len());
|
||||
@ -398,12 +397,16 @@ impl DatabaseViewEditor {
|
||||
|
||||
pub async fn v_update_group(&self, changeset: GroupChangesets) -> FlowyResult<()> {
|
||||
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());
|
||||
type_option_data.extend(controller.apply_group_changeset(&changeset).await?);
|
||||
old_field
|
||||
let (updated_groups, new_type_option) = controller.apply_group_changeset(&changeset).await?;
|
||||
type_option_data.extend(new_type_option);
|
||||
|
||||
(old_field, updated_groups)
|
||||
} else {
|
||||
None
|
||||
(None, vec![])
|
||||
};
|
||||
|
||||
if let Some(old_field) = old_field {
|
||||
@ -413,6 +416,12 @@ impl DatabaseViewEditor {
|
||||
.update_field(&self.view_id, type_option_data, old_field)
|
||||
.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(())
|
||||
|
@ -168,7 +168,7 @@ pub trait GroupControllerOperation: Send + Sync {
|
||||
async fn apply_group_changeset(
|
||||
&mut self,
|
||||
changesets: &GroupChangesets,
|
||||
) -> FlowyResult<TypeOptionData>;
|
||||
) -> FlowyResult<(Vec<GroupPB>, TypeOptionData)>;
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
@ -173,6 +173,7 @@ where
|
||||
self.field.id.clone(),
|
||||
group.name.clone(),
|
||||
group.id.clone(),
|
||||
group.visible,
|
||||
);
|
||||
self.group_by_id.insert(group.id.clone(), group_data);
|
||||
let (index, group_data) = self.get_group(&group.id).unwrap();
|
||||
@ -338,7 +339,13 @@ where
|
||||
.get(&group.id)
|
||||
.cloned()
|
||||
.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);
|
||||
});
|
||||
|
||||
@ -351,6 +358,7 @@ where
|
||||
self.field.id.clone(),
|
||||
group_rev.name,
|
||||
filter_content.clone(),
|
||||
group_rev.visible,
|
||||
);
|
||||
Some(GroupPB::from(group))
|
||||
})
|
||||
|
@ -11,7 +11,8 @@ use serde::Serialize;
|
||||
use flowy_error::FlowyResult;
|
||||
|
||||
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::field::{default_type_option_data_from_type, TypeOption, TypeOptionCellData};
|
||||
@ -45,10 +46,12 @@ pub trait GroupOperationInterceptor {
|
||||
type GroupTypeOption: TypeOption;
|
||||
async fn type_option_from_group_changeset(
|
||||
&self,
|
||||
changeset: &GroupChangeset,
|
||||
type_option: &Self::GroupTypeOption,
|
||||
view_id: &str,
|
||||
) -> Option<TypeOptionData>;
|
||||
_changeset: &GroupChangeset,
|
||||
_type_option: &Self::GroupTypeOption,
|
||||
_view_id: &str,
|
||||
) -> Option<TypeOptionData> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// C: represents the group configuration that impl [GroupConfigurationSerde]
|
||||
@ -396,7 +399,7 @@ where
|
||||
async fn apply_group_changeset(
|
||||
&mut self,
|
||||
changeset: &GroupChangesets,
|
||||
) -> FlowyResult<TypeOptionData> {
|
||||
) -> FlowyResult<(Vec<GroupPB>, TypeOptionData)> {
|
||||
for group_changeset in changeset.changesets.iter() {
|
||||
self.context.update_group(group_changeset)?;
|
||||
}
|
||||
@ -410,7 +413,16 @@ where
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
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 serde::{Deserialize, Serialize};
|
||||
|
||||
@ -12,8 +12,8 @@ use crate::services::group::action::GroupCustomize;
|
||||
use crate::services::group::configuration::GroupContext;
|
||||
use crate::services::group::controller::{BaseGroupController, GroupController};
|
||||
use crate::services::group::{
|
||||
move_group_row, GeneratedGroupConfig, GeneratedGroups, Group, GroupChangeset,
|
||||
GroupOperationInterceptor, GroupsBuilder, MoveGroupRowContext,
|
||||
move_group_row, GeneratedGroupConfig, GeneratedGroups, Group, GroupOperationInterceptor,
|
||||
GroupsBuilder, MoveGroupRowContext,
|
||||
};
|
||||
|
||||
#[derive(Default, Serialize, Deserialize)]
|
||||
@ -190,12 +190,4 @@ pub struct CheckboxGroupOperationInterceptorImpl {}
|
||||
#[async_trait]
|
||||
impl GroupOperationInterceptor for CheckboxGroupOperationInterceptorImpl {
|
||||
type GroupTypeOption = CheckboxTypeOption;
|
||||
async fn type_option_from_group_changeset(
|
||||
&self,
|
||||
_changeset: &GroupChangeset,
|
||||
_type_option: &Self::GroupTypeOption,
|
||||
_view_id: &str,
|
||||
) -> Option<TypeOptionData> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
@ -7,7 +7,7 @@ use chrono::{
|
||||
};
|
||||
use chrono_tz::Tz;
|
||||
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 serde::{Deserialize, Serialize};
|
||||
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::{
|
||||
make_no_status_group, move_group_row, GeneratedGroupConfig, GeneratedGroups, Group,
|
||||
GroupChangeset, GroupOperationInterceptor, GroupsBuilder, MoveGroupRowContext,
|
||||
GroupOperationInterceptor, GroupsBuilder, MoveGroupRowContext,
|
||||
};
|
||||
|
||||
pub trait GroupConfigurationContentSerde: Sized + Send + Sync {
|
||||
@ -458,14 +458,6 @@ pub struct DateGroupOperationInterceptorImpl {}
|
||||
#[async_trait]
|
||||
impl GroupOperationInterceptor for DateGroupOperationInterceptorImpl {
|
||||
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)]
|
||||
|
@ -6,7 +6,9 @@ use collab_database::rows::{Cells, Row, RowDetail};
|
||||
|
||||
use flowy_error::FlowyResult;
|
||||
|
||||
use crate::entities::{GroupChangesPB, GroupRowsNotificationPB, InsertedGroupPB, InsertedRowPB};
|
||||
use crate::entities::{
|
||||
GroupChangesPB, GroupPB, GroupRowsNotificationPB, InsertedGroupPB, InsertedRowPB,
|
||||
};
|
||||
use crate::services::group::action::{
|
||||
DidMoveGroupRowResult, DidUpdateGroupRowResult, GroupControllerOperation,
|
||||
};
|
||||
@ -30,6 +32,7 @@ impl DefaultGroupController {
|
||||
field.id.clone(),
|
||||
"".to_owned(),
|
||||
"".to_owned(),
|
||||
true,
|
||||
);
|
||||
Self {
|
||||
field_id: field.id.clone(),
|
||||
@ -129,8 +132,8 @@ impl GroupControllerOperation for DefaultGroupController {
|
||||
async fn apply_group_changeset(
|
||||
&mut self,
|
||||
_changeset: &GroupChangesets,
|
||||
) -> FlowyResult<TypeOptionData> {
|
||||
Ok(TypeOptionData::default())
|
||||
) -> FlowyResult<(Vec<GroupPB>, TypeOptionData)> {
|
||||
Ok((Vec::new(), TypeOptionData::default()))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
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 serde::{Deserialize, Serialize};
|
||||
|
||||
@ -17,8 +17,7 @@ use crate::services::group::configuration::GroupContext;
|
||||
use crate::services::group::controller::{BaseGroupController, GroupController};
|
||||
use crate::services::group::{
|
||||
make_no_status_group, move_group_row, GeneratedGroupConfig, GeneratedGroups, Group,
|
||||
GroupChangeset, GroupOperationInterceptor, GroupTypeOptionCellOperation, GroupsBuilder,
|
||||
MoveGroupRowContext,
|
||||
GroupOperationInterceptor, GroupTypeOptionCellOperation, GroupsBuilder, MoveGroupRowContext,
|
||||
};
|
||||
|
||||
#[derive(Default, Serialize, Deserialize)]
|
||||
@ -250,12 +249,4 @@ pub struct URLGroupOperationInterceptorImpl {
|
||||
#[async_trait::async_trait]
|
||||
impl GroupOperationInterceptor for URLGroupOperationInterceptorImpl {
|
||||
type GroupTypeOption = URLTypeOption;
|
||||
async fn type_option_from_group_changeset(
|
||||
&self,
|
||||
_changeset: &GroupChangeset,
|
||||
_type_option: &Self::GroupTypeOption,
|
||||
_view_id: &str,
|
||||
) -> Option<TypeOptionData> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
@ -148,13 +148,19 @@ pub struct 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;
|
||||
Self {
|
||||
id,
|
||||
field_id,
|
||||
is_default,
|
||||
is_visible: true,
|
||||
is_visible,
|
||||
name,
|
||||
rows: vec![],
|
||||
filter_content,
|
||||
|
@ -93,6 +93,7 @@ pub const DEFAULT_SHOW_WEEK_NUMBERS: bool = true;
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct BoardLayoutSetting {
|
||||
pub hide_ungrouped_column: bool,
|
||||
pub collapse_hidden_groups: bool,
|
||||
}
|
||||
|
||||
impl BoardLayoutSetting {
|
||||
@ -107,6 +108,9 @@ impl From<LayoutSetting> for BoardLayoutSetting {
|
||||
hide_ungrouped_column: setting
|
||||
.get_bool_value("hide_ungrouped_column")
|
||||
.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 {
|
||||
LayoutSettingBuilder::new()
|
||||
.insert_bool_value("hide_ungrouped_column", setting.hide_ungrouped_column)
|
||||
.insert_bool_value("collapse_hidden_groups", setting.collapse_hidden_groups)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
@ -11,6 +11,7 @@ async fn board_layout_setting_test() {
|
||||
let default_board_setting = BoardLayoutSetting::new();
|
||||
let new_board_setting = BoardLayoutSetting {
|
||||
hide_ungrouped_column: true,
|
||||
..default_board_setting
|
||||
};
|
||||
let scripts = vec![
|
||||
AssertBoardLayoutSetting {
|
||||
|
Loading…
Reference in New Issue
Block a user