Feat: rename stack inline (#3781)

* feat: rename stack in-line

* feat: rename stack in-line

* chore: compiler issues

* fix: conflicts and cleaning

* fix: code lost after merge

* test: fix failing rust tests

* fix: tauri localization wrong keys

---------

Co-authored-by: Richard Shiue <71320345+richardshiue@users.noreply.github.com>
This commit is contained in:
Mathias Mogensen
2023-10-26 03:38:37 +02:00
committed by GitHub
parent f40ae9ff25
commit aa27c4e6d4
24 changed files with 516 additions and 221 deletions

View File

@ -5,6 +5,7 @@ import 'package:dartz/dartz.dart';
class GroupBackendService {
final String viewId;
GroupBackendService(this.viewId);
Future<Either<Unit, FlowyError>> groupByField({
@ -19,10 +20,15 @@ class GroupBackendService {
Future<Either<Unit, FlowyError>> updateGroup({
required String groupId,
required String fieldId,
String? name,
bool? visible,
}) {
final payload = UpdateGroupPB.create()..groupId = groupId;
final payload = UpdateGroupPB.create()
..fieldId = fieldId
..viewId = viewId
..groupId = groupId;
if (name != null) {
payload.name = name;
}

View File

@ -3,6 +3,7 @@ import 'dart:collection';
import 'package:appflowy/plugins/database_view/application/defines.dart';
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:dartz/dartz.dart';
@ -11,6 +12,7 @@ import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
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';
@ -22,6 +24,7 @@ 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 =
@ -34,23 +37,15 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
required ViewPB view,
required this.databaseController,
}) : super(BoardState.initial(view.id)) {
groupBackendSvc = GroupBackendService(viewId);
boardController = AppFlowyBoardController(
onMoveGroup: (
fromGroupId,
fromIndex,
toGroupId,
toIndex,
) {
onMoveGroup: (fromGroupId, fromIndex, toGroupId, toIndex) {
databaseController.moveGroup(
fromGroupId: fromGroupId,
toGroupId: toGroupId,
);
},
onMoveGroupItem: (
groupId,
fromIndex,
toIndex,
) {
onMoveGroupItem: (groupId, fromIndex, toIndex) {
final fromRow = groupControllers[groupId]?.rowAtIndex(fromIndex);
final toRow = groupControllers[groupId]?.rowAtIndex(toIndex);
if (fromRow != null) {
@ -61,12 +56,7 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
);
}
},
onMoveGroupItemToGroup: (
fromGroupId,
fromIndex,
toGroupId,
toIndex,
) {
onMoveGroupItemToGroup: (fromGroupId, fromIndex, toGroupId, toIndex) {
final fromRow = groupControllers[fromGroupId]?.rowAtIndex(fromIndex);
final toRow = groupControllers[toGroupId]?.rowAtIndex(toIndex);
if (fromRow != null) {
@ -107,12 +97,11 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
didCreateRow: (group, row, int? index) {
emit(
state.copyWith(
editingRow: Some(
BoardEditingRow(
group: group,
row: row,
index: index,
),
isEditingRow: true,
editingRow: BoardEditingRow(
group: group,
row: row,
index: index,
),
),
);
@ -121,23 +110,26 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
startEditingRow: (group, row) {
emit(
state.copyWith(
editingRow: Some(
BoardEditingRow(
group: group,
row: row,
index: null,
),
editingRow: BoardEditingRow(
group: group,
row: row,
index: null,
),
),
);
_groupItemStartEditing(group, row, true);
},
endEditingRow: (rowId) {
state.editingRow.fold(() => null, (editingRow) {
assert(editingRow.row.id == rowId);
_groupItemStartEditing(editingRow.group, editingRow.row, false);
emit(state.copyWith(editingRow: none()));
});
if (state.editingRow != null && state.isEditingRow) {
assert(state.editingRow!.row.id == rowId);
_groupItemStartEditing(
state.editingRow!.group,
state.editingRow!.row,
false,
);
emit(state.copyWith(isEditingRow: false));
}
},
didReceiveGridUpdate: (DatabasePB grid) {
emit(state.copyWith(grid: Some(grid)));
@ -152,6 +144,20 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
),
);
},
startEditingHeader: (String groupId) {
emit(
state.copyWith(isEditingHeader: true, editingHeaderId: groupId),
);
},
endEditingHeader: (String groupId, String groupName) async {
await groupBackendSvc.updateGroup(
fieldId: groupControllers.values.first.group.fieldId,
groupId: groupId,
name: groupName,
);
emit(state.copyWith(isEditingHeader: false));
},
);
},
);
@ -303,6 +309,10 @@ class BoardEvent with _$BoardEvent {
const factory BoardEvent.initial() = _InitialBoard;
const factory BoardEvent.createBottomRow(String groupId) = _CreateBottomRow;
const factory BoardEvent.createHeaderRow(String groupId) = _CreateHeaderRow;
const factory BoardEvent.startEditingHeader(String groupId) =
_StartEditingHeader;
const factory BoardEvent.endEditingHeader(String groupId, String groupName) =
_EndEditingHeader;
const factory BoardEvent.didCreateRow(
GroupPB group,
RowMetaPB row,
@ -327,7 +337,10 @@ class BoardState with _$BoardState {
required String viewId,
required Option<DatabasePB> grid,
required List<String> groupIds,
required Option<BoardEditingRow> editingRow,
required bool isEditingHeader,
String? editingHeaderId,
required bool isEditingRow,
BoardEditingRow? editingRow,
required LoadingState loadingState,
required Option<FlowyError> noneOrError,
}) = _BoardState;
@ -336,7 +349,8 @@ class BoardState with _$BoardState {
grid: none(),
viewId: viewId,
groupIds: [],
editingRow: none(),
isEditingHeader: false,
isEditingRow: false,
noneOrError: none(),
loadingState: const LoadingState.loading(),
);

View File

@ -8,11 +8,11 @@ import 'package:appflowy/plugins/database_view/application/database_controller.d
import 'package:appflowy/plugins/database_view/application/field/field_controller.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/presentation/widgets/board_column_header.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.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';
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/row_entities.pb.dart';
import 'package:appflowy_board/appflowy_board.dart';
import 'package:easy_localization/easy_localization.dart';
@ -82,7 +82,7 @@ class BoardPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocProvider(
return BlocProvider<BoardBloc>(
create: (context) => BoardBloc(
view: view,
databaseController: databaseController,
@ -133,18 +133,20 @@ class _BoardContentState extends State<BoardContent> {
@override
void initState() {
super.initState();
scrollManager = AppFlowyBoardScrollController();
renderHook.addSelectOptionHook((options, groupId, _) {
// The cell should hide if the option id is equal to the groupId.
final isInGroup =
options.where((element) => element.id == groupId).isNotEmpty;
if (isInGroup || options.isEmpty) {
return const SizedBox();
return const SizedBox.shrink();
}
return null;
});
super.initState();
}
@override
@ -155,92 +157,51 @@ class _BoardContentState extends State<BoardContent> {
widget.onEditStateChanged?.call();
},
child: BlocBuilder<BoardBloc, BoardState>(
// Only rebuild when groups are added/removed/rearranged
buildWhen: (previous, current) => previous.groupIds != current.groupIds,
builder: (context, state) {
return Padding(
padding: GridSize.contentInsets,
child: _buildBoard(context),
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,
cardBuilder: (_, column, columnItem) => _buildCard(
context,
column,
columnItem,
),
groupConstraints: const BoxConstraints.tightFor(width: 300),
config: AppFlowyBoardConfig(
groupBackgroundColor:
Theme.of(context).colorScheme.surfaceVariant,
),
),
);
},
),
);
}
Widget _buildBoard(BuildContext context) {
return AppFlowyBoard(
boardScrollController: scrollManager,
scrollController: ScrollController(),
controller: context.read<BoardBloc>().boardController,
headerBuilder: _buildHeader,
footerBuilder: _buildFooter,
cardBuilder: (_, column, columnItem) => _buildCard(
context,
column,
columnItem,
),
groupConstraints: const BoxConstraints.tightFor(width: 300),
config: AppFlowyBoardConfig(
groupBackgroundColor: Theme.of(context).colorScheme.surfaceVariant,
),
);
}
void _handleEditStateChanged(BoardState state, BuildContext context) {
state.editingRow.fold(
() => null,
(editingRow) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (editingRow.index != null) {
} else {
scrollManager.scrollToBottom(editingRow.group.groupId);
}
});
},
);
}
@override
void dispose() {
super.dispose();
}
Widget _buildHeader(
BuildContext context,
AppFlowyGroupData groupData,
) {
final boardCustomData = groupData.customData as GroupData;
return AppFlowyGroupHeader(
title: Flexible(
fit: FlexFit.tight,
child: FlowyText.medium(
groupData.headerData.groupName,
fontSize: 14,
overflow: TextOverflow.clip,
),
),
icon: _buildHeaderIcon(boardCustomData),
addIcon: SizedBox(
height: 20,
width: 20,
child: FlowySvg(
FlowySvgs.add_s,
color: Theme.of(context).iconTheme.color,
),
),
onAddButtonClick: () {
context.read<BoardBloc>().add(
BoardEvent.createHeaderRow(groupData.id),
);
},
height: 50,
margin: config.headerPadding,
);
if (state.isEditingRow && state.editingRow != null) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (state.editingRow!.index == null) {
scrollManager.scrollToBottom(state.editingRow!.group.groupId);
}
});
}
}
Widget _buildFooter(BuildContext context, AppFlowyGroupData columnData) {
// final boardCustomData = columnData.customData as BoardCustomData;
// final group = boardCustomData.group;
return AppFlowyGroupFooter(
icon: SizedBox(
height: 20,
@ -251,7 +212,7 @@ class _BoardContentState extends State<BoardContent> {
),
),
title: FlowyText.medium(
LocaleKeys.board_column_create_new_card.tr(),
LocaleKeys.board_column_createNewCard.tr(),
fontSize: 14,
),
height: 50,
@ -269,25 +230,21 @@ class _BoardContentState extends State<BoardContent> {
AppFlowyGroupData afGroupData,
AppFlowyGroupItem afGroupItem,
) {
final boardBloc = context.read<BoardBloc>();
final groupItem = afGroupItem as GroupItem;
final groupData = afGroupData.customData as GroupData;
final rowMeta = groupItem.row;
final rowCache = context.read<BoardBloc>().getRowCache();
final rowCache = boardBloc.getRowCache();
/// Return placeholder widget if the rowCache is null.
if (rowCache == null) return SizedBox(key: ObjectKey(groupItem));
if (rowCache == null) return SizedBox.shrink(key: ObjectKey(groupItem));
final cellCache = rowCache.cellCache;
final fieldController = context.read<BoardBloc>().fieldController;
final viewId = context.read<BoardBloc>().viewId;
final fieldController = boardBloc.fieldController;
final viewId = boardBloc.viewId;
final cellBuilder = CardCellBuilder<String>(cellCache);
bool isEditing = false;
context.read<BoardBloc>().state.editingRow.fold(
() => null,
(editingRow) {
isEditing = editingRow.row.id == groupItem.row.id;
},
);
final isEditing = boardBloc.state.isEditingRow &&
boardBloc.state.editingRow?.row.id == groupItem.row.id;
final groupItemId = groupItem.row.id + groupData.group.groupId;
return AppFlowyGroupCard(
@ -305,26 +262,17 @@ class _BoardContentState extends State<BoardContent> {
cellBuilder: cellBuilder,
renderHook: renderHook,
openCard: (context) => _openCard(
viewId,
groupData.group.groupId,
fieldController,
rowMeta,
rowCache,
context,
context: context,
viewId: viewId,
groupId: groupData.group.groupId,
fieldController: fieldController,
rowMeta: rowMeta,
rowCache: rowCache,
),
onStartEditing: () {
context.read<BoardBloc>().add(
BoardEvent.startEditingRow(
groupData.group,
groupItem.row,
),
);
},
onEndEditing: () {
context
.read<BoardBloc>()
.add(BoardEvent.endEditingRow(groupItem.row.id));
},
onStartEditing: () => boardBloc
.add(BoardEvent.startEditingRow(groupData.group, groupItem.row)),
onEndEditing: () =>
boardBloc.add(BoardEvent.endEditingRow(groupItem.row.id)),
),
);
}
@ -342,19 +290,19 @@ class _BoardContentState extends State<BoardContent> {
);
}
void _openCard(
String viewId,
String groupId,
FieldController fieldController,
RowMetaPB rowMetaPB,
RowCache rowCache,
BuildContext context,
) {
void _openCard({
required BuildContext context,
required String viewId,
required String groupId,
required FieldController fieldController,
required RowMetaPB rowMeta,
required RowCache rowCache,
}) {
final rowInfo = RowInfo(
viewId: viewId,
fields: UnmodifiableListView(fieldController.fieldInfos),
rowMeta: rowMetaPB,
rowId: rowMetaPB.id,
rowMeta: rowMeta,
rowId: rowMeta.id,
);
final dataController = RowController(
@ -375,41 +323,3 @@ class _BoardContentState extends State<BoardContent> {
);
}
}
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:
break;
case FieldType.MultiSelect:
break;
case FieldType.Number:
break;
case FieldType.RichText:
break;
case FieldType.SingleSelect:
break;
case FieldType.URL:
break;
case FieldType.Checklist:
break;
}
if (widget != null) {
widget = SizedBox(
width: 20,
height: 20,
child: widget,
);
}
return widget;
}

View File

@ -0,0 +1,228 @@
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/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_board/appflowy_board.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/widget/flowy_tooltip.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class BoardColumnHeader extends StatefulWidget {
const BoardColumnHeader({
super.key,
required this.groupData,
this.margin,
});
final AppFlowyGroupData groupData;
final EdgeInsets? margin;
@override
State<BoardColumnHeader> createState() => _BoardColumnHeaderState();
}
class _BoardColumnHeaderState extends State<BoardColumnHeader> {
final FocusNode _focusNode = FocusNode();
late final TextEditingController _controller =
TextEditingController.fromValue(
TextEditingValue(
selection: TextSelection.collapsed(
offset: widget.groupData.headerData.groupName.length,
),
text: widget.groupData.headerData.groupName,
),
);
@override
void initState() {
super.initState();
_focusNode.addListener(() {
if (!_focusNode.hasFocus) {
_saveEdit();
}
});
}
@override
void dispose() {
_focusNode.dispose();
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final boardCustomData = widget.groupData.customData as GroupData;
return BlocProvider<BoardBloc>.value(
value: context.read<BoardBloc>(),
child: BlocBuilder<BoardBloc, BoardState>(
builder: (context, state) {
if (state.isEditingHeader) {
WidgetsBinding.instance.addPostFrameCallback((_) {
_focusNode.requestFocus();
});
}
Widget title = Expanded(
child: FlowyText.medium(
widget.groupData.headerData.groupName,
fontSize: 14,
overflow: TextOverflow.clip,
),
);
if (!boardCustomData.group.isDefault &&
boardCustomData.fieldType.canEditHeader) {
title = Flexible(
fit: FlexFit.tight,
child: FlowyTooltip(
message: LocaleKeys.board_column_renameGroupTooltip.tr(),
child: FlowyHover(
style: HoverStyle(
hoverColor: Colors.transparent,
foregroundColorOnHover:
AFThemeExtension.of(context).textColor,
),
child: GestureDetector(
onTap: () => context.read<BoardBloc>().add(
BoardEvent.startEditingHeader(
widget.groupData.id,
),
),
child: FlowyText.medium(
widget.groupData.headerData.groupName,
fontSize: 14,
overflow: TextOverflow.clip,
),
),
),
),
);
}
if (state.isEditingHeader &&
state.editingHeaderId == widget.groupData.id) {
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,
),
),
onAddButtonClick: () => context
.read<BoardBloc>()
.add(BoardEvent.createHeaderRow(widget.groupData.id)),
height: 50,
margin: widget.margin ?? EdgeInsets.zero,
);
},
),
);
}
Widget _buildTextField(BuildContext context) {
return Expanded(
child: RawKeyboardListener(
focusNode: FocusNode(),
onKey: (event) {
if (event is RawKeyDownEvent &&
[LogicalKeyboardKey.enter, LogicalKeyboardKey.escape]
.contains(event.logicalKey)) {
_saveEdit();
}
},
child: TextField(
controller: _controller,
focusNode: _focusNode,
onEditingComplete: _saveEdit,
maxLines: 1,
style: Theme.of(context).textTheme.bodyMedium!.copyWith(fontSize: 14),
decoration: InputDecoration(
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,
),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Theme.of(context).colorScheme.primary,
),
),
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Theme.of(context).colorScheme.primary,
),
),
border: OutlineInputBorder(
borderSide: BorderSide(
color: Theme.of(context).colorScheme.primary,
),
),
isDense: true,
),
),
),
);
}
void _saveEdit() {
context.read<BoardBloc>().add(
BoardEvent.endEditingHeader(
widget.groupData.id,
_controller.text,
),
);
}
}
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 null;
}

View File

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

View File

@ -31,7 +31,7 @@ class SelectOptionTypeOptionWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocProvider(
return BlocProvider<SelectOptionTypeOptionBloc>(
create: (context) => SelectOptionTypeOptionBloc(
options: options,
typeOptionAction: typeOptionAction,

View File

@ -12,9 +12,9 @@ class TextCellBloc extends Bloc<TextCellEvent, TextCellState> {
required this.cellController,
}) : super(TextCellState.initial(cellController)) {
on<TextCellEvent>(
(event, emit) async {
await event.when(
initial: () async {
(event, emit) {
event.when(
initial: () {
_startListening();
},
updateText: (text) {