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