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:
@ -6,6 +6,7 @@ import 'package:appflowy/plugins/database_view/application/field/field_info.dart
|
||||
import 'package:appflowy/plugins/database_view/application/group/group_service.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/row/row_service.dart';
|
||||
import 'package:appflowy_board/appflowy_board.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:dartz/dartz.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||
@ -14,6 +15,7 @@ import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:protobuf/protobuf.dart' hide FieldInfo;
|
||||
|
||||
import '../../application/field/field_controller.dart';
|
||||
import '../../application/row/row_cache.dart';
|
||||
@ -23,12 +25,13 @@ import 'group_controller.dart';
|
||||
part 'board_bloc.freezed.dart';
|
||||
|
||||
class BoardBloc extends Bloc<BoardEvent, BoardState> {
|
||||
late final GroupBackendService groupBackendSvc;
|
||||
final DatabaseController databaseController;
|
||||
late final AppFlowyBoardController boardController;
|
||||
final LinkedHashMap<String, GroupController> groupControllers =
|
||||
LinkedHashMap();
|
||||
GroupPB? ungroupedGroup;
|
||||
final List<GroupPB> groupList = [];
|
||||
|
||||
late final GroupBackendService groupBackendSvc;
|
||||
late final AppFlowyBoardController boardController;
|
||||
|
||||
FieldController get fieldController => databaseController.fieldController;
|
||||
String get viewId => databaseController.viewId;
|
||||
@ -82,10 +85,8 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
|
||||
groupId: groupId,
|
||||
startRowId: startRowId,
|
||||
);
|
||||
result.fold(
|
||||
(_) {},
|
||||
(err) => Log.error(err),
|
||||
);
|
||||
|
||||
result.fold((_) {}, (err) => Log.error(err));
|
||||
},
|
||||
createHeaderRow: (String groupId) async {
|
||||
final result = await databaseController.createRow(
|
||||
@ -93,10 +94,7 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
|
||||
fromBeginning: true,
|
||||
);
|
||||
|
||||
result.fold(
|
||||
(_) {},
|
||||
(err) => Log.error(err),
|
||||
);
|
||||
result.fold((_) {}, (err) => Log.error(err));
|
||||
},
|
||||
createGroup: (name) async {
|
||||
final result = await groupBackendSvc.createGroup(name: name);
|
||||
@ -115,6 +113,48 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
|
||||
);
|
||||
_groupItemStartEditing(group, row, true);
|
||||
},
|
||||
didReceiveGridUpdate: (DatabasePB grid) {
|
||||
emit(state.copyWith(grid: Some(grid)));
|
||||
},
|
||||
didReceiveError: (FlowyError error) {
|
||||
emit(state.copyWith(noneOrError: some(error)));
|
||||
},
|
||||
didReceiveGroups: (List<GroupPB> groups) {
|
||||
final hiddenGroups = _filterHiddenGroups(hideUngrouped, groups);
|
||||
emit(
|
||||
state.copyWith(
|
||||
hiddenGroups: hiddenGroups,
|
||||
groupIds: groups.map((group) => group.groupId).toList(),
|
||||
),
|
||||
);
|
||||
},
|
||||
didUpdateLayoutSettings: (layoutSettings) {
|
||||
final hiddenGroups = _filterHiddenGroups(hideUngrouped, groupList);
|
||||
emit(
|
||||
state.copyWith(
|
||||
layoutSettings: layoutSettings,
|
||||
hiddenGroups: hiddenGroups,
|
||||
),
|
||||
);
|
||||
},
|
||||
toggleGroupVisibility: (GroupPB group, bool isVisible) async {
|
||||
await _toggleGroupVisibility(group, isVisible);
|
||||
},
|
||||
toggleHiddenSectionVisibility: (isVisible) async {
|
||||
final newLayoutSettings = state.layoutSettings!;
|
||||
newLayoutSettings.freeze();
|
||||
|
||||
final newLayoutSetting = newLayoutSettings.rebuild(
|
||||
(message) => message.collapseHiddenGroups = isVisible,
|
||||
);
|
||||
|
||||
await databaseController.updateLayoutSetting(
|
||||
boardLayoutSetting: newLayoutSetting,
|
||||
);
|
||||
},
|
||||
reorderGroup: (fromGroupId, toGroupId) async {
|
||||
_reorderGroup(fromGroupId, toGroupId, emit);
|
||||
},
|
||||
startEditingRow: (group, row) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
@ -140,22 +180,6 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
|
||||
emit(state.copyWith(isEditingRow: false, editingRow: null));
|
||||
}
|
||||
},
|
||||
didReceiveGridUpdate: (DatabasePB grid) {
|
||||
emit(state.copyWith(grid: Some(grid)));
|
||||
},
|
||||
didReceiveError: (FlowyError error) {
|
||||
emit(state.copyWith(noneOrError: some(error)));
|
||||
},
|
||||
didReceiveGroups: (List<GroupPB> groups) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
groupIds: groups.map((group) => group.groupId).toList(),
|
||||
),
|
||||
);
|
||||
},
|
||||
didUpdateLayoutSettings: (layoutSettings) {
|
||||
emit(state.copyWith(layoutSettings: layoutSettings));
|
||||
},
|
||||
startEditingHeader: (String groupId) {
|
||||
emit(
|
||||
state.copyWith(isEditingHeader: true, editingHeaderId: groupId),
|
||||
@ -167,7 +191,6 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
|
||||
groupId: groupId,
|
||||
name: groupName,
|
||||
);
|
||||
|
||||
emit(state.copyWith(isEditingHeader: false));
|
||||
},
|
||||
);
|
||||
@ -178,13 +201,50 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
|
||||
void _groupItemStartEditing(GroupPB group, RowMetaPB row, bool isEdit) {
|
||||
final fieldInfo = fieldController.getField(group.fieldId);
|
||||
if (fieldInfo == null) {
|
||||
Log.warn("fieldInfo should not be null");
|
||||
return;
|
||||
return Log.warn("fieldInfo should not be null");
|
||||
}
|
||||
|
||||
boardController.enableGroupDragging(!isEdit);
|
||||
}
|
||||
|
||||
Future<void> _toggleGroupVisibility(GroupPB group, bool isVisible) async {
|
||||
if (group.isDefault) {
|
||||
final newLayoutSettings = state.layoutSettings!;
|
||||
newLayoutSettings.freeze();
|
||||
|
||||
final newLayoutSetting = newLayoutSettings.rebuild(
|
||||
(message) => message.hideUngroupedColumn = !isVisible,
|
||||
);
|
||||
|
||||
return databaseController.updateLayoutSetting(
|
||||
boardLayoutSetting: newLayoutSetting,
|
||||
);
|
||||
}
|
||||
|
||||
await groupBackendSvc.updateGroup(
|
||||
fieldId: groupControllers.values.first.group.fieldId,
|
||||
groupId: group.groupId,
|
||||
visible: isVisible,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _reorderGroup(
|
||||
String fromGroupId,
|
||||
String toGroupId,
|
||||
Emitter<BoardState> emit,
|
||||
) async {
|
||||
final fromIndex = groupList.indexWhere((g) => g.groupId == fromGroupId);
|
||||
final toIndex = groupList.indexWhere((g) => g.groupId == toGroupId);
|
||||
final group = groupList.removeAt(fromIndex);
|
||||
groupList.insert(toIndex, group);
|
||||
add(BoardEvent.didReceiveGroups(groupList));
|
||||
final result = await databaseController.moveGroup(
|
||||
fromGroupId: fromGroupId,
|
||||
toGroupId: toGroupId,
|
||||
);
|
||||
result.fold((l) => {}, (err) => Log.error(err));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() async {
|
||||
for (final controller in groupControllers.values) {
|
||||
@ -193,40 +253,45 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
|
||||
return super.close();
|
||||
}
|
||||
|
||||
bool get hideUngrouped =>
|
||||
databaseController.databaseLayoutSetting?.board.hideUngroupedColumn ??
|
||||
false;
|
||||
|
||||
FieldType? get groupingFieldType {
|
||||
final fieldInfo = databaseController.fieldController.fieldInfos
|
||||
.firstWhereOrNull((field) => field.isGroupField);
|
||||
|
||||
return fieldInfo?.fieldType;
|
||||
}
|
||||
|
||||
void initializeGroups(List<GroupPB> groups) {
|
||||
for (final controller in groupControllers.values) {
|
||||
controller.dispose();
|
||||
}
|
||||
|
||||
groupControllers.clear();
|
||||
boardController.clear();
|
||||
|
||||
final ungroupedGroupIndex =
|
||||
groups.indexWhere((group) => group.groupId == group.fieldId);
|
||||
|
||||
if (ungroupedGroupIndex != -1) {
|
||||
ungroupedGroup = groups[ungroupedGroupIndex];
|
||||
final group = groups.removeAt(ungroupedGroupIndex);
|
||||
if (!(state.layoutSettings?.hideUngroupedColumn ?? false)) {
|
||||
groups.add(group);
|
||||
}
|
||||
}
|
||||
groupList.clear();
|
||||
groupList.addAll(groups);
|
||||
|
||||
boardController.addGroups(
|
||||
groups
|
||||
.where((group) => fieldController.getField(group.fieldId) != null)
|
||||
.map((group) => initializeGroupData(group))
|
||||
.where(
|
||||
(group) =>
|
||||
fieldController.getField(group.fieldId) != null &&
|
||||
(group.isVisible || (group.isDefault && !hideUngrouped)),
|
||||
)
|
||||
.map((group) => _initializeGroupData(group))
|
||||
.toList(),
|
||||
);
|
||||
|
||||
for (final group in groups) {
|
||||
final controller = initializeGroupController(group);
|
||||
groupControllers[controller.group.groupId] = (controller);
|
||||
final controller = _initializeGroupController(group);
|
||||
groupControllers[controller.group.groupId] = controller;
|
||||
}
|
||||
}
|
||||
|
||||
RowCache? getRowCache() {
|
||||
return databaseController.rowCache;
|
||||
}
|
||||
RowCache? getRowCache() => databaseController.rowCache;
|
||||
|
||||
void _startListening() {
|
||||
final onDatabaseChanged = DatabaseCallbacks(
|
||||
@ -238,15 +303,22 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
|
||||
);
|
||||
final onLayoutSettingsChanged = DatabaseLayoutSettingCallbacks(
|
||||
onLayoutSettingsChanged: (layoutSettings) {
|
||||
if (isClosed || !layoutSettings.hasBoard()) {
|
||||
if (isClosed) {
|
||||
return;
|
||||
}
|
||||
if (ungroupedGroup != null) {
|
||||
final index = groupList.indexWhere((element) => element.isDefault);
|
||||
if (index != -1) {
|
||||
if (layoutSettings.board.hideUngroupedColumn) {
|
||||
boardController.removeGroup(ungroupedGroup!.fieldId);
|
||||
boardController.removeGroup(groupList[index].fieldId);
|
||||
} else {
|
||||
final newGroup = initializeGroupData(ungroupedGroup!);
|
||||
boardController.addGroup(newGroup);
|
||||
final newGroup = _initializeGroupData(groupList[index]);
|
||||
final visibleGroups = [...groupList]
|
||||
..retainWhere((g) => g.isVisible || g.isDefault);
|
||||
final indexInVisibleGroups =
|
||||
visibleGroups.indexWhere((g) => g.isDefault);
|
||||
if (indexInVisibleGroups != -1) {
|
||||
boardController.insertGroup(indexInVisibleGroups, newGroup);
|
||||
}
|
||||
}
|
||||
}
|
||||
add(BoardEvent.didUpdateLayoutSettings(layoutSettings.board));
|
||||
@ -254,30 +326,72 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
|
||||
);
|
||||
final onGroupChanged = GroupCallbacks(
|
||||
onGroupByField: (groups) {
|
||||
if (isClosed) return;
|
||||
ungroupedGroup = null;
|
||||
if (isClosed) {
|
||||
return;
|
||||
}
|
||||
|
||||
initializeGroups(groups);
|
||||
add(BoardEvent.didReceiveGroups(groups));
|
||||
},
|
||||
onDeleteGroup: (groupIds) {
|
||||
if (isClosed) return;
|
||||
if (isClosed) {
|
||||
return;
|
||||
}
|
||||
|
||||
boardController.removeGroups(groupIds);
|
||||
groupList.removeWhere((group) => groupIds.contains(group.groupId));
|
||||
add(BoardEvent.didReceiveGroups(groupList));
|
||||
},
|
||||
onInsertGroup: (insertGroups) {
|
||||
if (isClosed) return;
|
||||
if (isClosed) {
|
||||
return;
|
||||
}
|
||||
|
||||
final group = insertGroups.group;
|
||||
final newGroup = initializeGroupData(group);
|
||||
final controller = initializeGroupController(group);
|
||||
groupControllers[controller.group.groupId] = (controller);
|
||||
final newGroup = _initializeGroupData(group);
|
||||
final controller = _initializeGroupController(group);
|
||||
groupControllers[controller.group.groupId] = controller;
|
||||
boardController.addGroup(newGroup);
|
||||
groupList.insert(insertGroups.index, group);
|
||||
add(BoardEvent.didReceiveGroups(groupList));
|
||||
},
|
||||
onUpdateGroup: (updatedGroups) {
|
||||
if (isClosed) return;
|
||||
if (isClosed) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (final group in updatedGroups) {
|
||||
// see if the column is already in the board
|
||||
|
||||
final index = groupList.indexWhere((g) => g.groupId == group.groupId);
|
||||
if (index == -1) continue;
|
||||
final columnController =
|
||||
boardController.getGroupController(group.groupId);
|
||||
columnController?.updateGroupName(group.groupName);
|
||||
if (columnController != null) {
|
||||
// remove the group or update its name
|
||||
columnController.updateGroupName(group.groupName);
|
||||
if (!group.isVisible) {
|
||||
boardController.removeGroup(group.groupId);
|
||||
}
|
||||
} else {
|
||||
final newGroup = _initializeGroupData(group);
|
||||
final visibleGroups = [...groupList]..retainWhere(
|
||||
(g) =>
|
||||
g.isVisible ||
|
||||
g.isDefault && !hideUngrouped ||
|
||||
g.groupId == group.groupId,
|
||||
);
|
||||
final indexInVisibleGroups =
|
||||
visibleGroups.indexWhere((g) => g.groupId == group.groupId);
|
||||
if (indexInVisibleGroups != -1) {
|
||||
boardController.insertGroup(indexInVisibleGroups, newGroup);
|
||||
}
|
||||
}
|
||||
|
||||
groupList.removeAt(index);
|
||||
groupList.insert(index, group);
|
||||
}
|
||||
add(BoardEvent.didReceiveGroups(groupList));
|
||||
},
|
||||
);
|
||||
|
||||
@ -315,24 +429,35 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
|
||||
);
|
||||
}
|
||||
|
||||
GroupController initializeGroupController(GroupPB group) {
|
||||
GroupController _initializeGroupController(GroupPB group) {
|
||||
final delegate = GroupControllerDelegateImpl(
|
||||
controller: boardController,
|
||||
fieldController: fieldController,
|
||||
onNewColumnItem: (groupId, row, index) {
|
||||
add(BoardEvent.didCreateRow(group, row, index));
|
||||
},
|
||||
onNewColumnItem: (groupId, row, index) =>
|
||||
add(BoardEvent.didCreateRow(group, row, index)),
|
||||
);
|
||||
|
||||
final controller = GroupController(
|
||||
viewId: state.viewId,
|
||||
group: group,
|
||||
delegate: delegate,
|
||||
onGroupChanged: (newGroup) {
|
||||
if (isClosed) return;
|
||||
|
||||
final index =
|
||||
groupList.indexWhere((g) => g.groupId == newGroup.groupId);
|
||||
if (index != -1) {
|
||||
groupList.removeAt(index);
|
||||
groupList.insert(index, newGroup);
|
||||
add(BoardEvent.didReceiveGroups(groupList));
|
||||
}
|
||||
},
|
||||
);
|
||||
controller.startListening();
|
||||
return controller;
|
||||
|
||||
return controller..startListening();
|
||||
}
|
||||
|
||||
AppFlowyGroupData initializeGroupData(GroupPB group) {
|
||||
AppFlowyGroupData _initializeGroupData(GroupPB group) {
|
||||
return AppFlowyGroupData(
|
||||
id: group.groupId,
|
||||
name: group.groupName,
|
||||
@ -365,6 +490,14 @@ class BoardEvent with _$BoardEvent {
|
||||
RowMetaPB row,
|
||||
) = _StartEditRow;
|
||||
const factory BoardEvent.endEditingRow(RowId rowId) = _EndEditRow;
|
||||
const factory BoardEvent.toggleGroupVisibility(
|
||||
GroupPB group,
|
||||
bool isVisible,
|
||||
) = _ToggleGroupVisibility;
|
||||
const factory BoardEvent.toggleHiddenSectionVisibility(bool isVisible) =
|
||||
_ToggleHiddenSectionVisibility;
|
||||
const factory BoardEvent.reorderGroup(String fromGroupId, String toGroupId) =
|
||||
_ReorderGroup;
|
||||
const factory BoardEvent.didReceiveError(FlowyError error) = _DidReceiveError;
|
||||
const factory BoardEvent.didReceiveGridUpdate(
|
||||
DatabasePB grid,
|
||||
@ -383,12 +516,13 @@ class BoardState with _$BoardState {
|
||||
required Option<DatabasePB> grid,
|
||||
required List<String> groupIds,
|
||||
required bool isEditingHeader,
|
||||
String? editingHeaderId,
|
||||
required bool isEditingRow,
|
||||
BoardEditingRow? editingRow,
|
||||
required LoadingState loadingState,
|
||||
required Option<FlowyError> noneOrError,
|
||||
required BoardLayoutSettingPB? layoutSettings,
|
||||
String? editingHeaderId,
|
||||
BoardEditingRow? editingRow,
|
||||
required List<GroupPB> hiddenGroups,
|
||||
}) = _BoardState;
|
||||
|
||||
factory BoardState.initial(String viewId) => BoardState(
|
||||
@ -400,9 +534,16 @@ class BoardState with _$BoardState {
|
||||
noneOrError: none(),
|
||||
loadingState: const LoadingState.loading(),
|
||||
layoutSettings: null,
|
||||
hiddenGroups: [],
|
||||
);
|
||||
}
|
||||
|
||||
List<GroupPB> _filterHiddenGroups(bool hideUngrouped, List<GroupPB> groups) {
|
||||
return [...groups]..retainWhere(
|
||||
(group) => !group.isVisible || group.isDefault && hideUngrouped,
|
||||
);
|
||||
}
|
||||
|
||||
class GroupItem extends AppFlowyGroupItem {
|
||||
final RowMetaPB row;
|
||||
final FieldInfo fieldInfo;
|
||||
@ -430,12 +571,16 @@ class GroupControllerDelegateImpl extends GroupControllerDelegate {
|
||||
required this.onNewColumnItem,
|
||||
});
|
||||
|
||||
@override
|
||||
bool hasGroup(String groupId) {
|
||||
return controller.groupIds.contains(groupId);
|
||||
}
|
||||
|
||||
@override
|
||||
void insertRow(GroupPB group, RowMetaPB row, int? index) {
|
||||
final fieldInfo = fieldController.getField(group.fieldId);
|
||||
if (fieldInfo == null) {
|
||||
Log.warn("fieldInfo should not be null");
|
||||
return;
|
||||
return Log.warn("fieldInfo should not be null");
|
||||
}
|
||||
|
||||
if (index != null) {
|
||||
@ -454,17 +599,16 @@ class GroupControllerDelegateImpl extends GroupControllerDelegate {
|
||||
}
|
||||
|
||||
@override
|
||||
void removeRow(GroupPB group, RowId rowId) {
|
||||
controller.removeGroupItem(group.groupId, rowId.toString());
|
||||
}
|
||||
void removeRow(GroupPB group, RowId rowId) =>
|
||||
controller.removeGroupItem(group.groupId, rowId.toString());
|
||||
|
||||
@override
|
||||
void updateRow(GroupPB group, RowMetaPB row) {
|
||||
final fieldInfo = fieldController.getField(group.fieldId);
|
||||
if (fieldInfo == null) {
|
||||
Log.warn("fieldInfo should not be null");
|
||||
return;
|
||||
return Log.warn("fieldInfo should not be null");
|
||||
}
|
||||
|
||||
controller.updateGroupItem(
|
||||
group.groupId,
|
||||
GroupItem(
|
||||
@ -478,20 +622,17 @@ class GroupControllerDelegateImpl extends GroupControllerDelegate {
|
||||
void addNewRow(GroupPB group, RowMetaPB row, int? index) {
|
||||
final fieldInfo = fieldController.getField(group.fieldId);
|
||||
if (fieldInfo == null) {
|
||||
Log.warn("fieldInfo should not be null");
|
||||
return;
|
||||
return Log.warn("fieldInfo should not be null");
|
||||
}
|
||||
final item = GroupItem(
|
||||
row: row,
|
||||
fieldInfo: fieldInfo,
|
||||
draggable: false,
|
||||
);
|
||||
|
||||
final item = GroupItem(row: row, fieldInfo: fieldInfo, draggable: false);
|
||||
|
||||
if (index != null) {
|
||||
controller.insertGroupItem(group.groupId, index, item);
|
||||
} else {
|
||||
controller.addGroupItem(group.groupId, item);
|
||||
}
|
||||
|
||||
onNewColumnItem(group.groupId, row, index);
|
||||
}
|
||||
}
|
||||
@ -509,27 +650,26 @@ class BoardEditingRow {
|
||||
}
|
||||
|
||||
class GroupData {
|
||||
final GroupPB group;
|
||||
final FieldInfo fieldInfo;
|
||||
GroupData({
|
||||
required this.group,
|
||||
required this.fieldInfo,
|
||||
});
|
||||
|
||||
CheckboxGroup? asCheckboxGroup() {
|
||||
if (fieldType != FieldType.Checkbox) return null;
|
||||
return CheckboxGroup(group);
|
||||
}
|
||||
final GroupPB group;
|
||||
final FieldInfo fieldInfo;
|
||||
|
||||
CheckboxGroup? asCheckboxGroup() =>
|
||||
fieldType == FieldType.Checkbox ? CheckboxGroup(group) : null;
|
||||
|
||||
FieldType get fieldType => fieldInfo.fieldType;
|
||||
}
|
||||
|
||||
class CheckboxGroup {
|
||||
const CheckboxGroup(this.group);
|
||||
|
||||
final GroupPB group;
|
||||
|
||||
CheckboxGroup(this.group);
|
||||
|
||||
// Hardcode value: "Yes" that equal to the value defined in Rust
|
||||
// pub const CHECK: &str = "Yes";
|
||||
// Hardcode value: "Yes" that equal to the value defined in Rust
|
||||
// pub const CHECK: &str = "Yes";
|
||||
bool get isCheck => group.groupId == "Yes";
|
||||
}
|
||||
|
@ -1,12 +0,0 @@
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
|
||||
|
||||
class BoardGroupService {
|
||||
final String viewId;
|
||||
FieldPB? groupField;
|
||||
|
||||
BoardGroupService(this.viewId);
|
||||
|
||||
void setGroupField(FieldPB field) {
|
||||
groupField = field;
|
||||
}
|
||||
}
|
@ -7,10 +7,12 @@ import 'dart:typed_data';
|
||||
import 'package:appflowy/core/notification/grid_notification.dart';
|
||||
import 'package:flowy_infra/notifier.dart';
|
||||
import 'package:dartz/dartz.dart';
|
||||
import 'package:protobuf/protobuf.dart';
|
||||
|
||||
typedef OnGroupError = void Function(FlowyError);
|
||||
|
||||
abstract class GroupControllerDelegate {
|
||||
bool hasGroup(String groupId);
|
||||
void removeRow(GroupPB group, RowId rowId);
|
||||
void insertRow(GroupPB group, RowMetaPB row, int? index);
|
||||
void updateRow(GroupPB group, RowMetaPB row);
|
||||
@ -18,14 +20,16 @@ abstract class GroupControllerDelegate {
|
||||
}
|
||||
|
||||
class GroupController {
|
||||
final GroupPB group;
|
||||
GroupPB group;
|
||||
final SingleGroupListener _listener;
|
||||
final GroupControllerDelegate delegate;
|
||||
final void Function(GroupPB group) onGroupChanged;
|
||||
|
||||
GroupController({
|
||||
required String viewId,
|
||||
required this.group,
|
||||
required this.delegate,
|
||||
required this.onGroupChanged,
|
||||
}) : _listener = SingleGroupListener(group);
|
||||
|
||||
RowMetaPB? rowAtIndex(int index) {
|
||||
@ -46,37 +50,52 @@ class GroupController {
|
||||
onGroupChanged: (result) {
|
||||
result.fold(
|
||||
(GroupRowsNotificationPB changeset) {
|
||||
final newItems = [...group.rows];
|
||||
final isGroupExist = delegate.hasGroup(group.groupId);
|
||||
for (final deletedRow in changeset.deletedRows) {
|
||||
group.rows.removeWhere((rowPB) => rowPB.id == deletedRow);
|
||||
delegate.removeRow(group, deletedRow);
|
||||
newItems.removeWhere((rowPB) => rowPB.id == deletedRow);
|
||||
if (isGroupExist) {
|
||||
delegate.removeRow(group, deletedRow);
|
||||
}
|
||||
}
|
||||
|
||||
for (final insertedRow in changeset.insertedRows) {
|
||||
final index = insertedRow.hasIndex() ? insertedRow.index : null;
|
||||
if (insertedRow.hasIndex() &&
|
||||
group.rows.length > insertedRow.index) {
|
||||
group.rows.insert(insertedRow.index, insertedRow.rowMeta);
|
||||
newItems.length > insertedRow.index) {
|
||||
newItems.insert(insertedRow.index, insertedRow.rowMeta);
|
||||
} else {
|
||||
group.rows.add(insertedRow.rowMeta);
|
||||
newItems.add(insertedRow.rowMeta);
|
||||
}
|
||||
|
||||
if (insertedRow.isNew) {
|
||||
delegate.addNewRow(group, insertedRow.rowMeta, index);
|
||||
} else {
|
||||
delegate.insertRow(group, insertedRow.rowMeta, index);
|
||||
if (isGroupExist) {
|
||||
if (insertedRow.isNew) {
|
||||
delegate.addNewRow(group, insertedRow.rowMeta, index);
|
||||
} else {
|
||||
delegate.insertRow(group, insertedRow.rowMeta, index);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (final updatedRow in changeset.updatedRows) {
|
||||
final index = group.rows.indexWhere(
|
||||
final index = newItems.indexWhere(
|
||||
(rowPB) => rowPB.id == updatedRow.id,
|
||||
);
|
||||
|
||||
if (index != -1) {
|
||||
group.rows[index] = updatedRow;
|
||||
delegate.updateRow(group, updatedRow);
|
||||
newItems[index] = updatedRow;
|
||||
if (isGroupExist) {
|
||||
delegate.updateRow(group, updatedRow);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
group.freeze();
|
||||
group = group.rebuild((group) {
|
||||
group.rows.clear();
|
||||
group.rows.addAll(newItems);
|
||||
});
|
||||
onGroupChanged(group);
|
||||
},
|
||||
(err) => Log.error(err),
|
||||
);
|
||||
|
@ -1,112 +0,0 @@
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
import 'group_controller.dart';
|
||||
|
||||
part 'ungrouped_items_bloc.freezed.dart';
|
||||
|
||||
class UngroupedItemsBloc
|
||||
extends Bloc<UngroupedItemsEvent, UngroupedItemsState> {
|
||||
UngroupedItemsListener? listener;
|
||||
|
||||
UngroupedItemsBloc({required GroupPB group})
|
||||
: super(UngroupedItemsState(ungroupedItems: group.rows)) {
|
||||
on<UngroupedItemsEvent>(
|
||||
(event, emit) {
|
||||
event.when(
|
||||
initial: () {
|
||||
listener = UngroupedItemsListener(
|
||||
initialGroup: group,
|
||||
onGroupChanged: (ungroupedItems) {
|
||||
if (isClosed) return;
|
||||
add(
|
||||
UngroupedItemsEvent.updateGroup(
|
||||
ungroupedItems: ungroupedItems,
|
||||
),
|
||||
);
|
||||
},
|
||||
)..startListening();
|
||||
},
|
||||
updateGroup: (newItems) =>
|
||||
emit(UngroupedItemsState(ungroupedItems: newItems)),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
class UngroupedItemsEvent with _$UngroupedItemsEvent {
|
||||
const factory UngroupedItemsEvent.initial() = _Initial;
|
||||
const factory UngroupedItemsEvent.updateGroup({
|
||||
required List<RowMetaPB> ungroupedItems,
|
||||
}) = _UpdateGroup;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class UngroupedItemsState with _$UngroupedItemsState {
|
||||
const factory UngroupedItemsState({
|
||||
required List<RowMetaPB> ungroupedItems,
|
||||
}) = _UngroupedItemsState;
|
||||
}
|
||||
|
||||
class UngroupedItemsListener {
|
||||
List<RowMetaPB> _ungroupedItems;
|
||||
final SingleGroupListener _listener;
|
||||
final void Function(List<RowMetaPB> items) onGroupChanged;
|
||||
|
||||
UngroupedItemsListener({
|
||||
required GroupPB initialGroup,
|
||||
required this.onGroupChanged,
|
||||
}) : _ungroupedItems = List<RowMetaPB>.from(initialGroup.rows),
|
||||
_listener = SingleGroupListener(initialGroup);
|
||||
|
||||
void startListening() {
|
||||
_listener.start(
|
||||
onGroupChanged: (result) {
|
||||
result.fold(
|
||||
(GroupRowsNotificationPB changeset) {
|
||||
final newItems = List<RowMetaPB>.from(_ungroupedItems);
|
||||
for (final deletedRow in changeset.deletedRows) {
|
||||
newItems.removeWhere((rowPB) => rowPB.id == deletedRow);
|
||||
}
|
||||
|
||||
for (final insertedRow in changeset.insertedRows) {
|
||||
final index = newItems.indexWhere(
|
||||
(rowPB) => rowPB.id == insertedRow.rowMeta.id,
|
||||
);
|
||||
if (index != -1) {
|
||||
continue;
|
||||
}
|
||||
if (insertedRow.hasIndex() &&
|
||||
newItems.length > insertedRow.index) {
|
||||
newItems.insert(insertedRow.index, insertedRow.rowMeta);
|
||||
} else {
|
||||
newItems.add(insertedRow.rowMeta);
|
||||
}
|
||||
}
|
||||
|
||||
for (final updatedRow in changeset.updatedRows) {
|
||||
final index = newItems.indexWhere(
|
||||
(rowPB) => rowPB.id == updatedRow.id,
|
||||
);
|
||||
|
||||
if (index != -1) {
|
||||
newItems[index] = updatedRow;
|
||||
}
|
||||
}
|
||||
onGroupChanged.call(newItems);
|
||||
_ungroupedItems = newItems;
|
||||
},
|
||||
(err) => Log.error(err),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> dispose() async {
|
||||
_listener.stop();
|
||||
}
|
||||
}
|
@ -1,5 +1,3 @@
|
||||
// ignore_for_file: unused_field
|
||||
|
||||
import 'dart:collection';
|
||||
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
@ -9,7 +7,7 @@ import 'package:appflowy/plugins/database_view/application/field/field_controlle
|
||||
import 'package:appflowy/plugins/database_view/application/row/row_cache.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/row/row_controller.dart';
|
||||
import 'package:appflowy/plugins/database_view/board/presentation/widgets/board_column_header.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_type_extension.dart';
|
||||
import 'package:appflowy/plugins/database_view/tar_bar/tab_bar_view.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/row/row_detail.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
||||
@ -30,7 +28,7 @@ import '../../widgets/row/cell_builder.dart';
|
||||
import '../application/board_bloc.dart';
|
||||
import '../../widgets/card/card.dart';
|
||||
import 'toolbar/board_setting_bar.dart';
|
||||
import 'ungrouped_items_button.dart';
|
||||
import 'widgets/board_hidden_groups.dart';
|
||||
|
||||
class BoardPageTabBarBuilderImpl implements DatabaseTabBarItemBuilder {
|
||||
@override
|
||||
@ -39,46 +37,38 @@ class BoardPageTabBarBuilderImpl implements DatabaseTabBarItemBuilder {
|
||||
ViewPB view,
|
||||
DatabaseController controller,
|
||||
bool shrinkWrap,
|
||||
) {
|
||||
return BoardPage(
|
||||
key: _makeValueKey(controller),
|
||||
view: view,
|
||||
databaseController: controller,
|
||||
);
|
||||
}
|
||||
) =>
|
||||
BoardPage(view: view, databaseController: controller);
|
||||
|
||||
@override
|
||||
Widget settingBar(BuildContext context, DatabaseController controller) {
|
||||
return BoardSettingBar(
|
||||
key: _makeValueKey(controller),
|
||||
databaseController: controller,
|
||||
);
|
||||
}
|
||||
Widget settingBar(BuildContext context, DatabaseController controller) =>
|
||||
BoardSettingBar(
|
||||
key: _makeValueKey(controller),
|
||||
databaseController: controller,
|
||||
);
|
||||
|
||||
@override
|
||||
Widget settingBarExtension(
|
||||
BuildContext context,
|
||||
DatabaseController controller,
|
||||
) {
|
||||
return SizedBox.fromSize();
|
||||
}
|
||||
) =>
|
||||
const SizedBox.shrink();
|
||||
|
||||
ValueKey _makeValueKey(DatabaseController controller) {
|
||||
return ValueKey(controller.viewId);
|
||||
}
|
||||
ValueKey _makeValueKey(DatabaseController controller) =>
|
||||
ValueKey(controller.viewId);
|
||||
}
|
||||
|
||||
class BoardPage extends StatelessWidget {
|
||||
final DatabaseController databaseController;
|
||||
BoardPage({
|
||||
required this.view,
|
||||
required this.databaseController,
|
||||
Key? key,
|
||||
this.onEditStateChanged,
|
||||
}) : super(key: ValueKey(view.id));
|
||||
|
||||
final ViewPB view;
|
||||
|
||||
final DatabaseController databaseController;
|
||||
|
||||
/// Called when edit state changed
|
||||
final VoidCallback? onEditStateChanged;
|
||||
|
||||
@ -91,23 +81,18 @@ class BoardPage extends StatelessWidget {
|
||||
)..add(const BoardEvent.initial()),
|
||||
child: BlocBuilder<BoardBloc, BoardState>(
|
||||
buildWhen: (p, c) => p.loadingState != c.loadingState,
|
||||
builder: (context, state) {
|
||||
return state.loadingState.map(
|
||||
loading: (_) =>
|
||||
const Center(child: CircularProgressIndicator.adaptive()),
|
||||
finish: (result) {
|
||||
return result.successOrFail.fold(
|
||||
(_) => BoardContent(
|
||||
onEditStateChanged: onEditStateChanged,
|
||||
),
|
||||
(err) => FlowyErrorPage.message(
|
||||
err.toString(),
|
||||
howToFix: LocaleKeys.errorDialog_howToFixFallback.tr(),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
builder: (context, state) => state.loadingState.map(
|
||||
loading: (_) => const Center(
|
||||
child: CircularProgressIndicator.adaptive(),
|
||||
),
|
||||
finish: (result) => result.successOrFail.fold(
|
||||
(_) => BoardContent(onEditStateChanged: onEditStateChanged),
|
||||
(err) => FlowyErrorPage.message(
|
||||
err.toString(),
|
||||
howToFix: LocaleKeys.errorDialog_howToFixFallback.tr(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@ -126,12 +111,14 @@ class BoardContent extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _BoardContentState extends State<BoardContent> {
|
||||
late AppFlowyBoardScrollController scrollManager;
|
||||
late final ScrollController scrollController;
|
||||
final renderHook = RowCardRenderHook<String>();
|
||||
late final ScrollController scrollController;
|
||||
late final AppFlowyBoardScrollController scrollManager;
|
||||
|
||||
final config = const AppFlowyBoardConfig(
|
||||
groupBackgroundColor: Color(0xffF7F8FC),
|
||||
headerPadding: EdgeInsets.symmetric(horizontal: 8),
|
||||
cardPadding: EdgeInsets.symmetric(horizontal: 4, vertical: 3),
|
||||
);
|
||||
|
||||
@override
|
||||
@ -162,43 +149,36 @@ class _BoardContentState extends State<BoardContent> {
|
||||
},
|
||||
child: BlocBuilder<BoardBloc, BoardState>(
|
||||
builder: (context, state) {
|
||||
final showCreateGroupButton =
|
||||
context.read<BoardBloc>().groupingFieldType!.canCreateNewGroup;
|
||||
return Padding(
|
||||
padding: GridSize.contentInsets,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const VSpace(8.0),
|
||||
if (state.layoutSettings?.hideUngroupedColumn ?? false)
|
||||
_buildBoardHeader(context),
|
||||
Expanded(
|
||||
child: AppFlowyBoard(
|
||||
boardScrollController: scrollManager,
|
||||
scrollController: scrollController,
|
||||
controller: context.read<BoardBloc>().boardController,
|
||||
headerBuilder: (_, groupData) =>
|
||||
BlocProvider<BoardBloc>.value(
|
||||
value: context.read<BoardBloc>(),
|
||||
child: BoardColumnHeader(
|
||||
groupData: groupData,
|
||||
margin: config.headerPadding,
|
||||
),
|
||||
),
|
||||
footerBuilder: _buildFooter,
|
||||
trailing: BoardTrailing(scrollController: scrollController),
|
||||
cardBuilder: (_, column, columnItem) => _buildCard(
|
||||
context,
|
||||
column,
|
||||
columnItem,
|
||||
),
|
||||
groupConstraints: const BoxConstraints.tightFor(width: 300),
|
||||
config: AppFlowyBoardConfig(
|
||||
groupBackgroundColor:
|
||||
Theme.of(context).colorScheme.surfaceVariant,
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: AppFlowyBoard(
|
||||
boardScrollController: scrollManager,
|
||||
scrollController: scrollController,
|
||||
controller: context.read<BoardBloc>().boardController,
|
||||
groupConstraints: const BoxConstraints.tightFor(width: 300),
|
||||
config: const AppFlowyBoardConfig(
|
||||
groupPadding: EdgeInsets.symmetric(horizontal: 4),
|
||||
groupItemPadding: EdgeInsets.symmetric(horizontal: 4),
|
||||
),
|
||||
leading: HiddenGroupsColumn(margin: config.headerPadding),
|
||||
trailing: showCreateGroupButton
|
||||
? BoardTrailing(scrollController: scrollController)
|
||||
: null,
|
||||
headerBuilder: (_, groupData) => BlocProvider<BoardBloc>.value(
|
||||
value: context.read<BoardBloc>(),
|
||||
child: BoardColumnHeader(
|
||||
groupData: groupData,
|
||||
margin: config.headerPadding,
|
||||
),
|
||||
),
|
||||
footerBuilder: _buildFooter,
|
||||
cardBuilder: (_, column, columnItem) => _buildCard(
|
||||
context,
|
||||
column,
|
||||
columnItem,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
@ -206,19 +186,6 @@ class _BoardContentState extends State<BoardContent> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBoardHeader(BuildContext context) {
|
||||
return const Padding(
|
||||
padding: EdgeInsets.only(bottom: 8.0),
|
||||
child: SizedBox(
|
||||
height: 24,
|
||||
child: Align(
|
||||
alignment: AlignmentDirectional.centerEnd,
|
||||
child: UngroupedItemsButton(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _handleEditStateChanged(BoardState state, BuildContext context) {
|
||||
if (state.isEditingRow && state.editingRow != null) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
@ -231,25 +198,24 @@ class _BoardContentState extends State<BoardContent> {
|
||||
|
||||
Widget _buildFooter(BuildContext context, AppFlowyGroupData columnData) {
|
||||
return AppFlowyGroupFooter(
|
||||
height: 50,
|
||||
margin: config.footerPadding,
|
||||
icon: SizedBox(
|
||||
height: 20,
|
||||
width: 20,
|
||||
child: FlowySvg(
|
||||
FlowySvgs.add_s,
|
||||
color: Theme.of(context).iconTheme.color,
|
||||
color: Theme.of(context).hintColor,
|
||||
),
|
||||
),
|
||||
title: FlowyText.medium(
|
||||
LocaleKeys.board_column_createNewCard.tr(),
|
||||
color: Theme.of(context).hintColor,
|
||||
fontSize: 14,
|
||||
),
|
||||
height: 50,
|
||||
margin: config.footerPadding,
|
||||
onAddButtonClick: () {
|
||||
context.read<BoardBloc>().add(
|
||||
BoardEvent.createBottomRow(columnData.id),
|
||||
);
|
||||
},
|
||||
onAddButtonClick: () => context
|
||||
.read<BoardBloc>()
|
||||
.add(BoardEvent.createBottomRow(columnData.id)),
|
||||
);
|
||||
}
|
||||
|
||||
@ -307,15 +273,27 @@ class _BoardContentState extends State<BoardContent> {
|
||||
}
|
||||
|
||||
BoxDecoration _makeBoxDecoration(BuildContext context) {
|
||||
final borderSide = BorderSide(
|
||||
color: Theme.of(context).dividerColor,
|
||||
width: 1.0,
|
||||
);
|
||||
final isLightMode = Theme.of(context).brightness == Brightness.light;
|
||||
return BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
border: isLightMode ? Border.fromBorderSide(borderSide) : null,
|
||||
borderRadius: const BorderRadius.all(Radius.circular(6)),
|
||||
border: Border.fromBorderSide(
|
||||
BorderSide(
|
||||
color: Theme.of(context).dividerColor,
|
||||
width: 1.4,
|
||||
),
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
blurRadius: 4,
|
||||
spreadRadius: 0,
|
||||
color: const Color(0xFF1F2329).withOpacity(0.02),
|
||||
),
|
||||
BoxShadow(
|
||||
blurRadius: 4,
|
||||
spreadRadius: -2,
|
||||
color: const Color(0xFF1F2329).withOpacity(0.02),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@ -343,40 +321,37 @@ class _BoardContentState extends State<BoardContent> {
|
||||
|
||||
FlowyOverlay.show(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return RowDetailPage(
|
||||
cellBuilder: GridCellBuilder(cellCache: dataController.cellCache),
|
||||
rowController: dataController,
|
||||
);
|
||||
},
|
||||
builder: (_) => RowDetailPage(
|
||||
cellBuilder: GridCellBuilder(cellCache: dataController.cellCache),
|
||||
rowController: dataController,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class BoardTrailing extends StatefulWidget {
|
||||
const BoardTrailing({super.key, required this.scrollController});
|
||||
|
||||
final ScrollController scrollController;
|
||||
const BoardTrailing({required this.scrollController, super.key});
|
||||
|
||||
@override
|
||||
State<BoardTrailing> createState() => _BoardTrailingState();
|
||||
}
|
||||
|
||||
class _BoardTrailingState extends State<BoardTrailing> {
|
||||
bool isEditing = false;
|
||||
late final TextEditingController _textController;
|
||||
final TextEditingController _textController = TextEditingController();
|
||||
late final FocusNode _focusNode;
|
||||
|
||||
bool isEditing = false;
|
||||
|
||||
void _cancelAddNewGroup() {
|
||||
_textController.clear();
|
||||
setState(() {
|
||||
isEditing = false;
|
||||
});
|
||||
setState(() => isEditing = false);
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_textController = TextEditingController();
|
||||
_focusNode = FocusNode(
|
||||
onKeyEvent: (node, event) {
|
||||
if (_focusNode.hasFocus &&
|
||||
@ -406,7 +381,7 @@ class _BoardTrailingState extends State<BoardTrailing> {
|
||||
});
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(left: 8.0),
|
||||
padding: const EdgeInsets.only(left: 8.0, top: 12),
|
||||
child: Align(
|
||||
alignment: AlignmentDirectional.topStart,
|
||||
child: AnimatedSwitcher(
|
||||
@ -448,9 +423,7 @@ class _BoardTrailingState extends State<BoardTrailing> {
|
||||
width: 26,
|
||||
icon: const FlowySvg(FlowySvgs.add_s),
|
||||
iconColorOnHover: Theme.of(context).colorScheme.onSurface,
|
||||
onPressed: () => setState(() {
|
||||
isEditing = true;
|
||||
}),
|
||||
onPressed: () => setState(() => isEditing = true),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -1,237 +0,0 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/cell/cell_service.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/field/field_info.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/row/row_cache.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/row/row_controller.dart';
|
||||
import 'package:appflowy/plugins/database_view/board/application/board_bloc.dart';
|
||||
import 'package:appflowy/plugins/database_view/board/application/ungrouped_items_bloc.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/card/card_cell_builder.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/card/cells/card_cell.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/row/cell_builder.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/row/cells/text_cell/text_cell_bloc.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/row/row_detail.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/row_entities.pb.dart';
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/size.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class UngroupedItemsButton extends StatefulWidget {
|
||||
const UngroupedItemsButton({super.key});
|
||||
|
||||
@override
|
||||
State<UngroupedItemsButton> createState() => _UnscheduledEventsButtonState();
|
||||
}
|
||||
|
||||
class _UnscheduledEventsButtonState extends State<UngroupedItemsButton> {
|
||||
late final PopoverController _popoverController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_popoverController = PopoverController();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<BoardBloc, BoardState>(
|
||||
builder: (context, boardState) {
|
||||
final ungroupedGroup = context.watch<BoardBloc>().ungroupedGroup;
|
||||
final databaseController = context.read<BoardBloc>().databaseController;
|
||||
final primaryField = databaseController.fieldController.fieldInfos
|
||||
.firstWhereOrNull((element) => element.isPrimary)!;
|
||||
|
||||
if (ungroupedGroup == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return BlocProvider<UngroupedItemsBloc>(
|
||||
create: (_) => UngroupedItemsBloc(group: ungroupedGroup)
|
||||
..add(const UngroupedItemsEvent.initial()),
|
||||
child: BlocBuilder<UngroupedItemsBloc, UngroupedItemsState>(
|
||||
builder: (context, state) {
|
||||
return AppFlowyPopover(
|
||||
direction: PopoverDirection.bottomWithCenterAligned,
|
||||
triggerActions: PopoverTriggerFlags.none,
|
||||
controller: _popoverController,
|
||||
offset: const Offset(0, 8),
|
||||
constraints:
|
||||
const BoxConstraints(maxWidth: 282, maxHeight: 600),
|
||||
child: FlowyTooltip(
|
||||
message: LocaleKeys.board_ungroupedButtonTooltip.tr(),
|
||||
child: OutlinedButton(
|
||||
style: OutlinedButton.styleFrom(
|
||||
shape: RoundedRectangleBorder(
|
||||
side: BorderSide(
|
||||
color: Theme.of(context).dividerColor,
|
||||
width: 1,
|
||||
),
|
||||
borderRadius: Corners.s6Border,
|
||||
),
|
||||
side: BorderSide(
|
||||
color: Theme.of(context).dividerColor,
|
||||
width: 1,
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 4,
|
||||
),
|
||||
visualDensity: VisualDensity.compact,
|
||||
),
|
||||
onPressed: () {
|
||||
if (state.ungroupedItems.isNotEmpty) {
|
||||
_popoverController.show();
|
||||
}
|
||||
},
|
||||
child: FlowyText.regular(
|
||||
"${LocaleKeys.board_ungroupedButtonText.tr()} (${state.ungroupedItems.length})",
|
||||
fontSize: 10,
|
||||
),
|
||||
),
|
||||
),
|
||||
popupBuilder: (context) {
|
||||
return UngroupedItemList(
|
||||
viewId: databaseController.viewId,
|
||||
primaryField: primaryField,
|
||||
rowCache: databaseController.rowCache,
|
||||
ungroupedItems: state.ungroupedItems,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class UngroupedItemList extends StatelessWidget {
|
||||
final String viewId;
|
||||
final FieldInfo primaryField;
|
||||
final RowCache rowCache;
|
||||
final List<RowMetaPB> ungroupedItems;
|
||||
const UngroupedItemList({
|
||||
required this.viewId,
|
||||
required this.primaryField,
|
||||
required this.ungroupedItems,
|
||||
required this.rowCache,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final cells = <Widget>[
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4),
|
||||
child: FlowyText.medium(
|
||||
LocaleKeys.board_ungroupedItemsTitle.tr(),
|
||||
fontSize: 10,
|
||||
color: Theme.of(context).hintColor,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
...ungroupedItems.map(
|
||||
(item) {
|
||||
final rowController = RowController(
|
||||
rowMeta: item,
|
||||
viewId: viewId,
|
||||
rowCache: rowCache,
|
||||
);
|
||||
final renderHook = RowCardRenderHook<String>();
|
||||
renderHook.addTextCellHook((cellData, _, __) {
|
||||
return BlocBuilder<TextCellBloc, TextCellState>(
|
||||
builder: (context, state) {
|
||||
final text = cellData.isEmpty
|
||||
? LocaleKeys.grid_row_titlePlaceholder.tr()
|
||||
: cellData;
|
||||
|
||||
if (text.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: FlowyText.medium(
|
||||
text,
|
||||
textAlign: TextAlign.left,
|
||||
fontSize: 11,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
return UngroupedItem(
|
||||
cellContext: rowCache.loadCells(item)[primaryField.id]!,
|
||||
primaryField: primaryField,
|
||||
rowController: rowController,
|
||||
cellBuilder: CardCellBuilder<String>(rowController.cellCache),
|
||||
renderHook: renderHook,
|
||||
onPressed: () {
|
||||
FlowyOverlay.show(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return RowDetailPage(
|
||||
cellBuilder:
|
||||
GridCellBuilder(cellCache: rowController.cellCache),
|
||||
rowController: rowController,
|
||||
);
|
||||
},
|
||||
);
|
||||
PopoverContainer.of(context).close();
|
||||
},
|
||||
);
|
||||
},
|
||||
)
|
||||
];
|
||||
|
||||
return ListView.separated(
|
||||
itemBuilder: (context, index) => cells[index],
|
||||
itemCount: cells.length,
|
||||
separatorBuilder: (context, index) =>
|
||||
VSpace(GridSize.typeOptionSeparatorHeight),
|
||||
shrinkWrap: true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class UngroupedItem extends StatelessWidget {
|
||||
final DatabaseCellContext cellContext;
|
||||
final FieldInfo primaryField;
|
||||
final RowController rowController;
|
||||
final CardCellBuilder cellBuilder;
|
||||
final RowCardRenderHook<String> renderHook;
|
||||
final VoidCallback onPressed;
|
||||
const UngroupedItem({
|
||||
super.key,
|
||||
required this.cellContext,
|
||||
required this.onPressed,
|
||||
required this.cellBuilder,
|
||||
required this.rowController,
|
||||
required this.primaryField,
|
||||
required this.renderHook,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
height: 26,
|
||||
child: FlowyButton(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0),
|
||||
text: cellBuilder.buildCell(
|
||||
cellContext: cellContext,
|
||||
renderHook: renderHook,
|
||||
hasNotes: false,
|
||||
),
|
||||
onTap: onPressed,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -1,14 +1,15 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/database_view/board/application/board_bloc.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_type_extension.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/card/define.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/group.pb.dart';
|
||||
import 'package:appflowy_board/appflowy_board.dart';
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/theme_extension.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/hover.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
@ -18,11 +19,11 @@ class BoardColumnHeader extends StatefulWidget {
|
||||
const BoardColumnHeader({
|
||||
super.key,
|
||||
required this.groupData,
|
||||
this.margin,
|
||||
required this.margin,
|
||||
});
|
||||
|
||||
final AppFlowyGroupData groupData;
|
||||
final EdgeInsets? margin;
|
||||
final EdgeInsets margin;
|
||||
|
||||
@override
|
||||
State<BoardColumnHeader> createState() => _BoardColumnHeaderState();
|
||||
@ -74,7 +75,7 @@ class _BoardColumnHeaderState extends State<BoardColumnHeader> {
|
||||
child: FlowyText.medium(
|
||||
widget.groupData.headerData.groupName,
|
||||
fontSize: 14,
|
||||
overflow: TextOverflow.clip,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
);
|
||||
|
||||
@ -84,22 +85,16 @@ class _BoardColumnHeaderState extends State<BoardColumnHeader> {
|
||||
fit: FlexFit.tight,
|
||||
child: FlowyTooltip(
|
||||
message: LocaleKeys.board_column_renameGroupTooltip.tr(),
|
||||
child: FlowyHover(
|
||||
style: HoverStyle(
|
||||
hoverColor: Colors.transparent,
|
||||
foregroundColorOnHover:
|
||||
AFThemeExtension.of(context).textColor,
|
||||
),
|
||||
child: MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
child: GestureDetector(
|
||||
onTap: () => context.read<BoardBloc>().add(
|
||||
BoardEvent.startEditingHeader(
|
||||
widget.groupData.id,
|
||||
),
|
||||
),
|
||||
onTap: () => context
|
||||
.read<BoardBloc>()
|
||||
.add(BoardEvent.startEditingHeader(widget.groupData.id)),
|
||||
child: FlowyText.medium(
|
||||
widget.groupData.headerData.groupName,
|
||||
fontSize: 14,
|
||||
overflow: TextOverflow.clip,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -112,22 +107,31 @@ class _BoardColumnHeaderState extends State<BoardColumnHeader> {
|
||||
title = _buildTextField(context);
|
||||
}
|
||||
|
||||
return AppFlowyGroupHeader(
|
||||
title: title,
|
||||
icon: _buildHeaderIcon(boardCustomData),
|
||||
addIcon: SizedBox(
|
||||
height: 20,
|
||||
width: 20,
|
||||
child: FlowySvg(
|
||||
FlowySvgs.add_s,
|
||||
color: Theme.of(context).iconTheme.color,
|
||||
return Padding(
|
||||
padding: widget.margin,
|
||||
child: SizedBox(
|
||||
height: 50,
|
||||
child: Row(
|
||||
children: [
|
||||
_buildHeaderIcon(boardCustomData),
|
||||
title,
|
||||
const HSpace(6),
|
||||
_groupOptionsButton(context),
|
||||
const HSpace(4),
|
||||
FlowyTooltip(
|
||||
message: LocaleKeys.board_column_addToColumnTopTooltip.tr(),
|
||||
child: FlowyIconButton(
|
||||
width: 20,
|
||||
icon: const FlowySvg(FlowySvgs.add_s),
|
||||
iconColorOnHover: Theme.of(context).colorScheme.onSurface,
|
||||
onPressed: () => context
|
||||
.read<BoardBloc>()
|
||||
.add(BoardEvent.createHeaderRow(widget.groupData.id)),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
onAddButtonClick: () => context
|
||||
.read<BoardBloc>()
|
||||
.add(BoardEvent.createHeaderRow(widget.groupData.id)),
|
||||
height: 50,
|
||||
margin: widget.margin ?? EdgeInsets.zero,
|
||||
);
|
||||
},
|
||||
);
|
||||
@ -154,7 +158,6 @@ class _BoardColumnHeaderState extends State<BoardColumnHeader> {
|
||||
filled: true,
|
||||
fillColor: Theme.of(context).colorScheme.surface,
|
||||
hoverColor: Colors.transparent,
|
||||
// Magic number 4 makes the textField take up the same space as FlowyText
|
||||
contentPadding: EdgeInsets.symmetric(
|
||||
vertical: CardSizes.cardCellVPadding + 4,
|
||||
horizontal: 8,
|
||||
@ -181,45 +184,93 @@ class _BoardColumnHeaderState extends State<BoardColumnHeader> {
|
||||
);
|
||||
}
|
||||
|
||||
void _saveEdit() {
|
||||
context.read<BoardBloc>().add(
|
||||
BoardEvent.endEditingHeader(
|
||||
widget.groupData.id,
|
||||
_controller.text,
|
||||
void _saveEdit() => context
|
||||
.read<BoardBloc>()
|
||||
.add(BoardEvent.endEditingHeader(widget.groupData.id, _controller.text));
|
||||
|
||||
Widget _buildHeaderIcon(GroupData customData) =>
|
||||
switch (customData.fieldType) {
|
||||
FieldType.Checkbox => FlowySvg(
|
||||
customData.asCheckboxGroup()!.isCheck
|
||||
? FlowySvgs.check_filled_s
|
||||
: FlowySvgs.uncheck_s,
|
||||
blendMode: BlendMode.dst,
|
||||
),
|
||||
_ => const SizedBox.shrink(),
|
||||
};
|
||||
|
||||
Widget _groupOptionsButton(BuildContext context) {
|
||||
return AppFlowyPopover(
|
||||
clickHandler: PopoverClickHandler.gestureDetector,
|
||||
margin: const EdgeInsets.fromLTRB(8, 8, 8, 4),
|
||||
constraints: BoxConstraints.loose(const Size(168, 300)),
|
||||
direction: PopoverDirection.bottomWithLeftAligned,
|
||||
child: FlowyIconButton(
|
||||
width: 20,
|
||||
icon: const FlowySvg(FlowySvgs.details_horizontal_s),
|
||||
iconColorOnHover: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
popupBuilder: (popoverContext) {
|
||||
final customGroupData = widget.groupData.customData as GroupData;
|
||||
final menuItems = GroupOptions.values.toList();
|
||||
if (!customGroupData.fieldType.canEditHeader) {
|
||||
menuItems.remove(GroupOptions.rename);
|
||||
}
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
...menuItems.map(
|
||||
(action) => SizedBox(
|
||||
height: GridSize.popoverItemHeight,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(bottom: 4.0),
|
||||
child: FlowyButton(
|
||||
leftIcon: FlowySvg(action.icon),
|
||||
text: FlowyText.medium(
|
||||
action.text,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
onTap: () {
|
||||
action.call(context, customGroupData.group);
|
||||
PopoverContainer.of(popoverContext).close();
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Widget? _buildHeaderIcon(GroupData customData) {
|
||||
Widget? widget;
|
||||
switch (customData.fieldType) {
|
||||
case FieldType.Checkbox:
|
||||
final group = customData.asCheckboxGroup()!;
|
||||
widget = FlowySvg(
|
||||
group.isCheck ? FlowySvgs.check_filled_s : FlowySvgs.uncheck_s,
|
||||
blendMode: BlendMode.dst,
|
||||
);
|
||||
break;
|
||||
case FieldType.DateTime:
|
||||
case FieldType.LastEditedTime:
|
||||
case FieldType.CreatedTime:
|
||||
case FieldType.MultiSelect:
|
||||
case FieldType.Number:
|
||||
case FieldType.RichText:
|
||||
case FieldType.SingleSelect:
|
||||
case FieldType.URL:
|
||||
case FieldType.Checklist:
|
||||
break;
|
||||
}
|
||||
|
||||
if (widget != null) {
|
||||
widget = SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: widget,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return widget;
|
||||
}
|
||||
|
||||
enum GroupOptions {
|
||||
rename,
|
||||
hide;
|
||||
|
||||
void call(BuildContext context, GroupPB group) {
|
||||
switch (this) {
|
||||
case rename:
|
||||
context
|
||||
.read<BoardBloc>()
|
||||
.add(BoardEvent.startEditingHeader(group.groupId));
|
||||
break;
|
||||
case hide:
|
||||
context
|
||||
.read<BoardBloc>()
|
||||
.add(BoardEvent.toggleGroupVisibility(group, false));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
FlowySvgData get icon => switch (this) {
|
||||
rename => FlowySvgs.edit_s,
|
||||
hide => FlowySvgs.hide_s,
|
||||
};
|
||||
|
||||
String get text => switch (this) {
|
||||
rename => LocaleKeys.board_column_renameColumn.tr(),
|
||||
hide => LocaleKeys.board_column_hideColumn.tr(),
|
||||
};
|
||||
}
|
||||
|
@ -0,0 +1,483 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/cell/cell_service.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/database_controller.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/field/field_info.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/row/row_cache.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/row/row_controller.dart';
|
||||
import 'package:appflowy/plugins/database_view/board/application/board_bloc.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/card/card_cell_builder.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/card/cells/card_cell.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/row/cell_builder.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/row/cells/text_cell/text_cell_bloc.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/row/row_detail.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/hover.dart';
|
||||
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class HiddenGroupsColumn extends StatelessWidget {
|
||||
final EdgeInsets margin;
|
||||
const HiddenGroupsColumn({super.key, required this.margin});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final databaseController = context.read<BoardBloc>().databaseController;
|
||||
return BlocSelector<BoardBloc, BoardState, BoardLayoutSettingPB?>(
|
||||
selector: (state) => state.layoutSettings,
|
||||
builder: (context, layoutSettings) {
|
||||
if (layoutSettings == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
final isCollapsed = layoutSettings.collapseHiddenGroups;
|
||||
return AnimatedSize(
|
||||
alignment: AlignmentDirectional.topStart,
|
||||
curve: Curves.easeOut,
|
||||
duration: const Duration(milliseconds: 150),
|
||||
child: isCollapsed
|
||||
? SizedBox(
|
||||
height: 50,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 40, right: 8),
|
||||
child: Center(
|
||||
child: _collapseExpandIcon(context, isCollapsed),
|
||||
),
|
||||
),
|
||||
)
|
||||
: SizedBox(
|
||||
width: 260,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 50,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(
|
||||
left: 40 + margin.left,
|
||||
right: margin.right,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: FlowyText.medium(
|
||||
LocaleKeys
|
||||
.board_hiddenGroupSection_sectionTitle
|
||||
.tr(),
|
||||
fontSize: 14,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
color: Theme.of(context).hintColor,
|
||||
),
|
||||
),
|
||||
_collapseExpandIcon(context, isCollapsed),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: HiddenGroupList(
|
||||
databaseController: databaseController,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _collapseExpandIcon(BuildContext context, bool isCollapsed) {
|
||||
return FlowyTooltip(
|
||||
message: isCollapsed
|
||||
? LocaleKeys.board_hiddenGroupSection_expandTooltip.tr()
|
||||
: LocaleKeys.board_hiddenGroupSection_collapseTooltip.tr(),
|
||||
child: FlowyIconButton(
|
||||
width: 20,
|
||||
height: 20,
|
||||
iconColorOnHover: Theme.of(context).colorScheme.onSurface,
|
||||
onPressed: () => context
|
||||
.read<BoardBloc>()
|
||||
.add(BoardEvent.toggleHiddenSectionVisibility(!isCollapsed)),
|
||||
icon: FlowySvg(
|
||||
isCollapsed
|
||||
? FlowySvgs.hamburger_s_s
|
||||
: FlowySvgs.pull_left_outlined_s,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class HiddenGroupList extends StatelessWidget {
|
||||
const HiddenGroupList({
|
||||
super.key,
|
||||
required this.databaseController,
|
||||
});
|
||||
|
||||
final DatabaseController databaseController;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final bloc = context.read<BoardBloc>();
|
||||
return BlocBuilder<BoardBloc, BoardState>(
|
||||
builder: (_, state) => ReorderableListView.builder(
|
||||
proxyDecorator: (child, index, animation) => Material(
|
||||
color: Colors.transparent,
|
||||
child: Stack(
|
||||
children: [
|
||||
child,
|
||||
MouseRegion(
|
||||
cursor: Platform.isWindows
|
||||
? SystemMouseCursors.click
|
||||
: SystemMouseCursors.grabbing,
|
||||
child: const SizedBox.expand(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
buildDefaultDragHandles: false,
|
||||
itemCount: state.hiddenGroups.length,
|
||||
itemBuilder: (_, index) => Padding(
|
||||
padding: const EdgeInsets.only(bottom: 4),
|
||||
key: ValueKey("hiddenGroup${state.hiddenGroups[index].groupId}"),
|
||||
child: HiddenGroupCard(
|
||||
group: state.hiddenGroups[index],
|
||||
index: index,
|
||||
bloc: bloc,
|
||||
),
|
||||
),
|
||||
onReorder: (oldIndex, newIndex) {
|
||||
if (oldIndex < newIndex) {
|
||||
newIndex--;
|
||||
}
|
||||
final fromGroupId = state.hiddenGroups[oldIndex].groupId;
|
||||
final toGroupId = state.hiddenGroups[newIndex].groupId;
|
||||
bloc.add(BoardEvent.reorderGroup(fromGroupId, toGroupId));
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class HiddenGroupCard extends StatefulWidget {
|
||||
const HiddenGroupCard({
|
||||
super.key,
|
||||
required this.group,
|
||||
required this.index,
|
||||
required this.bloc,
|
||||
});
|
||||
|
||||
final GroupPB group;
|
||||
final BoardBloc bloc;
|
||||
final int index;
|
||||
|
||||
@override
|
||||
State<HiddenGroupCard> createState() => _HiddenGroupCardState();
|
||||
}
|
||||
|
||||
class _HiddenGroupCardState extends State<HiddenGroupCard> {
|
||||
final PopoverController _popoverController = PopoverController();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final databaseController = widget.bloc.databaseController;
|
||||
final primaryField = databaseController.fieldController.fieldInfos
|
||||
.firstWhereOrNull((element) => element.isPrimary)!;
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(left: 26),
|
||||
child: AppFlowyPopover(
|
||||
controller: _popoverController,
|
||||
direction: PopoverDirection.bottomWithCenterAligned,
|
||||
triggerActions: PopoverTriggerFlags.none,
|
||||
constraints: const BoxConstraints(maxWidth: 234, maxHeight: 300),
|
||||
popupBuilder: (popoverContext) => HiddenGroupPopupItemList(
|
||||
bloc: widget.bloc,
|
||||
viewId: databaseController.viewId,
|
||||
groupId: widget.group.groupId,
|
||||
primaryField: primaryField,
|
||||
rowCache: databaseController.rowCache,
|
||||
),
|
||||
child: HiddenGroupButtonContent(
|
||||
popoverController: _popoverController,
|
||||
groupId: widget.group.groupId,
|
||||
index: widget.index,
|
||||
bloc: widget.bloc,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class HiddenGroupButtonContent extends StatelessWidget {
|
||||
final String groupId;
|
||||
final int index;
|
||||
final BoardBloc bloc;
|
||||
const HiddenGroupButtonContent({
|
||||
super.key,
|
||||
required this.popoverController,
|
||||
required this.groupId,
|
||||
required this.index,
|
||||
required this.bloc,
|
||||
});
|
||||
|
||||
final PopoverController popoverController;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(right: 8.0),
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: popoverController.show,
|
||||
child: FlowyHover(
|
||||
builder: (context, isHovering) {
|
||||
return BlocProvider<BoardBloc>.value(
|
||||
value: bloc,
|
||||
child: BlocBuilder<BoardBloc, BoardState>(
|
||||
builder: (context, state) {
|
||||
final group = state.hiddenGroups.firstWhereOrNull(
|
||||
(g) => g.groupId == groupId,
|
||||
);
|
||||
if (group == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return SizedBox(
|
||||
height: 30,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 4,
|
||||
vertical: 3,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
HiddenGroupCardActions(
|
||||
isVisible: isHovering,
|
||||
index: index,
|
||||
),
|
||||
const HSpace(4),
|
||||
FlowyText.medium(
|
||||
group.groupName,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const HSpace(6),
|
||||
Expanded(
|
||||
child: FlowyText.medium(
|
||||
group.rows.length.toString(),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
color: Theme.of(context).hintColor,
|
||||
),
|
||||
),
|
||||
if (isHovering) ...[
|
||||
FlowyIconButton(
|
||||
width: 20,
|
||||
icon: FlowySvg(
|
||||
FlowySvgs.show_m,
|
||||
color: Theme.of(context).hintColor,
|
||||
),
|
||||
onPressed: () => context.read<BoardBloc>().add(
|
||||
BoardEvent.toggleGroupVisibility(
|
||||
group,
|
||||
true,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class HiddenGroupCardActions extends StatelessWidget {
|
||||
final bool isVisible;
|
||||
final int index;
|
||||
|
||||
const HiddenGroupCardActions({
|
||||
super.key,
|
||||
required this.isVisible,
|
||||
required this.index,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ReorderableDragStartListener(
|
||||
index: index,
|
||||
enabled: isVisible,
|
||||
child: MouseRegion(
|
||||
cursor: SystemMouseCursors.grab,
|
||||
child: SizedBox(
|
||||
height: 14,
|
||||
width: 14,
|
||||
child: isVisible
|
||||
? FlowySvg(
|
||||
FlowySvgs.drag_element_s,
|
||||
color: Theme.of(context).hintColor,
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class HiddenGroupPopupItemList extends StatelessWidget {
|
||||
const HiddenGroupPopupItemList({
|
||||
required this.bloc,
|
||||
required this.groupId,
|
||||
required this.viewId,
|
||||
required this.primaryField,
|
||||
required this.rowCache,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final BoardBloc bloc;
|
||||
final String groupId;
|
||||
final String viewId;
|
||||
final FieldInfo primaryField;
|
||||
final RowCache rowCache;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider.value(
|
||||
value: bloc,
|
||||
child: BlocBuilder<BoardBloc, BoardState>(
|
||||
builder: (context, state) {
|
||||
final group = state.hiddenGroups.firstWhereOrNull(
|
||||
(g) => g.groupId == groupId,
|
||||
);
|
||||
if (group == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
final cells = <Widget>[
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4),
|
||||
child: FlowyText.medium(
|
||||
group.groupName,
|
||||
fontSize: 10,
|
||||
color: Theme.of(context).hintColor,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
...group.rows.map(
|
||||
(item) {
|
||||
final rowController = RowController(
|
||||
rowMeta: item,
|
||||
viewId: viewId,
|
||||
rowCache: rowCache,
|
||||
);
|
||||
final renderHook = RowCardRenderHook<String>();
|
||||
renderHook.addTextCellHook((cellData, _, __) {
|
||||
return BlocBuilder<TextCellBloc, TextCellState>(
|
||||
builder: (context, state) {
|
||||
final text = cellData.isEmpty
|
||||
? LocaleKeys.grid_row_titlePlaceholder.tr()
|
||||
: cellData;
|
||||
|
||||
if (text.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: FlowyText.medium(
|
||||
text,
|
||||
textAlign: TextAlign.left,
|
||||
fontSize: 11,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
return HiddenGroupPopupItem(
|
||||
cellContext: rowCache.loadCells(item)[primaryField.id]!,
|
||||
primaryField: primaryField,
|
||||
rowController: rowController,
|
||||
cellBuilder: CardCellBuilder<String>(rowController.cellCache),
|
||||
renderHook: renderHook,
|
||||
onPressed: () {
|
||||
FlowyOverlay.show(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return RowDetailPage(
|
||||
cellBuilder: GridCellBuilder(
|
||||
cellCache: rowController.cellCache,
|
||||
),
|
||||
rowController: rowController,
|
||||
);
|
||||
},
|
||||
);
|
||||
PopoverContainer.of(context).close();
|
||||
},
|
||||
);
|
||||
},
|
||||
)
|
||||
];
|
||||
|
||||
return ListView.separated(
|
||||
itemBuilder: (context, index) => cells[index],
|
||||
itemCount: cells.length,
|
||||
separatorBuilder: (context, index) =>
|
||||
VSpace(GridSize.typeOptionSeparatorHeight),
|
||||
shrinkWrap: true,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class HiddenGroupPopupItem extends StatelessWidget {
|
||||
const HiddenGroupPopupItem({
|
||||
super.key,
|
||||
required this.cellContext,
|
||||
required this.onPressed,
|
||||
required this.cellBuilder,
|
||||
required this.rowController,
|
||||
required this.primaryField,
|
||||
required this.renderHook,
|
||||
});
|
||||
|
||||
final DatabaseCellContext cellContext;
|
||||
final FieldInfo primaryField;
|
||||
final RowController rowController;
|
||||
final CardCellBuilder cellBuilder;
|
||||
final RowCardRenderHook<String> renderHook;
|
||||
final VoidCallback onPressed;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
height: 26,
|
||||
child: FlowyButton(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0),
|
||||
text: cellBuilder.buildCell(
|
||||
cellContext: cellContext,
|
||||
renderHook: renderHook,
|
||||
hasNotes: !cellContext.rowMeta.isDocumentEmpty,
|
||||
),
|
||||
onTap: onPressed,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -21,7 +21,7 @@ class GridHeaderBloc extends Bloc<GridHeaderEvent, GridHeaderState> {
|
||||
on<GridHeaderEvent>(
|
||||
(event, emit) async {
|
||||
await event.map(
|
||||
initial: (_InitialHeader value) async {
|
||||
initial: (_InitialHeader value) {
|
||||
_startListening();
|
||||
add(
|
||||
GridHeaderEvent.didReceiveFieldUpdate(fieldController.fieldInfos),
|
||||
@ -65,7 +65,7 @@ class GridHeaderBloc extends Bloc<GridHeaderEvent, GridHeaderState> {
|
||||
result.fold((l) {}, (err) => Log.error(err));
|
||||
}
|
||||
|
||||
Future<void> _startListening() async {
|
||||
void _startListening() {
|
||||
fieldController.addListener(
|
||||
onReceiveFields: (fields) =>
|
||||
add(GridHeaderEvent.didReceiveFieldUpdate(fields)),
|
||||
|
@ -6,7 +6,7 @@ class GridSize {
|
||||
static double get scrollBarSize => 8 * scale;
|
||||
static double get headerHeight => 40 * scale;
|
||||
static double get footerHeight => 40 * scale;
|
||||
static double get leadingHeaderPadding => 50 * scale;
|
||||
static double get leadingHeaderPadding => 40 * scale;
|
||||
static double get trailHeaderPadding => 140 * scale;
|
||||
static double get headerContainerPadding => 0 * scale;
|
||||
static double get cellHPadding => 10 * scale;
|
||||
|
@ -59,4 +59,10 @@ extension FieldTypeListExtension on FieldType {
|
||||
FieldType.SingleSelect => true,
|
||||
_ => false,
|
||||
};
|
||||
|
||||
bool get canCreateNewGroup => switch (this) {
|
||||
FieldType.MultiSelect => true,
|
||||
FieldType.SingleSelect => true,
|
||||
_ => false,
|
||||
};
|
||||
}
|
||||
|
@ -10,7 +10,6 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import '../application/database_controller.dart';
|
||||
import '../grid/presentation/layout/sizes.dart';
|
||||
import 'tab_bar_header.dart';
|
||||
|
||||
abstract class DatabaseTabBarItemBuilder {
|
||||
@ -95,13 +94,11 @@ class _DatabaseTabBarViewState extends State<DatabaseTabBarView> {
|
||||
if (value) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
return SizedBox(
|
||||
return const SizedBox(
|
||||
height: 30,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: GridSize.leadingHeaderPadding,
|
||||
),
|
||||
child: const TabBarHeader(),
|
||||
padding: EdgeInsets.symmetric(horizontal: 40),
|
||||
child: TabBarHeader(),
|
||||
),
|
||||
);
|
||||
},
|
||||
|
Reference in New Issue
Block a user