mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-09-11 19:17:43 +03:00
feat: add kanban shortcuts (#5270)
* feat: add kanban shortcuts * feat: new ux for creating new kanban cards * chore: fix tests * fix: open card after creation in mobile board * chore: adjust code style according to launch review * chore: update frontend/appflowy_flutter/test/bloc_test/board_test/create_card_test.dart Co-authored-by: Mathias Mogensen <42929161+Xazin@users.noreply.github.com> * chore: more review * chore: implement move card to adjacent group * chore: reset focus upon card drag start * feat: N to start creating a row from bottom * fix: text card update * feat: shift + enter to create a new card after currently focused card * fix: row detail title * feat: shift + cmd + up to create card above * fix: double dispose and code cleanup * chore: code cleanup * fix: widget rebuilds * fix: build * chore: update frontend/appflowy_flutter/lib/mobile/presentation/database/board/mobile_board_page.dart Co-authored-by: Mathias Mogensen <42929161+Xazin@users.noreply.github.com> * fix: ontapoutside for cards being edited * fix: correct integration test * fix: always build * chore: code cleanup * fix: mobile build and bugs * fix: widget rebuilds * fix: code cleanup and fix mobile open * fix: disallow dragging when editing --------- Co-authored-by: Mathias Mogensen <42929161+Xazin@users.noreply.github.com>
This commit is contained in:
parent
28a27d1b67
commit
a490f34a61
@ -1,4 +1,5 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/plugins/database/board/presentation/board_page.dart';
|
||||
import 'package:appflowy/plugins/database/board/presentation/widgets/board_column_header.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/card/card.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
@ -82,23 +83,19 @@ void main() {
|
||||
findsOneWidget,
|
||||
);
|
||||
|
||||
await tester.tap(
|
||||
find
|
||||
.descendant(
|
||||
of: find.byType(AppFlowyGroupFooter),
|
||||
matching: find.byType(FlowySvg),
|
||||
)
|
||||
.at(1),
|
||||
await tester.tapButton(
|
||||
find.byType(BoardColumnFooter).at(1),
|
||||
);
|
||||
|
||||
const newCardName = 'Card 4';
|
||||
await tester.enterText(
|
||||
find.descendant(
|
||||
of: lastCard,
|
||||
of: find.byType(BoardColumnFooter),
|
||||
matching: find.byType(TextField),
|
||||
),
|
||||
newCardName,
|
||||
);
|
||||
await tester.testTextInput.receiveAction(TextInputAction.done);
|
||||
await tester.pumpAndSettle(const Duration(milliseconds: 500));
|
||||
|
||||
await tester.tap(find.byType(AppFlowyBoard));
|
||||
|
@ -56,7 +56,7 @@ void main() {
|
||||
expect(
|
||||
find.descendant(
|
||||
of: find.byType(AppFlowyEditor),
|
||||
matching: find.byType(BoardPage),
|
||||
matching: find.byType(DesktopBoardPage),
|
||||
),
|
||||
findsOneWidget,
|
||||
);
|
||||
@ -104,7 +104,7 @@ void main() {
|
||||
expect(
|
||||
find.descendant(
|
||||
of: find.byType(AppFlowyEditor),
|
||||
matching: find.byType(BoardPage),
|
||||
matching: find.byType(DesktopBoardPage),
|
||||
),
|
||||
findsOneWidget,
|
||||
);
|
||||
|
@ -61,7 +61,7 @@ void main() {
|
||||
expect(find.byType(GridPage), findsOneWidget);
|
||||
break;
|
||||
case ViewLayoutPB.Board:
|
||||
expect(find.byType(BoardPage), findsOneWidget);
|
||||
expect(find.byType(DesktopBoardPage), findsOneWidget);
|
||||
break;
|
||||
case ViewLayoutPB.Calendar:
|
||||
expect(find.byType(CalendarPage), findsOneWidget);
|
||||
|
@ -1463,7 +1463,7 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
||||
|
||||
void assertCurrentDatabaseTagIs(DatabaseLayoutPB layout) => switch (layout) {
|
||||
DatabaseLayoutPB.Board =>
|
||||
expect(find.byType(BoardPage), findsOneWidget),
|
||||
expect(find.byType(DesktopBoardPage), findsOneWidget),
|
||||
DatabaseLayoutPB.Calendar =>
|
||||
expect(find.byType(CalendarPage), findsOneWidget),
|
||||
DatabaseLayoutPB.Grid => expect(find.byType(GridPage), findsOneWidget),
|
||||
@ -1521,7 +1521,7 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
||||
}
|
||||
|
||||
Finder finderForDatabaseLayoutType(DatabaseLayoutPB layout) => switch (layout) {
|
||||
DatabaseLayoutPB.Board => find.byType(BoardPage),
|
||||
DatabaseLayoutPB.Board => find.byType(DesktopBoardPage),
|
||||
DatabaseLayoutPB.Calendar => find.byType(CalendarPage),
|
||||
DatabaseLayoutPB.Grid => find.byType(GridPage),
|
||||
_ => throw Exception('Unknown database layout type: $layout'),
|
||||
|
@ -1,4 +1,4 @@
|
||||
export 'mobile_board_screen.dart';
|
||||
export 'mobile_board_content.dart';
|
||||
export 'mobile_board_page.dart';
|
||||
export 'widgets/mobile_hidden_groups_column.dart';
|
||||
export 'widgets/mobile_board_trailing.dart';
|
||||
|
@ -3,12 +3,16 @@ import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/mobile/presentation/database/board/board.dart';
|
||||
import 'package:appflowy/mobile/presentation/database/board/widgets/group_card_header.dart';
|
||||
import 'package:appflowy/mobile/presentation/database/card/card.dart';
|
||||
import 'package:appflowy/mobile/presentation/widgets/widgets.dart';
|
||||
import 'package:appflowy/plugins/database/application/database_controller.dart';
|
||||
import 'package:appflowy/plugins/database/board/application/board_bloc.dart';
|
||||
import 'package:appflowy/plugins/database/grid/presentation/widgets/header/field_type_extension.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/card/card.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/cell/card_cell_builder.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/cell/card_cell_style_maps/mobile_board_card_cell_style.dart';
|
||||
import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
|
||||
import 'package:appflowy_board/appflowy_board.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
@ -16,25 +20,100 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
class MobileBoardContent extends StatefulWidget {
|
||||
const MobileBoardContent({
|
||||
class MobileBoardPage extends StatefulWidget {
|
||||
const MobileBoardPage({
|
||||
super.key,
|
||||
required this.view,
|
||||
required this.databaseController,
|
||||
this.onEditStateChanged,
|
||||
});
|
||||
|
||||
final ViewPB view;
|
||||
|
||||
final DatabaseController databaseController;
|
||||
|
||||
/// Called when edit state changed
|
||||
final VoidCallback? onEditStateChanged;
|
||||
|
||||
@override
|
||||
State<MobileBoardContent> createState() => _MobileBoardContentState();
|
||||
State<MobileBoardPage> createState() => _MobileBoardPageState();
|
||||
}
|
||||
|
||||
class _MobileBoardContentState extends State<MobileBoardContent> {
|
||||
late final ScrollController scrollController;
|
||||
late final AppFlowyBoardScrollController scrollManager;
|
||||
class _MobileBoardPageState extends State<MobileBoardPage> {
|
||||
late final ValueNotifier<DidCreateRowResult?> _didCreateRow;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_didCreateRow = ValueNotifier(null)..addListener(_handleDidCreateRow);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_didCreateRow
|
||||
..removeListener(_handleDidCreateRow)
|
||||
..dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider<BoardBloc>(
|
||||
create: (_) => BoardBloc(
|
||||
databaseController: widget.databaseController,
|
||||
didCreateRow: _didCreateRow,
|
||||
)..add(const BoardEvent.initial()),
|
||||
child: BlocBuilder<BoardBloc, BoardState>(
|
||||
builder: (context, state) => state.maybeMap(
|
||||
loading: (_) => const Center(
|
||||
child: CircularProgressIndicator.adaptive(),
|
||||
),
|
||||
error: (err) => FlowyMobileStateContainer.error(
|
||||
emoji: '🛸',
|
||||
title: LocaleKeys.board_mobile_failedToLoad.tr(),
|
||||
errorMsg: err.toString(),
|
||||
),
|
||||
ready: (data) => const _BoardContent(),
|
||||
orElse: () => const SizedBox.shrink(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _handleDidCreateRow() {
|
||||
if (_didCreateRow.value != null) {
|
||||
final result = _didCreateRow.value!;
|
||||
switch (result.action) {
|
||||
case DidCreateRowAction.openAsPage:
|
||||
context.push(
|
||||
MobileRowDetailPage.routeName,
|
||||
extra: {
|
||||
MobileRowDetailPage.argRowId: result.rowMeta.id,
|
||||
MobileRowDetailPage.argDatabaseController:
|
||||
widget.databaseController,
|
||||
},
|
||||
);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _BoardContent extends StatefulWidget {
|
||||
const _BoardContent();
|
||||
|
||||
@override
|
||||
State<_BoardContent> createState() => _BoardContentState();
|
||||
}
|
||||
|
||||
class _BoardContentState extends State<_BoardContent> {
|
||||
late final ScrollController scrollController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// mobile may not need this
|
||||
// scroll to bottom when add a new card
|
||||
scrollManager = AppFlowyBoardScrollController();
|
||||
scrollController = ScrollController();
|
||||
}
|
||||
|
||||
@ -57,54 +136,48 @@ class _MobileBoardContentState extends State<MobileBoardContent> {
|
||||
cardMargin: const EdgeInsets.all(4),
|
||||
);
|
||||
|
||||
return BlocListener<BoardBloc, BoardState>(
|
||||
listenWhen: (previous, current) =>
|
||||
previous.recentAddedRowMeta != current.recentAddedRowMeta,
|
||||
listener: (context, state) {
|
||||
context.push(
|
||||
MobileRowDetailPage.routeName,
|
||||
extra: {
|
||||
MobileRowDetailPage.argRowId: state.recentAddedRowMeta!.id,
|
||||
MobileRowDetailPage.argDatabaseController:
|
||||
context.read<BoardBloc>().databaseController,
|
||||
return BlocBuilder<BoardBloc, BoardState>(
|
||||
builder: (context, state) {
|
||||
return state.maybeMap(
|
||||
orElse: () => const SizedBox.shrink(),
|
||||
ready: (state) {
|
||||
final showCreateGroupButton = context
|
||||
.read<BoardBloc>()
|
||||
.groupingFieldType
|
||||
?.canCreateNewGroup ??
|
||||
false;
|
||||
final showHiddenGroups = state.hiddenGroups.isNotEmpty;
|
||||
return AppFlowyBoard(
|
||||
scrollController: scrollController,
|
||||
controller: context.read<BoardBloc>().boardController,
|
||||
groupConstraints:
|
||||
BoxConstraints.tightFor(width: screenWidth * 0.7),
|
||||
config: config,
|
||||
leading: showHiddenGroups
|
||||
? MobileHiddenGroupsColumn(
|
||||
padding: config.groupHeaderPadding,
|
||||
)
|
||||
: const HSpace(16),
|
||||
trailing: showCreateGroupButton
|
||||
? const MobileBoardTrailing()
|
||||
: const HSpace(16),
|
||||
headerBuilder: (_, groupData) => BlocProvider<BoardBloc>.value(
|
||||
value: context.read<BoardBloc>(),
|
||||
child: GroupCardHeader(
|
||||
groupData: groupData,
|
||||
),
|
||||
),
|
||||
footerBuilder: _buildFooter,
|
||||
cardBuilder: (_, column, columnItem) => _buildCard(
|
||||
context: context,
|
||||
afGroupData: column,
|
||||
afGroupItem: columnItem,
|
||||
cardMargin: config.cardMargin,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
child: BlocBuilder<BoardBloc, BoardState>(
|
||||
builder: (context, state) {
|
||||
final showCreateGroupButton =
|
||||
context.read<BoardBloc>().groupingFieldType.canCreateNewGroup;
|
||||
final showHiddenGroups = state.hiddenGroups.isNotEmpty;
|
||||
return AppFlowyBoard(
|
||||
boardScrollController: scrollManager,
|
||||
scrollController: scrollController,
|
||||
controller: context.read<BoardBloc>().boardController,
|
||||
groupConstraints: BoxConstraints.tightFor(width: screenWidth * 0.7),
|
||||
config: config,
|
||||
leading: showHiddenGroups
|
||||
? MobileHiddenGroupsColumn(
|
||||
padding: config.groupHeaderPadding,
|
||||
)
|
||||
: const HSpace(16),
|
||||
trailing: showCreateGroupButton
|
||||
? const MobileBoardTrailing()
|
||||
: const HSpace(16),
|
||||
headerBuilder: (_, groupData) => BlocProvider<BoardBloc>.value(
|
||||
value: context.read<BoardBloc>(),
|
||||
child: GroupCardHeader(
|
||||
groupData: groupData,
|
||||
),
|
||||
),
|
||||
footerBuilder: _buildFooter,
|
||||
cardBuilder: (_, column, columnItem) => _buildCard(
|
||||
context: context,
|
||||
afGroupData: column,
|
||||
afGroupItem: columnItem,
|
||||
cardMargin: config.cardMargin,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@ -129,9 +202,14 @@ class _MobileBoardContentState extends State<MobileBoardContent> {
|
||||
color: style.colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
onPressed: () => context
|
||||
.read<BoardBloc>()
|
||||
.add(BoardEvent.createBottomRow(columnData.id)),
|
||||
onPressed: () => context.read<BoardBloc>().add(
|
||||
BoardEvent.createRow(
|
||||
columnData.id,
|
||||
OrderObjectPositionTypePB.End,
|
||||
null,
|
||||
null,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@ -146,16 +224,9 @@ class _MobileBoardContentState extends State<MobileBoardContent> {
|
||||
final groupItem = afGroupItem as GroupItem;
|
||||
final groupData = afGroupData.customData as GroupData;
|
||||
final rowMeta = groupItem.row;
|
||||
final rowCache = boardBloc.getRowCache();
|
||||
|
||||
/// Return placeholder widget if the rowCache is null.
|
||||
if (rowCache == null) return SizedBox.shrink(key: ObjectKey(groupItem));
|
||||
final viewId = boardBloc.viewId;
|
||||
|
||||
final cellBuilder =
|
||||
CardCellBuilder(databaseController: boardBloc.databaseController);
|
||||
final isEditing = boardBloc.state.isEditingRow &&
|
||||
boardBloc.state.editingRow?.row.id == groupItem.row.id;
|
||||
|
||||
final groupItemId = groupItem.row.id + groupData.group.groupId;
|
||||
|
||||
@ -166,12 +237,12 @@ class _MobileBoardContentState extends State<MobileBoardContent> {
|
||||
child: RowCard(
|
||||
fieldController: boardBloc.fieldController,
|
||||
rowMeta: rowMeta,
|
||||
viewId: viewId,
|
||||
rowCache: rowCache,
|
||||
viewId: boardBloc.viewId,
|
||||
rowCache: boardBloc.rowCache,
|
||||
groupingFieldId: groupItem.fieldInfo.id,
|
||||
isEditing: isEditing,
|
||||
isEditing: false,
|
||||
cellBuilder: cellBuilder,
|
||||
openCard: (context) {
|
||||
onTap: (context) {
|
||||
context.push(
|
||||
MobileRowDetailPage.routeName,
|
||||
extra: {
|
||||
@ -181,10 +252,8 @@ class _MobileBoardContentState extends State<MobileBoardContent> {
|
||||
},
|
||||
);
|
||||
},
|
||||
onStartEditing: () => boardBloc
|
||||
.add(BoardEvent.startEditingRow(groupData.group, groupItem.row)),
|
||||
onEndEditing: () =>
|
||||
boardBloc.add(BoardEvent.endEditingRow(groupItem.row.id)),
|
||||
onStartEditing: () {},
|
||||
onEndEditing: () {},
|
||||
styleConfiguration: RowCardStyleConfiguration(
|
||||
cellStyleMap: mobileBoardCardCellStyleMap(context),
|
||||
showAccessory: false,
|
@ -1,16 +1,14 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
|
||||
import 'package:appflowy/mobile/presentation/widgets/flowy_mobile_quick_action_button.dart';
|
||||
import 'package:appflowy/plugins/database/board/application/board_bloc.dart';
|
||||
import 'package:appflowy/plugins/database/grid/presentation/widgets/header/field_type_extension.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_backend/protobuf/flowy-database2/protobuf.dart';
|
||||
import 'package:appflowy_board/appflowy_board.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
@ -73,8 +71,12 @@ class _GroupCardHeaderState extends State<GroupCardHeader> {
|
||||
);
|
||||
}
|
||||
|
||||
if (state.isEditingHeader &&
|
||||
state.editingHeaderId == widget.groupData.id) {
|
||||
final isEditing = state.maybeMap(
|
||||
ready: (value) => value.editingHeaderId == widget.groupData.id,
|
||||
orElse: () => false,
|
||||
);
|
||||
|
||||
if (isEditing) {
|
||||
title = TextField(
|
||||
controller: _controller,
|
||||
autofocus: true,
|
||||
@ -135,7 +137,7 @@ class _GroupCardHeaderState extends State<GroupCardHeader> {
|
||||
icon: FlowySvgs.hide_s,
|
||||
onTap: () {
|
||||
context.read<BoardBloc>().add(
|
||||
BoardEvent.toggleGroupVisibility(
|
||||
BoardEvent.setGroupVisibility(
|
||||
widget.groupData.customData.group
|
||||
as GroupPB,
|
||||
false,
|
||||
@ -154,9 +156,16 @@ class _GroupCardHeaderState extends State<GroupCardHeader> {
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
splashRadius: 5,
|
||||
onPressed: () => context.read<BoardBloc>().add(
|
||||
BoardEvent.createHeaderRow(widget.groupData.id),
|
||||
),
|
||||
onPressed: () {
|
||||
context.read<BoardBloc>().add(
|
||||
BoardEvent.createRow(
|
||||
widget.groupData.id,
|
||||
OrderObjectPositionTypePB.Start,
|
||||
null,
|
||||
null,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
|
@ -23,7 +23,10 @@ class MobileHiddenGroupsColumn extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
final databaseController = context.read<BoardBloc>().databaseController;
|
||||
return BlocSelector<BoardBloc, BoardState, BoardLayoutSettingPB?>(
|
||||
selector: (state) => state.layoutSettings,
|
||||
selector: (state) => state.maybeMap(
|
||||
orElse: () => null,
|
||||
ready: (value) => value.layoutSettings,
|
||||
),
|
||||
builder: (context, layoutSettings) {
|
||||
if (layoutSettings == null) {
|
||||
return const SizedBox.shrink();
|
||||
@ -105,29 +108,36 @@ class MobileHiddenGroupList extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<BoardBloc, BoardState>(
|
||||
builder: (_, state) => ReorderableListView.builder(
|
||||
itemCount: state.hiddenGroups.length,
|
||||
itemBuilder: (_, index) => MobileHiddenGroup(
|
||||
key: ValueKey(state.hiddenGroups[index].groupId),
|
||||
group: state.hiddenGroups[index],
|
||||
index: index,
|
||||
),
|
||||
proxyDecorator: (child, index, animation) => BlocProvider.value(
|
||||
value: context.read<BoardBloc>(),
|
||||
child: Material(color: Colors.transparent, child: child),
|
||||
),
|
||||
physics: const ClampingScrollPhysics(),
|
||||
onReorder: (oldIndex, newIndex) {
|
||||
if (oldIndex < newIndex) {
|
||||
newIndex--;
|
||||
}
|
||||
final fromGroupId = state.hiddenGroups[oldIndex].groupId;
|
||||
final toGroupId = state.hiddenGroups[newIndex].groupId;
|
||||
context
|
||||
.read<BoardBloc>()
|
||||
.add(BoardEvent.reorderGroup(fromGroupId, toGroupId));
|
||||
},
|
||||
),
|
||||
builder: (_, state) {
|
||||
return state.maybeMap(
|
||||
orElse: () => const SizedBox.shrink(),
|
||||
ready: (state) {
|
||||
return ReorderableListView.builder(
|
||||
itemCount: state.hiddenGroups.length,
|
||||
itemBuilder: (_, index) => MobileHiddenGroup(
|
||||
key: ValueKey(state.hiddenGroups[index].groupId),
|
||||
group: state.hiddenGroups[index],
|
||||
index: index,
|
||||
),
|
||||
proxyDecorator: (child, index, animation) => BlocProvider.value(
|
||||
value: context.read<BoardBloc>(),
|
||||
child: Material(color: Colors.transparent, child: child),
|
||||
),
|
||||
physics: const ClampingScrollPhysics(),
|
||||
onReorder: (oldIndex, newIndex) {
|
||||
if (oldIndex < newIndex) {
|
||||
newIndex--;
|
||||
}
|
||||
final fromGroupId = state.hiddenGroups[oldIndex].groupId;
|
||||
final toGroupId = state.hiddenGroups[newIndex].groupId;
|
||||
context
|
||||
.read<BoardBloc>()
|
||||
.add(BoardEvent.reorderGroup(fromGroupId, toGroupId));
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -148,92 +158,77 @@ class MobileHiddenGroup extends StatelessWidget {
|
||||
final primaryField = databaseController.fieldController.fieldInfos
|
||||
.firstWhereOrNull((element) => element.isPrimary)!;
|
||||
|
||||
return BlocBuilder<BoardBloc, BoardState>(
|
||||
builder: (context, state) {
|
||||
final group = state.hiddenGroups.firstWhereOrNull(
|
||||
(g) => g.groupId == this.group.groupId,
|
||||
);
|
||||
if (group == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
final cells = group.rows.map(
|
||||
(item) {
|
||||
final cellContext =
|
||||
databaseController.rowCache.loadCells(item).firstWhere(
|
||||
(cellContext) => cellContext.fieldId == primaryField.id,
|
||||
);
|
||||
|
||||
return TextButton(
|
||||
style: TextButton.styleFrom(
|
||||
textStyle: Theme.of(context).textTheme.bodyMedium,
|
||||
foregroundColor: Theme.of(context).colorScheme.onBackground,
|
||||
visualDensity: VisualDensity.compact,
|
||||
),
|
||||
child: CardCellBuilder(
|
||||
databaseController:
|
||||
context.read<BoardBloc>().databaseController,
|
||||
).build(
|
||||
cellContext: cellContext,
|
||||
styleMap: {FieldType.RichText: _titleCellStyle(context)},
|
||||
hasNotes: !item.isDocumentEmpty,
|
||||
),
|
||||
onPressed: () {
|
||||
context.push(
|
||||
MobileRowDetailPage.routeName,
|
||||
extra: {
|
||||
MobileRowDetailPage.argRowId: item.id,
|
||||
MobileRowDetailPage.argDatabaseController:
|
||||
context.read<BoardBloc>().databaseController,
|
||||
},
|
||||
final cells = group.rows.map(
|
||||
(item) {
|
||||
final cellContext =
|
||||
databaseController.rowCache.loadCells(item).firstWhere(
|
||||
(cellContext) => cellContext.fieldId == primaryField.id,
|
||||
);
|
||||
|
||||
return TextButton(
|
||||
style: TextButton.styleFrom(
|
||||
textStyle: Theme.of(context).textTheme.bodyMedium,
|
||||
foregroundColor: Theme.of(context).colorScheme.onBackground,
|
||||
visualDensity: VisualDensity.compact,
|
||||
),
|
||||
child: CardCellBuilder(
|
||||
databaseController: context.read<BoardBloc>().databaseController,
|
||||
).build(
|
||||
cellContext: cellContext,
|
||||
styleMap: {FieldType.RichText: _titleCellStyle(context)},
|
||||
hasNotes: !item.isDocumentEmpty,
|
||||
),
|
||||
onPressed: () {
|
||||
context.push(
|
||||
MobileRowDetailPage.routeName,
|
||||
extra: {
|
||||
MobileRowDetailPage.argRowId: item.id,
|
||||
MobileRowDetailPage.argDatabaseController:
|
||||
context.read<BoardBloc>().databaseController,
|
||||
},
|
||||
);
|
||||
},
|
||||
).toList();
|
||||
|
||||
return ExpansionTile(
|
||||
tilePadding: EdgeInsets.zero,
|
||||
childrenPadding: EdgeInsets.zero,
|
||||
title: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
context.read<BoardBloc>().generateGroupNameFromGroup(group),
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
GestureDetector(
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.all(4),
|
||||
child: FlowySvg(
|
||||
FlowySvgs.hide_m,
|
||||
size: Size.square(20),
|
||||
),
|
||||
),
|
||||
onTap: () => showFlowyMobileConfirmDialog(
|
||||
context,
|
||||
title: FlowyText(LocaleKeys.board_mobile_showGroup.tr()),
|
||||
content: FlowyText(
|
||||
LocaleKeys.board_mobile_showGroupContent.tr(),
|
||||
),
|
||||
actionButtonTitle: LocaleKeys.button_yes.tr(),
|
||||
actionButtonColor: Theme.of(context).colorScheme.primary,
|
||||
onActionButtonPressed: () => context.read<BoardBloc>().add(
|
||||
BoardEvent.toggleGroupVisibility(
|
||||
group,
|
||||
true,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
children: cells,
|
||||
);
|
||||
},
|
||||
).toList();
|
||||
|
||||
return ExpansionTile(
|
||||
tilePadding: EdgeInsets.zero,
|
||||
childrenPadding: EdgeInsets.zero,
|
||||
title: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
context.read<BoardBloc>().generateGroupNameFromGroup(group),
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
GestureDetector(
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.all(4),
|
||||
child: FlowySvg(
|
||||
FlowySvgs.hide_m,
|
||||
size: Size.square(20),
|
||||
),
|
||||
),
|
||||
onTap: () => showFlowyMobileConfirmDialog(
|
||||
context,
|
||||
title: FlowyText(LocaleKeys.board_mobile_showGroup.tr()),
|
||||
content: FlowyText(
|
||||
LocaleKeys.board_mobile_showGroupContent.tr(),
|
||||
),
|
||||
actionButtonTitle: LocaleKeys.button_yes.tr(),
|
||||
actionButtonColor: Theme.of(context).colorScheme.primary,
|
||||
onActionButtonPressed: () => context
|
||||
.read<BoardBloc>()
|
||||
.add(BoardEvent.setGroupVisibility(group, true)),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
children: cells,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -162,7 +162,7 @@ class _MobileRowDetailPageState extends State<MobileRowDetailPage> {
|
||||
}
|
||||
|
||||
deleteRow
|
||||
? RowBackendService.deleteRow(viewId, rowId)
|
||||
? RowBackendService.deleteRows(viewId, [rowId])
|
||||
: RowBackendService.duplicateRow(viewId, rowId);
|
||||
|
||||
context
|
||||
|
@ -48,7 +48,7 @@ class RelationCellBloc extends Bloc<RelationCellEvent, RelationCellState> {
|
||||
emit(state.copyWith(rows: const []));
|
||||
return;
|
||||
}
|
||||
final payload = RepeatedRowIdPB(
|
||||
final payload = GetRelatedRowDataPB(
|
||||
databaseId: state.relatedDatabaseMeta!.databaseId,
|
||||
rowIds: cellData.rowIds,
|
||||
);
|
||||
|
@ -1,6 +1,7 @@
|
||||
import 'package:appflowy/workspace/application/view/view_service.dart';
|
||||
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
||||
import 'package:appflowy_result/appflowy_result.dart';
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
@ -12,11 +13,9 @@ class RelationDatabaseListCubit extends Cubit<RelationDatabaseListState> {
|
||||
}
|
||||
|
||||
void _loadDatabaseMetas() async {
|
||||
final getDatabaseResult = await DatabaseEventGetDatabases().send();
|
||||
final metaPBs = getDatabaseResult.fold<List<DatabaseMetaPB>>(
|
||||
(s) => s.items,
|
||||
(f) => [],
|
||||
);
|
||||
final metaPBs = await DatabaseEventGetDatabases()
|
||||
.send()
|
||||
.fold<List<DatabaseMetaPB>>((s) => s.items, (f) => []);
|
||||
final futures = metaPBs.map((meta) {
|
||||
return ViewBackendService.getView(meta.inlineViewId).then(
|
||||
(result) => result.fold(
|
||||
|
@ -255,9 +255,7 @@ class RowCache {
|
||||
|
||||
RowInfo buildGridRow(RowMetaPB rowMetaPB) {
|
||||
return RowInfo(
|
||||
viewId: viewId,
|
||||
fields: _fieldDelegate.fieldInfos,
|
||||
rowId: rowMetaPB.id,
|
||||
rowMeta: rowMetaPB,
|
||||
);
|
||||
}
|
||||
@ -285,12 +283,13 @@ class RowChangesetNotifier extends ChangeNotifier {
|
||||
|
||||
@unfreezed
|
||||
class RowInfo with _$RowInfo {
|
||||
const RowInfo._();
|
||||
factory RowInfo({
|
||||
required String rowId,
|
||||
required String viewId,
|
||||
required UnmodifiableListView<FieldInfo> fields,
|
||||
required RowMetaPB rowMeta,
|
||||
}) = _RowInfo;
|
||||
|
||||
String get rowId => rowMeta.id;
|
||||
}
|
||||
|
||||
typedef InsertedIndexs = List<InsertedIndex>;
|
||||
|
@ -96,15 +96,15 @@ class RowBackendService {
|
||||
return DatabaseEventUpdateRowMeta(payload).send();
|
||||
}
|
||||
|
||||
static Future<FlowyResult<void, FlowyError>> deleteRow(
|
||||
static Future<FlowyResult<void, FlowyError>> deleteRows(
|
||||
String viewId,
|
||||
RowId rowId,
|
||||
List<RowId> rowIds,
|
||||
) {
|
||||
final payload = RowIdPB.create()
|
||||
final payload = RepeatedRowIdPB.create()
|
||||
..viewId = viewId
|
||||
..rowId = rowId;
|
||||
..rowIds.addAll(rowIds);
|
||||
|
||||
return DatabaseEventDeleteRow(payload).send();
|
||||
return DatabaseEventDeleteRows(payload).send();
|
||||
}
|
||||
|
||||
static Future<FlowyResult<void, FlowyError>> duplicateRow(
|
||||
|
@ -0,0 +1,93 @@
|
||||
import 'package:appflowy/plugins/database/application/database_controller.dart';
|
||||
import 'package:appflowy/plugins/database/board/application/board_bloc.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'board_actions_bloc.freezed.dart';
|
||||
|
||||
class BoardActionsCubit extends Cubit<BoardActionsState> {
|
||||
BoardActionsCubit({
|
||||
required this.databaseController,
|
||||
}) : super(const BoardActionsState.initial());
|
||||
|
||||
final DatabaseController databaseController;
|
||||
|
||||
void startEditingRow(GroupedRowId groupedRowId) {
|
||||
emit(BoardActionsState.startEditingRow(groupedRowId: groupedRowId));
|
||||
emit(const BoardActionsState.initial());
|
||||
}
|
||||
|
||||
void endEditing(GroupedRowId groupedRowId) {
|
||||
emit(const BoardActionsState.endEditingRow());
|
||||
emit(BoardActionsState.setFocus(groupedRowIds: [groupedRowId]));
|
||||
emit(const BoardActionsState.initial());
|
||||
}
|
||||
|
||||
void openCard(RowMetaPB rowMeta) {
|
||||
emit(BoardActionsState.openCard(rowMeta: rowMeta));
|
||||
emit(const BoardActionsState.initial());
|
||||
}
|
||||
|
||||
void openCardWithRowId(rowId) {
|
||||
final rowMeta = databaseController.rowCache.getRow(rowId)!.rowMeta;
|
||||
openCard(rowMeta);
|
||||
}
|
||||
|
||||
void setFocus(List<GroupedRowId> groupedRowIds) {
|
||||
emit(BoardActionsState.setFocus(groupedRowIds: groupedRowIds));
|
||||
emit(const BoardActionsState.initial());
|
||||
}
|
||||
|
||||
void startCreateBottomRow(String groupId) {
|
||||
emit(BoardActionsState.startCreateBottomRow(groupId: groupId));
|
||||
emit(const BoardActionsState.initial());
|
||||
}
|
||||
|
||||
void createRow(
|
||||
GroupedRowId? groupedRowId,
|
||||
CreateBoardCardRelativePosition relativePosition,
|
||||
) {
|
||||
emit(
|
||||
BoardActionsState.createRow(
|
||||
groupedRowId: groupedRowId,
|
||||
position: relativePosition,
|
||||
),
|
||||
);
|
||||
emit(const BoardActionsState.initial());
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
class BoardActionsState with _$BoardActionsState {
|
||||
const factory BoardActionsState.initial() = _BoardActionsInitialState;
|
||||
|
||||
const factory BoardActionsState.openCard({
|
||||
required RowMetaPB rowMeta,
|
||||
}) = _BoardActionsOpenCardState;
|
||||
|
||||
const factory BoardActionsState.startEditingRow({
|
||||
required GroupedRowId groupedRowId,
|
||||
}) = _BoardActionsStartEditingRowState;
|
||||
|
||||
const factory BoardActionsState.endEditingRow() =
|
||||
_BoardActionsEndEditingRowState;
|
||||
|
||||
const factory BoardActionsState.setFocus({
|
||||
required List<GroupedRowId> groupedRowIds,
|
||||
}) = _BoardActionsSetFocusState;
|
||||
|
||||
const factory BoardActionsState.startCreateBottomRow({
|
||||
required String groupId,
|
||||
}) = _BoardActionsStartCreateBottomRowState;
|
||||
|
||||
const factory BoardActionsState.createRow({
|
||||
required GroupedRowId? groupedRowId,
|
||||
required CreateBoardCardRelativePosition position,
|
||||
}) = _BoardActionCreateRowState;
|
||||
}
|
||||
|
||||
enum CreateBoardCardRelativePosition {
|
||||
before,
|
||||
after,
|
||||
}
|
@ -8,10 +8,11 @@ import 'package:appflowy/plugins/database/application/row/row_service.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:appflowy_board/appflowy_board.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart' hide Log;
|
||||
import 'package:appflowy_result/appflowy_result.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
@ -27,95 +28,108 @@ part 'board_bloc.freezed.dart';
|
||||
|
||||
class BoardBloc extends Bloc<BoardEvent, BoardState> {
|
||||
BoardBloc({
|
||||
required ViewPB view,
|
||||
required this.databaseController,
|
||||
}) : super(BoardState.initial(view.id)) {
|
||||
this.didCreateRow,
|
||||
AppFlowyBoardController? boardController,
|
||||
}) : super(const BoardState.loading()) {
|
||||
groupBackendSvc = GroupBackendService(viewId);
|
||||
boardController = AppFlowyBoardController(
|
||||
onMoveGroup: (fromGroupId, fromIndex, toGroupId, toIndex) =>
|
||||
databaseController.moveGroup(
|
||||
fromGroupId: fromGroupId,
|
||||
toGroupId: toGroupId,
|
||||
),
|
||||
onMoveGroupItem: (groupId, fromIndex, toIndex) {
|
||||
final fromRow = groupControllers[groupId]?.rowAtIndex(fromIndex);
|
||||
final toRow = groupControllers[groupId]?.rowAtIndex(toIndex);
|
||||
if (fromRow != null) {
|
||||
databaseController.moveGroupRow(
|
||||
fromRow: fromRow,
|
||||
toRow: toRow,
|
||||
fromGroupId: groupId,
|
||||
toGroupId: groupId,
|
||||
);
|
||||
}
|
||||
},
|
||||
onMoveGroupItemToGroup: (fromGroupId, fromIndex, toGroupId, toIndex) {
|
||||
final fromRow = groupControllers[fromGroupId]?.rowAtIndex(fromIndex);
|
||||
final toRow = groupControllers[toGroupId]?.rowAtIndex(toIndex);
|
||||
if (fromRow != null) {
|
||||
databaseController.moveGroupRow(
|
||||
fromRow: fromRow,
|
||||
toRow: toRow,
|
||||
fromGroupId: fromGroupId,
|
||||
toGroupId: toGroupId,
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
_initBoardController(boardController);
|
||||
_dispatch();
|
||||
}
|
||||
|
||||
final DatabaseController databaseController;
|
||||
late final AppFlowyBoardController boardController;
|
||||
final LinkedHashMap<String, GroupController> groupControllers =
|
||||
LinkedHashMap();
|
||||
final List<GroupPB> groupList = [];
|
||||
|
||||
late final AppFlowyBoardController boardController;
|
||||
final ValueNotifier<DidCreateRowResult?>? didCreateRow;
|
||||
|
||||
late final GroupBackendService groupBackendSvc;
|
||||
|
||||
FieldController get fieldController => databaseController.fieldController;
|
||||
String get viewId => databaseController.viewId;
|
||||
|
||||
void _initBoardController(AppFlowyBoardController? controller) {
|
||||
boardController = controller ??
|
||||
AppFlowyBoardController(
|
||||
onMoveGroup: (fromGroupId, fromIndex, toGroupId, toIndex) =>
|
||||
databaseController.moveGroup(
|
||||
fromGroupId: fromGroupId,
|
||||
toGroupId: toGroupId,
|
||||
),
|
||||
onMoveGroupItem: (groupId, fromIndex, toIndex) {
|
||||
final fromRow = groupControllers[groupId]?.rowAtIndex(fromIndex);
|
||||
final toRow = groupControllers[groupId]?.rowAtIndex(toIndex);
|
||||
if (fromRow != null) {
|
||||
databaseController.moveGroupRow(
|
||||
fromRow: fromRow,
|
||||
toRow: toRow,
|
||||
fromGroupId: groupId,
|
||||
toGroupId: groupId,
|
||||
);
|
||||
}
|
||||
},
|
||||
onMoveGroupItemToGroup: (fromGroupId, fromIndex, toGroupId, toIndex) {
|
||||
final fromRow =
|
||||
groupControllers[fromGroupId]?.rowAtIndex(fromIndex);
|
||||
final toRow = groupControllers[toGroupId]?.rowAtIndex(toIndex);
|
||||
if (fromRow != null) {
|
||||
databaseController.moveGroupRow(
|
||||
fromRow: fromRow,
|
||||
toRow: toRow,
|
||||
fromGroupId: fromGroupId,
|
||||
toGroupId: toGroupId,
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _dispatch() {
|
||||
on<BoardEvent>(
|
||||
(event, emit) async {
|
||||
await event.when(
|
||||
initial: () async {
|
||||
emit(BoardState.initial(viewId));
|
||||
_startListening();
|
||||
await _openGrid(emit);
|
||||
await _openDatabase(emit);
|
||||
},
|
||||
createHeaderRow: (groupId) async {
|
||||
final rowId = groupControllers[groupId]?.firstRow()?.id;
|
||||
final position = rowId == null
|
||||
? OrderObjectPositionTypePB.Start
|
||||
: OrderObjectPositionTypePB.Before;
|
||||
createRow: (groupId, position, title, targetRowId) async {
|
||||
final primaryField = databaseController.fieldController.fieldInfos
|
||||
.firstWhereOrNull((element) => element.isPrimary)!;
|
||||
final void Function(RowDataBuilder)? cellBuilder = title == null
|
||||
? null
|
||||
: (builder) => builder.insertText(primaryField, title);
|
||||
|
||||
final result = await RowBackendService.createRow(
|
||||
viewId: databaseController.viewId,
|
||||
groupId: groupId,
|
||||
position: position,
|
||||
targetRowId: rowId,
|
||||
targetRowId: targetRowId,
|
||||
withCells: cellBuilder,
|
||||
);
|
||||
|
||||
result.fold(
|
||||
(rowMeta) => emit(state.copyWith(recentAddedRowMeta: rowMeta)),
|
||||
(err) => Log.error(err),
|
||||
);
|
||||
},
|
||||
createBottomRow: (groupId) async {
|
||||
final rowId = groupControllers[groupId]?.lastRow()?.id;
|
||||
final position = rowId == null
|
||||
? OrderObjectPositionTypePB.End
|
||||
: OrderObjectPositionTypePB.After;
|
||||
final result = await RowBackendService.createRow(
|
||||
viewId: databaseController.viewId,
|
||||
groupId: groupId,
|
||||
position: position,
|
||||
targetRowId: rowId,
|
||||
);
|
||||
final startEditing = position != OrderObjectPositionTypePB.End;
|
||||
final action = PlatformExtension.isMobile
|
||||
? DidCreateRowAction.openAsPage
|
||||
: startEditing
|
||||
? DidCreateRowAction.startEditing
|
||||
: DidCreateRowAction.none;
|
||||
|
||||
result.fold(
|
||||
(rowMeta) => emit(state.copyWith(recentAddedRowMeta: rowMeta)),
|
||||
(rowMeta) {
|
||||
state.maybeMap(
|
||||
ready: (value) {
|
||||
didCreateRow?.value = DidCreateRowResult(
|
||||
action: action,
|
||||
rowMeta: rowMeta,
|
||||
groupId: groupId,
|
||||
);
|
||||
},
|
||||
orElse: () {},
|
||||
);
|
||||
},
|
||||
(err) => Log.error(err),
|
||||
);
|
||||
},
|
||||
@ -127,89 +141,62 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
|
||||
final result = await groupBackendSvc.deleteGroup(groupId: groupId);
|
||||
result.fold((_) {}, (err) => Log.error(err));
|
||||
},
|
||||
didCreateRow: (group, row, int? index) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
isEditingRow: true,
|
||||
editingRow: BoardEditingRow(
|
||||
group: group,
|
||||
row: row,
|
||||
index: index,
|
||||
),
|
||||
),
|
||||
);
|
||||
_groupItemStartEditing(group, row, true);
|
||||
},
|
||||
didReceiveGridUpdate: (DatabasePB grid) {
|
||||
emit(state.copyWith(grid: grid));
|
||||
},
|
||||
didReceiveError: (FlowyError error) {
|
||||
emit(state.copyWith(noneOrError: error));
|
||||
didReceiveError: (error) {
|
||||
emit(BoardState.error(error: error));
|
||||
},
|
||||
didReceiveGroups: (List<GroupPB> groups) {
|
||||
final hiddenGroups = _filterHiddenGroups(hideUngrouped, groups);
|
||||
emit(
|
||||
state.copyWith(
|
||||
hiddenGroups: hiddenGroups,
|
||||
groupIds: groups.map((group) => group.groupId).toList(),
|
||||
),
|
||||
state.maybeMap(
|
||||
ready: (state) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
hiddenGroups: _filterHiddenGroups(hideUngrouped, groups),
|
||||
groupIds: groups.map((group) => group.groupId).toList(),
|
||||
),
|
||||
);
|
||||
},
|
||||
orElse: () {},
|
||||
);
|
||||
},
|
||||
didUpdateLayoutSettings: (layoutSettings) {
|
||||
final hiddenGroups = _filterHiddenGroups(hideUngrouped, groupList);
|
||||
emit(
|
||||
state.copyWith(
|
||||
layoutSettings: layoutSettings,
|
||||
hiddenGroups: hiddenGroups,
|
||||
),
|
||||
state.maybeMap(
|
||||
ready: (state) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
layoutSettings: layoutSettings,
|
||||
hiddenGroups: _filterHiddenGroups(hideUngrouped, groupList),
|
||||
),
|
||||
);
|
||||
},
|
||||
orElse: () {},
|
||||
);
|
||||
},
|
||||
toggleGroupVisibility: (GroupPB group, bool isVisible) async {
|
||||
await _toggleGroupVisibility(group, isVisible);
|
||||
setGroupVisibility: (GroupPB group, bool isVisible) async {
|
||||
await _setGroupVisibility(group, isVisible);
|
||||
},
|
||||
toggleHiddenSectionVisibility: (isVisible) async {
|
||||
final newLayoutSettings = state.layoutSettings!;
|
||||
newLayoutSettings.freeze();
|
||||
await state.maybeMap(
|
||||
ready: (state) async {
|
||||
final newLayoutSettings = state.layoutSettings!;
|
||||
newLayoutSettings.freeze();
|
||||
|
||||
final newLayoutSetting = newLayoutSettings.rebuild(
|
||||
(message) => message.collapseHiddenGroups = isVisible,
|
||||
);
|
||||
final newLayoutSetting = newLayoutSettings.rebuild(
|
||||
(message) => message.collapseHiddenGroups = isVisible,
|
||||
);
|
||||
|
||||
await databaseController.updateLayoutSetting(
|
||||
boardLayoutSetting: newLayoutSetting,
|
||||
await databaseController.updateLayoutSetting(
|
||||
boardLayoutSetting: newLayoutSetting,
|
||||
);
|
||||
},
|
||||
orElse: () {},
|
||||
);
|
||||
},
|
||||
reorderGroup: (fromGroupId, toGroupId) async {
|
||||
_reorderGroup(fromGroupId, toGroupId, emit);
|
||||
},
|
||||
startEditingRow: (group, row) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
isEditingRow: true,
|
||||
editingRow: BoardEditingRow(
|
||||
group: group,
|
||||
row: row,
|
||||
index: null,
|
||||
),
|
||||
),
|
||||
);
|
||||
_groupItemStartEditing(group, row, true);
|
||||
},
|
||||
endEditingRow: (rowId) {
|
||||
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, editingRow: null));
|
||||
}
|
||||
},
|
||||
startEditingHeader: (String groupId) {
|
||||
emit(
|
||||
state.copyWith(isEditingHeader: true, editingHeaderId: groupId),
|
||||
state.maybeMap(
|
||||
ready: (state) => emit(state.copyWith(editingHeaderId: groupId)),
|
||||
orElse: () {},
|
||||
);
|
||||
},
|
||||
endEditingHeader: (String groupId, String? groupName) async {
|
||||
@ -218,41 +205,79 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
|
||||
groupId: groupId,
|
||||
name: groupName,
|
||||
);
|
||||
emit(state.copyWith(isEditingHeader: false));
|
||||
state.maybeMap(
|
||||
ready: (state) => emit(state.copyWith(editingHeaderId: null)),
|
||||
orElse: () {},
|
||||
);
|
||||
},
|
||||
deleteCards: (groupedRowIds) async {
|
||||
final rowIds = groupedRowIds.map((e) => e.rowId).toList();
|
||||
await RowBackendService.deleteRows(viewId, rowIds);
|
||||
},
|
||||
moveGroupToAdjacentGroup: (groupedRowId, toPrevious) async {
|
||||
final fromRow =
|
||||
databaseController.rowCache.getRow(groupedRowId.rowId)?.rowMeta;
|
||||
final currentGroupIndex =
|
||||
boardController.groupIds.indexOf(groupedRowId.groupId);
|
||||
final toGroupIndex =
|
||||
toPrevious ? currentGroupIndex - 1 : currentGroupIndex + 1;
|
||||
if (fromRow != null &&
|
||||
toGroupIndex > -1 &&
|
||||
toGroupIndex < boardController.groupIds.length) {
|
||||
final toGroupId = boardController.groupDatas[toGroupIndex].id;
|
||||
final result = await databaseController.moveGroupRow(
|
||||
fromRow: fromRow,
|
||||
fromGroupId: groupedRowId.groupId,
|
||||
toGroupId: toGroupId,
|
||||
);
|
||||
result.fold(
|
||||
(s) {
|
||||
final previousState = state;
|
||||
emit(
|
||||
BoardState.setFocus(
|
||||
groupedRowIds: [
|
||||
GroupedRowId(
|
||||
groupId: toGroupId,
|
||||
rowId: groupedRowId.rowId,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
emit(previousState);
|
||||
},
|
||||
(f) {},
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _groupItemStartEditing(GroupPB group, RowMetaPB row, bool isEdit) {
|
||||
final fieldInfo = fieldController.getField(group.fieldId);
|
||||
if (fieldInfo == null) {
|
||||
return Log.warn("fieldInfo should not be null");
|
||||
}
|
||||
|
||||
boardController.enableGroupDragging(!isEdit);
|
||||
}
|
||||
|
||||
Future<void> _toggleGroupVisibility(GroupPB group, bool isVisible) async {
|
||||
Future<void> _setGroupVisibility(GroupPB group, bool isVisible) async {
|
||||
if (group.isDefault) {
|
||||
final newLayoutSettings = state.layoutSettings!;
|
||||
newLayoutSettings.freeze();
|
||||
await state.maybeMap(
|
||||
ready: (state) async {
|
||||
final newLayoutSettings = state.layoutSettings!;
|
||||
newLayoutSettings.freeze();
|
||||
|
||||
final newLayoutSetting = newLayoutSettings.rebuild(
|
||||
(message) => message.hideUngroupedColumn = !isVisible,
|
||||
final newLayoutSetting = newLayoutSettings.rebuild(
|
||||
(message) => message.hideUngroupedColumn = !isVisible,
|
||||
);
|
||||
|
||||
await databaseController.updateLayoutSetting(
|
||||
boardLayoutSetting: newLayoutSetting,
|
||||
);
|
||||
},
|
||||
orElse: () {},
|
||||
);
|
||||
|
||||
return databaseController.updateLayoutSetting(
|
||||
boardLayoutSetting: newLayoutSetting,
|
||||
} else {
|
||||
await groupBackendSvc.updateGroup(
|
||||
fieldId: groupControllers.values.first.group.fieldId,
|
||||
groupId: group.groupId,
|
||||
visible: isVisible,
|
||||
);
|
||||
}
|
||||
|
||||
await groupBackendSvc.updateGroup(
|
||||
fieldId: groupControllers.values.first.group.fieldId,
|
||||
groupId: group.groupId,
|
||||
visible: isVisible,
|
||||
);
|
||||
}
|
||||
|
||||
void _reorderGroup(
|
||||
@ -277,6 +302,7 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
|
||||
for (final controller in groupControllers.values) {
|
||||
await controller.dispose();
|
||||
}
|
||||
boardController.dispose();
|
||||
return super.close();
|
||||
}
|
||||
|
||||
@ -284,11 +310,13 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
|
||||
databaseController.databaseLayoutSetting?.board.hideUngroupedColumn ??
|
||||
false;
|
||||
|
||||
FieldType get groupingFieldType {
|
||||
final fieldInfo =
|
||||
databaseController.fieldController.getField(groupList.first.fieldId)!;
|
||||
|
||||
return fieldInfo.fieldType;
|
||||
FieldType? get groupingFieldType {
|
||||
if (groupList.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
return databaseController.fieldController
|
||||
.getField(groupList.first.fieldId)
|
||||
?.fieldType;
|
||||
}
|
||||
|
||||
void initializeGroups(List<GroupPB> groups) {
|
||||
@ -321,16 +349,9 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
|
||||
}
|
||||
}
|
||||
|
||||
RowCache? getRowCache() => databaseController.rowCache;
|
||||
RowCache get rowCache => databaseController.rowCache;
|
||||
|
||||
void _startListening() {
|
||||
final onDatabaseChanged = DatabaseCallbacks(
|
||||
onDatabaseChanged: (database) {
|
||||
if (!isClosed) {
|
||||
add(BoardEvent.didReceiveGridUpdate(database));
|
||||
}
|
||||
},
|
||||
);
|
||||
final onLayoutSettingsChanged = DatabaseLayoutSettingCallbacks(
|
||||
onLayoutSettingsChanged: (layoutSettings) {
|
||||
if (isClosed) {
|
||||
@ -433,7 +454,6 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
|
||||
);
|
||||
|
||||
databaseController.addListener(
|
||||
onDatabaseChanged: onDatabaseChanged,
|
||||
onLayoutSettingsChanged: onLayoutSettingsChanged,
|
||||
onGroupChanged: onGroupChanged,
|
||||
);
|
||||
@ -451,31 +471,18 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
|
||||
return <AppFlowyGroupItem>[...items];
|
||||
}
|
||||
|
||||
Future<void> _openGrid(Emitter<BoardState> emit) async {
|
||||
final result = await databaseController.open();
|
||||
result.fold(
|
||||
(grid) {
|
||||
databaseController.setIsLoading(false);
|
||||
emit(
|
||||
state.copyWith(
|
||||
loadingState: LoadingState.finish(FlowyResult.success(null)),
|
||||
),
|
||||
Future<void> _openDatabase(Emitter<BoardState> emit) {
|
||||
return databaseController.open().fold(
|
||||
(datbasePB) => databaseController.setIsLoading(false),
|
||||
(err) => emit(BoardState.error(error: err)),
|
||||
);
|
||||
},
|
||||
(err) => emit(
|
||||
state.copyWith(
|
||||
loadingState: LoadingState.finish(FlowyResult.failure(err)),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
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) {},
|
||||
);
|
||||
|
||||
final controller = GroupController(
|
||||
@ -579,71 +586,77 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
|
||||
@freezed
|
||||
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.createRow(
|
||||
String groupId,
|
||||
OrderObjectPositionTypePB position,
|
||||
String? title,
|
||||
String? targetRowId,
|
||||
) = _CreateRow;
|
||||
const factory BoardEvent.createGroup(String name) = _CreateGroup;
|
||||
const factory BoardEvent.startEditingHeader(String groupId) =
|
||||
_StartEditingHeader;
|
||||
const factory BoardEvent.endEditingHeader(String groupId, String? groupName) =
|
||||
_EndEditingHeader;
|
||||
const factory BoardEvent.didCreateRow(
|
||||
GroupPB group,
|
||||
RowMetaPB row,
|
||||
int? index,
|
||||
) = _DidCreateRow;
|
||||
const factory BoardEvent.startEditingRow(
|
||||
GroupPB group,
|
||||
RowMetaPB row,
|
||||
) = _StartEditRow;
|
||||
const factory BoardEvent.endEditingRow(RowId rowId) = _EndEditRow;
|
||||
const factory BoardEvent.toggleGroupVisibility(
|
||||
const factory BoardEvent.setGroupVisibility(
|
||||
GroupPB group,
|
||||
bool isVisible,
|
||||
) = _ToggleGroupVisibility;
|
||||
) = _SetGroupVisibility;
|
||||
const factory BoardEvent.toggleHiddenSectionVisibility(bool isVisible) =
|
||||
_ToggleHiddenSectionVisibility;
|
||||
const factory BoardEvent.deleteGroup(String groupId) = _DeleteGroup;
|
||||
const factory BoardEvent.reorderGroup(String fromGroupId, String toGroupId) =
|
||||
_ReorderGroup;
|
||||
const factory BoardEvent.didReceiveError(FlowyError error) = _DidReceiveError;
|
||||
const factory BoardEvent.didReceiveGridUpdate(
|
||||
DatabasePB grid,
|
||||
) = _DidReceiveGridUpdate;
|
||||
const factory BoardEvent.didReceiveGroups(List<GroupPB> groups) =
|
||||
_DidReceiveGroups;
|
||||
const factory BoardEvent.didUpdateLayoutSettings(
|
||||
BoardLayoutSettingPB layoutSettings,
|
||||
) = _DidUpdateLayoutSettings;
|
||||
const factory BoardEvent.deleteCards(List<GroupedRowId> groupedRowIds) =
|
||||
_DeleteCards;
|
||||
const factory BoardEvent.moveGroupToAdjacentGroup(
|
||||
GroupedRowId groupedRowId,
|
||||
bool toPrevious,
|
||||
) = _MoveGroupToAdjacentGroup;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class BoardState with _$BoardState {
|
||||
const factory BoardState({
|
||||
const BoardState._();
|
||||
|
||||
const factory BoardState.loading() = _BoardLoadingState;
|
||||
|
||||
const factory BoardState.error({
|
||||
required FlowyError error,
|
||||
}) = _BoardErrorState;
|
||||
|
||||
const factory BoardState.ready({
|
||||
required String viewId,
|
||||
required DatabasePB? grid,
|
||||
required List<String> groupIds,
|
||||
required bool isEditingHeader,
|
||||
required bool isEditingRow,
|
||||
required LoadingState loadingState,
|
||||
required FlowyError? noneOrError,
|
||||
required BoardLayoutSettingPB? layoutSettings,
|
||||
String? editingHeaderId,
|
||||
BoardEditingRow? editingRow,
|
||||
RowMetaPB? recentAddedRowMeta,
|
||||
required List<GroupPB> hiddenGroups,
|
||||
}) = _BoardState;
|
||||
String? editingHeaderId,
|
||||
}) = _BoardReadyState;
|
||||
|
||||
factory BoardState.initial(String viewId) => BoardState(
|
||||
grid: null,
|
||||
const factory BoardState.setFocus({
|
||||
required List<GroupedRowId> groupedRowIds,
|
||||
}) = _BoardSetFocusState;
|
||||
|
||||
factory BoardState.initial(String viewId) => BoardState.ready(
|
||||
viewId: viewId,
|
||||
groupIds: [],
|
||||
isEditingHeader: false,
|
||||
isEditingRow: false,
|
||||
noneOrError: null,
|
||||
loadingState: const LoadingState.loading(),
|
||||
layoutSettings: null,
|
||||
hiddenGroups: [],
|
||||
);
|
||||
|
||||
bool get isLoading => maybeMap(loading: (_) => true, orElse: () => false);
|
||||
bool get isError => maybeMap(error: (_) => true, orElse: () => false);
|
||||
bool get isReady => maybeMap(ready: (_) => true, orElse: () => false);
|
||||
bool get isSetFocus => maybeMap(setFocus: (_) => true, orElse: () => false);
|
||||
}
|
||||
|
||||
List<GroupPB> _filterHiddenGroups(bool hideUngrouped, List<GroupPB> groups) {
|
||||
@ -658,7 +671,7 @@ class GroupItem extends AppFlowyGroupItem {
|
||||
required this.fieldInfo,
|
||||
bool draggable = true,
|
||||
}) {
|
||||
super.draggable = draggable;
|
||||
super.draggable.value = draggable;
|
||||
}
|
||||
|
||||
final RowMetaPB row;
|
||||
@ -668,6 +681,23 @@ class GroupItem extends AppFlowyGroupItem {
|
||||
String get id => row.id.toString();
|
||||
}
|
||||
|
||||
/// Identifies a card in a database view that has grouping. To support cases
|
||||
/// in which a card can belong to more than one group at the same time (e.g.
|
||||
/// FieldType.Multiselect), we include the card's group id as well.
|
||||
///
|
||||
class GroupedRowId extends Equatable {
|
||||
const GroupedRowId({
|
||||
required this.rowId,
|
||||
required this.groupId,
|
||||
});
|
||||
|
||||
final String rowId;
|
||||
final String groupId;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [rowId, groupId];
|
||||
}
|
||||
|
||||
class GroupControllerDelegateImpl extends GroupControllerDelegate {
|
||||
GroupControllerDelegateImpl({
|
||||
required this.controller,
|
||||
@ -743,18 +773,6 @@ class GroupControllerDelegateImpl extends GroupControllerDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
class BoardEditingRow {
|
||||
BoardEditingRow({
|
||||
required this.group,
|
||||
required this.row,
|
||||
required this.index,
|
||||
});
|
||||
|
||||
GroupPB group;
|
||||
RowMetaPB row;
|
||||
int? index;
|
||||
}
|
||||
|
||||
class GroupData {
|
||||
GroupData({
|
||||
required this.group,
|
||||
@ -779,3 +797,21 @@ class CheckboxGroup {
|
||||
// pub const CHECK: &str = "Yes";
|
||||
bool get isCheck => group.groupId == "Yes";
|
||||
}
|
||||
|
||||
enum DidCreateRowAction {
|
||||
none,
|
||||
openAsPage,
|
||||
startEditing,
|
||||
}
|
||||
|
||||
class DidCreateRowResult {
|
||||
DidCreateRowResult({
|
||||
required this.action,
|
||||
required this.rowMeta,
|
||||
required this.groupId,
|
||||
});
|
||||
|
||||
final DidCreateRowAction action;
|
||||
final RowMetaPB rowMeta;
|
||||
final String groupId;
|
||||
}
|
||||
|
@ -1,12 +1,11 @@
|
||||
import 'dart:collection';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/mobile/presentation/database/board/mobile_board_content.dart';
|
||||
import 'package:appflowy/mobile/presentation/widgets/flowy_mobile_state_container.dart';
|
||||
import 'package:appflowy/mobile/presentation/database/board/mobile_board_page.dart';
|
||||
import 'package:appflowy/plugins/database/application/database_controller.dart';
|
||||
import 'package:appflowy/plugins/database/application/row/row_cache.dart';
|
||||
import 'package:appflowy/plugins/database/application/row/row_controller.dart';
|
||||
import 'package:appflowy/plugins/database/board/application/board_actions_bloc.dart';
|
||||
import 'package:appflowy/plugins/database/board/presentation/widgets/board_column_header.dart';
|
||||
import 'package:appflowy/plugins/database/grid/presentation/grid_page.dart';
|
||||
import 'package:appflowy/plugins/database/grid/presentation/widgets/header/field_type_extension.dart';
|
||||
@ -15,7 +14,8 @@ import 'package:appflowy/plugins/database/tab_bar/tab_bar_view.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/card/card_bloc.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/cell/card_cell_style_maps/desktop_board_card_cell_style.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/row/row_detail.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/row_entities.pb.dart';
|
||||
import 'package:appflowy/shared/conditional_listenable_builder.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:appflowy_board/appflowy_board.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
@ -28,12 +28,13 @@ import 'package:flutter/material.dart' hide Card;
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import '../../../../workspace/application/view/view_bloc.dart';
|
||||
import '../../widgets/card/card.dart';
|
||||
import '../../widgets/cell/card_cell_builder.dart';
|
||||
import '../application/board_bloc.dart';
|
||||
import 'toolbar/board_setting_bar.dart';
|
||||
import 'widgets/board_focus_scope.dart';
|
||||
import 'widgets/board_hidden_groups.dart';
|
||||
import 'widgets/board_shortcut_container.dart';
|
||||
|
||||
class BoardPageTabBarBuilderImpl extends DatabaseTabBarItemBuilder {
|
||||
final _toggleExtension = ToggleExtensionNotifier();
|
||||
@ -46,7 +47,17 @@ class BoardPageTabBarBuilderImpl extends DatabaseTabBarItemBuilder {
|
||||
bool shrinkWrap,
|
||||
String? initialRowId,
|
||||
) =>
|
||||
BoardPage(view: view, databaseController: controller);
|
||||
PlatformExtension.isDesktop
|
||||
? DesktopBoardPage(
|
||||
key: _makeValueKey(controller),
|
||||
view: view,
|
||||
databaseController: controller,
|
||||
)
|
||||
: MobileBoardPage(
|
||||
key: _makeValueKey(controller),
|
||||
view: view,
|
||||
databaseController: controller,
|
||||
);
|
||||
|
||||
@override
|
||||
Widget settingBar(BuildContext context, DatabaseController controller) =>
|
||||
@ -79,12 +90,13 @@ class BoardPageTabBarBuilderImpl extends DatabaseTabBarItemBuilder {
|
||||
ValueKey(controller.viewId);
|
||||
}
|
||||
|
||||
class BoardPage extends StatelessWidget {
|
||||
BoardPage({
|
||||
class DesktopBoardPage extends StatefulWidget {
|
||||
const DesktopBoardPage({
|
||||
super.key,
|
||||
required this.view,
|
||||
required this.databaseController,
|
||||
this.onEditStateChanged,
|
||||
}) : super(key: ValueKey(view.id));
|
||||
});
|
||||
|
||||
final ViewPB view;
|
||||
|
||||
@ -93,54 +105,154 @@ class BoardPage extends StatelessWidget {
|
||||
/// Called when edit state changed
|
||||
final VoidCallback? onEditStateChanged;
|
||||
|
||||
@override
|
||||
State<DesktopBoardPage> createState() => _DesktopBoardPageState();
|
||||
}
|
||||
|
||||
class _DesktopBoardPageState extends State<DesktopBoardPage> {
|
||||
late final AppFlowyBoardController _boardController = AppFlowyBoardController(
|
||||
onMoveGroup: (fromGroupId, fromIndex, toGroupId, toIndex) =>
|
||||
widget.databaseController.moveGroup(
|
||||
fromGroupId: fromGroupId,
|
||||
toGroupId: toGroupId,
|
||||
),
|
||||
onMoveGroupItem: (groupId, fromIndex, toIndex) {
|
||||
final groupControllers = _boardBloc.groupControllers;
|
||||
final fromRow = groupControllers[groupId]?.rowAtIndex(fromIndex);
|
||||
final toRow = groupControllers[groupId]?.rowAtIndex(toIndex);
|
||||
if (fromRow != null) {
|
||||
widget.databaseController.moveGroupRow(
|
||||
fromRow: fromRow,
|
||||
toRow: toRow,
|
||||
fromGroupId: groupId,
|
||||
toGroupId: groupId,
|
||||
);
|
||||
}
|
||||
},
|
||||
onMoveGroupItemToGroup: (fromGroupId, fromIndex, toGroupId, toIndex) {
|
||||
final groupControllers = _boardBloc.groupControllers;
|
||||
final fromRow = groupControllers[fromGroupId]?.rowAtIndex(fromIndex);
|
||||
final toRow = groupControllers[toGroupId]?.rowAtIndex(toIndex);
|
||||
if (fromRow != null) {
|
||||
widget.databaseController.moveGroupRow(
|
||||
fromRow: fromRow,
|
||||
toRow: toRow,
|
||||
fromGroupId: fromGroupId,
|
||||
toGroupId: toGroupId,
|
||||
);
|
||||
}
|
||||
},
|
||||
onStartDraggingCard: (groupId, index) {
|
||||
final groupControllers = _boardBloc.groupControllers;
|
||||
final toRow = groupControllers[groupId]?.rowAtIndex(index);
|
||||
if (toRow != null) {
|
||||
_focusScope.clear();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
late final _focusScope = BoardFocusScope(
|
||||
boardController: _boardController,
|
||||
);
|
||||
late final BoardBloc _boardBloc;
|
||||
late final BoardActionsCubit _boardActionsCubit;
|
||||
late final ValueNotifier<DidCreateRowResult?> _didCreateRow;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_didCreateRow = ValueNotifier(null)..addListener(_handleDidCreateRow);
|
||||
_boardBloc = BoardBloc(
|
||||
databaseController: widget.databaseController,
|
||||
didCreateRow: _didCreateRow,
|
||||
boardController: _boardController,
|
||||
)..add(const BoardEvent.initial());
|
||||
_boardActionsCubit = BoardActionsCubit(
|
||||
databaseController: widget.databaseController,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_focusScope.dispose();
|
||||
_boardBloc.close();
|
||||
_boardActionsCubit.close();
|
||||
_didCreateRow
|
||||
..removeListener(_handleDidCreateRow)
|
||||
..dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider<BoardBloc>(
|
||||
create: (context) => BoardBloc(
|
||||
view: view,
|
||||
databaseController: databaseController,
|
||||
)..add(const BoardEvent.initial()),
|
||||
return MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider<BoardBloc>.value(
|
||||
value: _boardBloc,
|
||||
),
|
||||
BlocProvider.value(
|
||||
value: _boardActionsCubit,
|
||||
),
|
||||
],
|
||||
child: BlocBuilder<BoardBloc, BoardState>(
|
||||
buildWhen: (p, c) => p.loadingState != c.loadingState,
|
||||
builder: (context, state) => state.loadingState.when(
|
||||
loading: () => const Center(
|
||||
builder: (context, state) => state.maybeMap(
|
||||
loading: (_) => const Center(
|
||||
child: CircularProgressIndicator.adaptive(),
|
||||
),
|
||||
idle: () => const SizedBox.shrink(),
|
||||
finish: (result) => result.fold(
|
||||
(_) => PlatformExtension.isMobile
|
||||
? const MobileBoardContent()
|
||||
: DesktopBoardContent(onEditStateChanged: onEditStateChanged),
|
||||
(err) => PlatformExtension.isMobile
|
||||
? FlowyMobileStateContainer.error(
|
||||
emoji: '🛸',
|
||||
title: LocaleKeys.board_mobile_failedToLoad.tr(),
|
||||
errorMsg: err.toString(),
|
||||
)
|
||||
: FlowyErrorPage.message(
|
||||
err.toString(),
|
||||
howToFix: LocaleKeys.errorDialog_howToFixFallback.tr(),
|
||||
),
|
||||
error: (err) => FlowyErrorPage.message(
|
||||
err.toString(),
|
||||
howToFix: LocaleKeys.errorDialog_howToFixFallback.tr(),
|
||||
),
|
||||
orElse: () => _BoardContent(
|
||||
onEditStateChanged: widget.onEditStateChanged,
|
||||
focusScope: _focusScope,
|
||||
boardController: _boardController,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _handleDidCreateRow() async {
|
||||
// work around: wait for the new card to be inserted into the board before enabling edit
|
||||
await Future.delayed(const Duration(milliseconds: 50));
|
||||
if (_didCreateRow.value != null) {
|
||||
final result = _didCreateRow.value!;
|
||||
switch (result.action) {
|
||||
case DidCreateRowAction.openAsPage:
|
||||
_boardActionsCubit.openCard(result.rowMeta);
|
||||
break;
|
||||
case DidCreateRowAction.startEditing:
|
||||
_boardActionsCubit.startEditingRow(
|
||||
GroupedRowId(
|
||||
groupId: result.groupId,
|
||||
rowId: result.rowMeta.id,
|
||||
),
|
||||
);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class DesktopBoardContent extends StatefulWidget {
|
||||
const DesktopBoardContent({
|
||||
super.key,
|
||||
class _BoardContent extends StatefulWidget {
|
||||
const _BoardContent({
|
||||
required this.boardController,
|
||||
required this.focusScope,
|
||||
this.onEditStateChanged,
|
||||
});
|
||||
|
||||
final AppFlowyBoardController boardController;
|
||||
final BoardFocusScope focusScope;
|
||||
final VoidCallback? onEditStateChanged;
|
||||
|
||||
@override
|
||||
State<DesktopBoardContent> createState() => _DesktopBoardContentState();
|
||||
State<_BoardContent> createState() => _BoardContentState();
|
||||
}
|
||||
|
||||
class _DesktopBoardContentState extends State<DesktopBoardContent> {
|
||||
class _BoardContentState extends State<_BoardContent> {
|
||||
final ScrollController scrollController = ScrollController();
|
||||
final AppFlowyBoardScrollController scrollManager =
|
||||
AppFlowyBoardScrollController();
|
||||
@ -148,16 +260,19 @@ class _DesktopBoardContentState extends State<DesktopBoardContent> {
|
||||
final config = const AppFlowyBoardConfig(
|
||||
groupMargin: EdgeInsets.symmetric(horizontal: 4),
|
||||
groupBodyPadding: EdgeInsets.symmetric(horizontal: 4),
|
||||
groupFooterPadding: EdgeInsets.fromLTRB(4, 14, 4, 4),
|
||||
groupFooterPadding: EdgeInsets.fromLTRB(8, 14, 8, 4),
|
||||
groupHeaderPadding: EdgeInsets.symmetric(horizontal: 8),
|
||||
cardMargin: EdgeInsets.symmetric(horizontal: 4, vertical: 3),
|
||||
stretchGroupHeight: false,
|
||||
);
|
||||
|
||||
late final cellBuilder = CardCellBuilder(
|
||||
databaseController: context.read<BoardBloc>().databaseController,
|
||||
databaseController: databaseController,
|
||||
);
|
||||
|
||||
DatabaseController get databaseController =>
|
||||
context.read<BoardBloc>().databaseController;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
scrollController.dispose();
|
||||
@ -166,15 +281,49 @@ class _DesktopBoardContentState extends State<DesktopBoardContent> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocListener<BoardBloc, BoardState>(
|
||||
listener: (context, state) {
|
||||
widget.onEditStateChanged?.call();
|
||||
},
|
||||
child: BlocBuilder<BoardBloc, BoardState>(
|
||||
builder: (context, state) {
|
||||
final showCreateGroupButton =
|
||||
context.read<BoardBloc>().groupingFieldType.canCreateNewGroup;
|
||||
return Padding(
|
||||
return MultiBlocListener(
|
||||
listeners: [
|
||||
BlocListener<BoardBloc, BoardState>(
|
||||
listener: (context, state) {
|
||||
state.maybeMap(
|
||||
ready: (value) {
|
||||
widget.onEditStateChanged?.call();
|
||||
},
|
||||
orElse: () {},
|
||||
);
|
||||
},
|
||||
),
|
||||
BlocListener<BoardActionsCubit, BoardActionsState>(
|
||||
listener: (context, state) {
|
||||
state.maybeMap(
|
||||
openCard: (value) {
|
||||
_openCard(
|
||||
context: context,
|
||||
databaseController:
|
||||
context.read<BoardBloc>().databaseController,
|
||||
rowMeta: value.rowMeta,
|
||||
);
|
||||
},
|
||||
setFocus: (value) {
|
||||
widget.focusScope.focusedGroupedRows = value.groupedRowIds;
|
||||
},
|
||||
startEditingRow: (value) {
|
||||
widget.boardController.enableGroupDragging(false);
|
||||
widget.focusScope.clear();
|
||||
},
|
||||
endEditingRow: (value) {
|
||||
widget.boardController.enableGroupDragging(true);
|
||||
},
|
||||
orElse: () {},
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
child: FocusScope(
|
||||
autofocus: true,
|
||||
child: BoardShortcutContainer(
|
||||
focusScope: widget.focusScope,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: AppFlowyBoard(
|
||||
boardScrollController: scrollManager,
|
||||
@ -183,7 +332,11 @@ class _DesktopBoardContentState extends State<DesktopBoardContent> {
|
||||
groupConstraints: const BoxConstraints.tightFor(width: 256),
|
||||
config: config,
|
||||
leading: HiddenGroupsColumn(margin: config.groupHeaderPadding),
|
||||
trailing: showCreateGroupButton
|
||||
trailing: context
|
||||
.read<BoardBloc>()
|
||||
.groupingFieldType
|
||||
?.canCreateNewGroup ??
|
||||
false
|
||||
? BoardTrailing(scrollController: scrollController)
|
||||
: const HSpace(40),
|
||||
headerBuilder: (_, groupData) => BlocProvider<BoardBloc>.value(
|
||||
@ -193,112 +346,369 @@ class _DesktopBoardContentState extends State<DesktopBoardContent> {
|
||||
margin: config.groupHeaderPadding,
|
||||
),
|
||||
),
|
||||
footerBuilder: _buildFooter,
|
||||
cardBuilder: (_, column, columnItem) => _buildCard(
|
||||
context,
|
||||
column,
|
||||
columnItem,
|
||||
footerBuilder: (_, groupData) => MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider.value(
|
||||
value: context.read<BoardBloc>(),
|
||||
),
|
||||
BlocProvider.value(
|
||||
value: context.read<BoardActionsCubit>(),
|
||||
),
|
||||
],
|
||||
child: BoardColumnFooter(
|
||||
columnData: groupData,
|
||||
boardConfig: config,
|
||||
scrollManager: scrollManager,
|
||||
),
|
||||
),
|
||||
cardBuilder: (_, column, columnItem) => MultiBlocProvider(
|
||||
key: ValueKey("board_card_${column.id}_${columnItem.id}"),
|
||||
providers: [
|
||||
BlocProvider<BoardBloc>.value(
|
||||
value: context.read<BoardBloc>(),
|
||||
),
|
||||
BlocProvider.value(
|
||||
value: context.read<BoardActionsCubit>(),
|
||||
),
|
||||
],
|
||||
child: _BoardCard(
|
||||
afGroupData: column,
|
||||
groupItem: columnItem as GroupItem,
|
||||
boardConfig: config,
|
||||
notifier: widget.focusScope,
|
||||
cellBuilder: cellBuilder,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@visibleForTesting
|
||||
class BoardColumnFooter extends StatefulWidget {
|
||||
const BoardColumnFooter({
|
||||
super.key,
|
||||
required this.columnData,
|
||||
required this.boardConfig,
|
||||
required this.scrollManager,
|
||||
});
|
||||
|
||||
final AppFlowyGroupData columnData;
|
||||
final AppFlowyBoardConfig boardConfig;
|
||||
final AppFlowyBoardScrollController scrollManager;
|
||||
|
||||
@override
|
||||
State<BoardColumnFooter> createState() => _BoardColumnFooterState();
|
||||
}
|
||||
|
||||
class _BoardColumnFooterState extends State<BoardColumnFooter> {
|
||||
final TextEditingController _textController = TextEditingController();
|
||||
late final FocusNode _focusNode;
|
||||
bool _isCreating = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_focusNode = FocusNode(
|
||||
onKeyEvent: (node, event) {
|
||||
if (_focusNode.hasFocus &&
|
||||
event.logicalKey == LogicalKeyboardKey.escape) {
|
||||
_focusNode.unfocus();
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
return KeyEventResult.ignored;
|
||||
},
|
||||
)..addListener(() {
|
||||
if (!_focusNode.hasFocus) {
|
||||
setState(() => _isCreating = false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_textController.dispose();
|
||||
_focusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (_isCreating) {
|
||||
_focusNode.requestFocus();
|
||||
}
|
||||
});
|
||||
return Padding(
|
||||
padding: widget.boardConfig.groupFooterPadding,
|
||||
child: AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 150),
|
||||
child:
|
||||
_isCreating ? _createCardsTextField() : _startCreatingCardsButton(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _createCardsTextField() {
|
||||
const nada = DoNothingAndStopPropagationIntent();
|
||||
return Shortcuts(
|
||||
shortcuts: {
|
||||
const SingleActivator(LogicalKeyboardKey.arrowUp): nada,
|
||||
const SingleActivator(LogicalKeyboardKey.arrowDown): nada,
|
||||
const SingleActivator(LogicalKeyboardKey.arrowUp, shift: true): nada,
|
||||
const SingleActivator(LogicalKeyboardKey.arrowDown, shift: true): nada,
|
||||
const SingleActivator(LogicalKeyboardKey.keyE): nada,
|
||||
const SingleActivator(LogicalKeyboardKey.keyN): nada,
|
||||
const SingleActivator(LogicalKeyboardKey.delete): nada,
|
||||
const SingleActivator(LogicalKeyboardKey.backspace): nada,
|
||||
const SingleActivator(LogicalKeyboardKey.enter): nada,
|
||||
const SingleActivator(LogicalKeyboardKey.numpadEnter): nada,
|
||||
const SingleActivator(LogicalKeyboardKey.comma): nada,
|
||||
const SingleActivator(LogicalKeyboardKey.period): nada,
|
||||
SingleActivator(
|
||||
LogicalKeyboardKey.arrowUp,
|
||||
shift: true,
|
||||
meta: Platform.isMacOS,
|
||||
control: !Platform.isMacOS,
|
||||
): nada,
|
||||
},
|
||||
child: FlowyTextField(
|
||||
hintTextConstraints: const BoxConstraints(maxHeight: 36),
|
||||
controller: _textController,
|
||||
focusNode: _focusNode,
|
||||
onSubmitted: (name) {
|
||||
context.read<BoardBloc>().add(
|
||||
BoardEvent.createRow(
|
||||
widget.columnData.id,
|
||||
OrderObjectPositionTypePB.End,
|
||||
name,
|
||||
null,
|
||||
),
|
||||
);
|
||||
widget.scrollManager.scrollToBottom(widget.columnData.id);
|
||||
_textController.clear();
|
||||
_focusNode.requestFocus();
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFooter(BuildContext context, AppFlowyGroupData columnData) {
|
||||
return Padding(
|
||||
padding: config.groupFooterPadding,
|
||||
Widget _startCreatingCardsButton() {
|
||||
return BlocListener<BoardActionsCubit, BoardActionsState>(
|
||||
listener: (context, state) {
|
||||
state.maybeWhen(
|
||||
startCreateBottomRow: (groupId) {
|
||||
if (groupId == widget.columnData.id) {
|
||||
setState(() => _isCreating = true);
|
||||
}
|
||||
},
|
||||
orElse: () {},
|
||||
);
|
||||
},
|
||||
child: FlowyTooltip(
|
||||
message: LocaleKeys.board_column_addToColumnBottomTooltip.tr(),
|
||||
child: FlowyHover(
|
||||
child: AppFlowyGroupFooter(
|
||||
height: 36,
|
||||
icon: FlowySvg(
|
||||
child: SizedBox(
|
||||
height: 36,
|
||||
child: FlowyButton(
|
||||
leftIcon: FlowySvg(
|
||||
FlowySvgs.add_s,
|
||||
color: Theme.of(context).hintColor,
|
||||
),
|
||||
title: FlowyText.medium(
|
||||
text: FlowyText.medium(
|
||||
LocaleKeys.board_column_createNewCard.tr(),
|
||||
color: Theme.of(context).hintColor,
|
||||
),
|
||||
onAddButtonClick: () => context
|
||||
.read<BoardBloc>()
|
||||
.add(BoardEvent.createBottomRow(columnData.id)),
|
||||
onTap: () {
|
||||
setState(() => _isCreating = true);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildCard(
|
||||
BuildContext context,
|
||||
AppFlowyGroupData afGroupData,
|
||||
AppFlowyGroupItem afGroupItem,
|
||||
) {
|
||||
class _BoardCard extends StatefulWidget {
|
||||
const _BoardCard({
|
||||
required this.afGroupData,
|
||||
required this.groupItem,
|
||||
required this.boardConfig,
|
||||
required this.cellBuilder,
|
||||
required this.notifier,
|
||||
});
|
||||
|
||||
final AppFlowyGroupData afGroupData;
|
||||
final GroupItem groupItem;
|
||||
final AppFlowyBoardConfig boardConfig;
|
||||
final CardCellBuilder cellBuilder;
|
||||
final BoardFocusScope notifier;
|
||||
|
||||
@override
|
||||
State<_BoardCard> createState() => _BoardCardState();
|
||||
}
|
||||
|
||||
class _BoardCardState extends State<_BoardCard> {
|
||||
bool _isEditing = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final boardBloc = context.read<BoardBloc>();
|
||||
final groupItem = afGroupItem as GroupItem;
|
||||
final groupData = afGroupData.customData as GroupData;
|
||||
final rowCache = boardBloc.getRowCache();
|
||||
final rowInfo = rowCache?.getRow(groupItem.row.id);
|
||||
|
||||
/// Return placeholder widget if the rowCache or rowInfo is null.
|
||||
if (rowCache == null) {
|
||||
return SizedBox.shrink(key: ObjectKey(groupItem));
|
||||
}
|
||||
final groupData = widget.afGroupData.customData as GroupData;
|
||||
final rowCache = boardBloc.rowCache;
|
||||
|
||||
final databaseController = boardBloc.databaseController;
|
||||
final viewId = boardBloc.viewId;
|
||||
final rowMeta =
|
||||
rowCache.getRow(widget.groupItem.id)?.rowMeta ?? widget.groupItem.row;
|
||||
|
||||
final isEditing = boardBloc.state.isEditingRow &&
|
||||
boardBloc.state.editingRow?.row.id == groupItem.row.id;
|
||||
const nada = DoNothingAndStopPropagationIntent();
|
||||
|
||||
final groupItemId = "${groupData.group.groupId}${groupItem.row.id}";
|
||||
final rowMeta = rowInfo?.rowMeta ?? groupItem.row;
|
||||
return BlocListener<BoardActionsCubit, BoardActionsState>(
|
||||
listener: (context, state) {
|
||||
state.maybeMap(
|
||||
startEditingRow: (value) {
|
||||
if (value.groupedRowId.rowId == widget.groupItem.id &&
|
||||
value.groupedRowId.groupId == groupData.group.groupId) {
|
||||
setState(() => _isEditing = true);
|
||||
}
|
||||
},
|
||||
endEditingRow: (_) {
|
||||
if (_isEditing) {
|
||||
setState(() => _isEditing = false);
|
||||
}
|
||||
},
|
||||
createRow: (value) {
|
||||
if ((_isEditing && value.groupedRowId == null) ||
|
||||
(value.groupedRowId?.rowId == widget.groupItem.id &&
|
||||
value.groupedRowId?.groupId == groupData.group.groupId)) {
|
||||
context.read<BoardBloc>().add(
|
||||
BoardEvent.createRow(
|
||||
groupData.group.groupId,
|
||||
value.position == CreateBoardCardRelativePosition.before
|
||||
? OrderObjectPositionTypePB.Before
|
||||
: OrderObjectPositionTypePB.After,
|
||||
null,
|
||||
widget.groupItem.row.id,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
orElse: () {},
|
||||
);
|
||||
},
|
||||
child: Shortcuts(
|
||||
shortcuts: {
|
||||
const SingleActivator(LogicalKeyboardKey.arrowUp): nada,
|
||||
const SingleActivator(LogicalKeyboardKey.arrowDown): nada,
|
||||
const SingleActivator(LogicalKeyboardKey.arrowUp, shift: true): nada,
|
||||
const SingleActivator(LogicalKeyboardKey.arrowDown, shift: true):
|
||||
nada,
|
||||
const SingleActivator(LogicalKeyboardKey.keyE): nada,
|
||||
const SingleActivator(LogicalKeyboardKey.keyN): nada,
|
||||
const SingleActivator(LogicalKeyboardKey.delete): nada,
|
||||
const SingleActivator(LogicalKeyboardKey.backspace): nada,
|
||||
const SingleActivator(LogicalKeyboardKey.enter): nada,
|
||||
const SingleActivator(LogicalKeyboardKey.numpadEnter): nada,
|
||||
const SingleActivator(LogicalKeyboardKey.comma): nada,
|
||||
const SingleActivator(LogicalKeyboardKey.period): nada,
|
||||
SingleActivator(
|
||||
LogicalKeyboardKey.arrowUp,
|
||||
shift: true,
|
||||
meta: Platform.isMacOS,
|
||||
control: !Platform.isMacOS,
|
||||
): nada,
|
||||
},
|
||||
child: ConditionalListenableBuilder<List<GroupedRowId>>(
|
||||
valueListenable: widget.notifier,
|
||||
buildWhen: (previous, current) {
|
||||
final focusItem = GroupedRowId(
|
||||
groupId: groupData.group.groupId,
|
||||
rowId: rowMeta.id,
|
||||
);
|
||||
final previousContainsFocus = previous.contains(focusItem);
|
||||
final currentContainsFocus = current.contains(focusItem);
|
||||
|
||||
return Container(
|
||||
key: ValueKey(groupItemId),
|
||||
margin: config.cardMargin,
|
||||
decoration: _makeBoxDecoration(context),
|
||||
child: RowCard(
|
||||
fieldController: databaseController.fieldController,
|
||||
rowMeta: rowMeta,
|
||||
viewId: viewId,
|
||||
rowCache: rowCache,
|
||||
groupingFieldId: groupItem.fieldInfo.id,
|
||||
isEditing: isEditing,
|
||||
cellBuilder: cellBuilder,
|
||||
openCard: (context) => _openCard(
|
||||
context: context,
|
||||
databaseController: databaseController,
|
||||
groupId: groupData.group.groupId,
|
||||
rowMeta: context.read<CardBloc>().state.rowMeta,
|
||||
),
|
||||
styleConfiguration: RowCardStyleConfiguration(
|
||||
cellStyleMap: desktopBoardCardCellStyleMap(context),
|
||||
hoverStyle: HoverStyle(
|
||||
hoverColor: Theme.of(context).brightness == Brightness.light
|
||||
? const Color(0x0F1F2329)
|
||||
: const Color(0x0FEFF4FB),
|
||||
foregroundColorOnHover: Theme.of(context).colorScheme.onBackground,
|
||||
return previousContainsFocus != currentContainsFocus;
|
||||
},
|
||||
builder: (context, focusedItems, child) => Container(
|
||||
margin: widget.boardConfig.cardMargin,
|
||||
decoration: _makeBoxDecoration(
|
||||
context,
|
||||
groupData.group.groupId,
|
||||
widget.groupItem.id,
|
||||
),
|
||||
child: child,
|
||||
),
|
||||
child: RowCard(
|
||||
fieldController: databaseController.fieldController,
|
||||
rowMeta: rowMeta,
|
||||
viewId: boardBloc.viewId,
|
||||
rowCache: rowCache,
|
||||
groupingFieldId: widget.groupItem.fieldInfo.id,
|
||||
isEditing: _isEditing,
|
||||
cellBuilder: widget.cellBuilder,
|
||||
onTap: (context) => _openCard(
|
||||
context: context,
|
||||
databaseController: databaseController,
|
||||
rowMeta: context.read<CardBloc>().state.rowMeta,
|
||||
),
|
||||
onShiftTap: (_) {
|
||||
Focus.of(context).requestFocus();
|
||||
widget.notifier.toggle(
|
||||
GroupedRowId(
|
||||
rowId: widget.groupItem.row.id,
|
||||
groupId: groupData.group.groupId,
|
||||
),
|
||||
);
|
||||
},
|
||||
styleConfiguration: RowCardStyleConfiguration(
|
||||
cellStyleMap: desktopBoardCardCellStyleMap(context),
|
||||
hoverStyle: HoverStyle(
|
||||
hoverColor: Theme.of(context).brightness == Brightness.light
|
||||
? const Color(0x0F1F2329)
|
||||
: const Color(0x0FEFF4FB),
|
||||
foregroundColorOnHover:
|
||||
Theme.of(context).colorScheme.onBackground,
|
||||
),
|
||||
),
|
||||
onStartEditing: () =>
|
||||
context.read<BoardActionsCubit>().startEditingRow(
|
||||
GroupedRowId(
|
||||
groupId: groupData.group.groupId,
|
||||
rowId: rowMeta.id,
|
||||
),
|
||||
),
|
||||
onEndEditing: () => context.read<BoardActionsCubit>().endEditing(
|
||||
GroupedRowId(
|
||||
groupId: groupData.group.groupId,
|
||||
rowId: rowMeta.id,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
onStartEditing: () =>
|
||||
boardBloc.add(BoardEvent.startEditingRow(groupData.group, rowMeta)),
|
||||
onEndEditing: () => boardBloc.add(BoardEvent.endEditingRow(rowMeta.id)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
BoxDecoration _makeBoxDecoration(BuildContext context) {
|
||||
BoxDecoration _makeBoxDecoration(
|
||||
BuildContext context,
|
||||
String groupId,
|
||||
String rowId,
|
||||
) {
|
||||
return BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
borderRadius: const BorderRadius.all(Radius.circular(6)),
|
||||
border: Border.fromBorderSide(
|
||||
BorderSide(
|
||||
color: Theme.of(context).brightness == Brightness.light
|
||||
? const Color(0xFF1F2329).withOpacity(0.12)
|
||||
: const Color(0xFF59647A),
|
||||
color: widget.notifier
|
||||
.isFocused(GroupedRowId(rowId: rowId, groupId: groupId))
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: Theme.of(context).brightness == Brightness.light
|
||||
? const Color(0xFF1F2329).withOpacity(0.12)
|
||||
: const Color(0xFF59647A),
|
||||
),
|
||||
),
|
||||
boxShadow: [
|
||||
@ -314,39 +724,6 @@ class _DesktopBoardContentState extends State<DesktopBoardContent> {
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
void _openCard({
|
||||
required BuildContext context,
|
||||
required DatabaseController databaseController,
|
||||
required String groupId,
|
||||
required RowMetaPB rowMeta,
|
||||
}) {
|
||||
final rowInfo = RowInfo(
|
||||
viewId: databaseController.viewId,
|
||||
fields:
|
||||
UnmodifiableListView(databaseController.fieldController.fieldInfos),
|
||||
rowMeta: rowMeta,
|
||||
rowId: rowMeta.id,
|
||||
);
|
||||
|
||||
final rowController = RowController(
|
||||
rowMeta: rowInfo.rowMeta,
|
||||
viewId: rowInfo.viewId,
|
||||
rowCache: databaseController.rowCache,
|
||||
groupId: groupId,
|
||||
);
|
||||
|
||||
FlowyOverlay.show(
|
||||
context: context,
|
||||
builder: (_) => BlocProvider.value(
|
||||
value: context.read<ViewBloc>(),
|
||||
child: RowDetailPage(
|
||||
databaseController: databaseController,
|
||||
rowController: rowController,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class BoardTrailing extends StatefulWidget {
|
||||
@ -458,3 +835,23 @@ class _BoardTrailingState extends State<BoardTrailing> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _openCard({
|
||||
required BuildContext context,
|
||||
required DatabaseController databaseController,
|
||||
required RowMetaPB rowMeta,
|
||||
}) {
|
||||
final rowController = RowController(
|
||||
rowMeta: rowMeta,
|
||||
viewId: databaseController.viewId,
|
||||
rowCache: databaseController.rowCache,
|
||||
);
|
||||
|
||||
FlowyOverlay.show(
|
||||
context: context,
|
||||
builder: (_) => RowDetailPage(
|
||||
databaseController: databaseController,
|
||||
rowController: rowController,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -4,8 +4,7 @@ import 'package:appflowy/plugins/database/board/application/board_bloc.dart';
|
||||
import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';
|
||||
import 'package:appflowy/plugins/database/grid/presentation/widgets/header/field_type_extension.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/dialogs.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_backend/protobuf/flowy-database2/protobuf.dart';
|
||||
import 'package:appflowy_board/appflowy_board.dart';
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
@ -65,72 +64,83 @@ class _BoardColumnHeaderState extends State<BoardColumnHeader> {
|
||||
|
||||
return BlocBuilder<BoardBloc, BoardState>(
|
||||
builder: (context, state) {
|
||||
if (state.isEditingHeader) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_focusNode.requestFocus();
|
||||
});
|
||||
}
|
||||
return state.maybeMap(
|
||||
orElse: () => const SizedBox.shrink(),
|
||||
ready: (state) {
|
||||
if (state.editingHeaderId != null) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_focusNode.requestFocus();
|
||||
});
|
||||
}
|
||||
|
||||
Widget title = Expanded(
|
||||
child: FlowyText.medium(
|
||||
widget.groupData.headerData.groupName,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
);
|
||||
Widget title = Expanded(
|
||||
child: FlowyText.medium(
|
||||
widget.groupData.headerData.groupName,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
);
|
||||
|
||||
if (!boardCustomData.group.isDefault &&
|
||||
boardCustomData.fieldType.canEditHeader) {
|
||||
title = Flexible(
|
||||
fit: FlexFit.tight,
|
||||
child: FlowyTooltip(
|
||||
message: LocaleKeys.board_column_renameGroupTooltip.tr(),
|
||||
child: MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
child: GestureDetector(
|
||||
onTap: () => context
|
||||
.read<BoardBloc>()
|
||||
.add(BoardEvent.startEditingHeader(widget.groupData.id)),
|
||||
child: FlowyText.medium(
|
||||
widget.groupData.headerData.groupName,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
if (!boardCustomData.group.isDefault &&
|
||||
boardCustomData.fieldType.canEditHeader) {
|
||||
title = Flexible(
|
||||
fit: FlexFit.tight,
|
||||
child: FlowyTooltip(
|
||||
message: LocaleKeys.board_column_renameGroupTooltip.tr(),
|
||||
child: MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
child: GestureDetector(
|
||||
onTap: () => context.read<BoardBloc>().add(
|
||||
BoardEvent.startEditingHeader(widget.groupData.id),
|
||||
),
|
||||
child: FlowyText.medium(
|
||||
widget.groupData.headerData.groupName,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (state.editingHeaderId == widget.groupData.id) {
|
||||
title = _buildTextField(context);
|
||||
}
|
||||
|
||||
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(),
|
||||
preferBelow: false,
|
||||
child: FlowyIconButton(
|
||||
width: 20,
|
||||
icon: const FlowySvg(FlowySvgs.add_s),
|
||||
iconColorOnHover:
|
||||
Theme.of(context).colorScheme.onSurface,
|
||||
onPressed: () => context.read<BoardBloc>().add(
|
||||
BoardEvent.createRow(
|
||||
widget.groupData.id,
|
||||
OrderObjectPositionTypePB.Start,
|
||||
null,
|
||||
null,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (state.isEditingHeader &&
|
||||
state.editingHeaderId == widget.groupData.id) {
|
||||
title = _buildTextField(context);
|
||||
}
|
||||
|
||||
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(),
|
||||
preferBelow: false,
|
||||
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)),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
@ -257,7 +267,7 @@ enum GroupOptions {
|
||||
case hide:
|
||||
context
|
||||
.read<BoardBloc>()
|
||||
.add(BoardEvent.toggleGroupVisibility(group, false));
|
||||
.add(BoardEvent.setGroupVisibility(group, false));
|
||||
break;
|
||||
case delete:
|
||||
NavigatorAlertDialog(
|
||||
|
@ -0,0 +1,365 @@
|
||||
import 'package:appflowy/plugins/database/board/application/board_bloc.dart';
|
||||
import 'package:appflowy_board/appflowy_board.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
class BoardFocusScope extends ChangeNotifier
|
||||
implements ValueListenable<List<GroupedRowId>> {
|
||||
BoardFocusScope({
|
||||
required this.boardController,
|
||||
});
|
||||
|
||||
final AppFlowyBoardController boardController;
|
||||
List<GroupedRowId> _focusedCards = [];
|
||||
|
||||
@override
|
||||
List<GroupedRowId> get value => _focusedCards;
|
||||
|
||||
UnmodifiableListView<GroupedRowId> get focusedGroupedRows =>
|
||||
UnmodifiableListView(_focusedCards);
|
||||
|
||||
set focusedGroupedRows(List<GroupedRowId> focusedGroupedRows) {
|
||||
_deepCopy();
|
||||
_focusedCards
|
||||
..clear()
|
||||
..addAll(focusedGroupedRows);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
bool isFocused(GroupedRowId groupedRowId) =>
|
||||
_focusedCards.contains(groupedRowId);
|
||||
|
||||
void toggle(GroupedRowId groupedRowId) {
|
||||
_deepCopy();
|
||||
if (_focusedCards.contains(groupedRowId)) {
|
||||
_focusedCards.remove(groupedRowId);
|
||||
} else {
|
||||
_focusedCards.add(groupedRowId);
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void focusNext() {
|
||||
_deepCopy();
|
||||
|
||||
// if no card is focused, focus on the first card in the board
|
||||
if (_focusedCards.isEmpty) {
|
||||
_focusFirstCard();
|
||||
notifyListeners();
|
||||
return;
|
||||
}
|
||||
|
||||
final lastFocusedCard = _focusedCards.last;
|
||||
final groupController = boardController.controller(lastFocusedCard.groupId);
|
||||
final iterable = groupController?.items
|
||||
.skipWhile((item) => item.id != lastFocusedCard.rowId);
|
||||
|
||||
// if the last-focused card's group cannot be found, or if the last-focused card cannot be found in the group, focus on the first card in the board
|
||||
if (iterable == null || iterable.isEmpty) {
|
||||
_focusFirstCard();
|
||||
notifyListeners();
|
||||
return;
|
||||
}
|
||||
|
||||
if (iterable.length == 1) {
|
||||
// focus on the first card in the next group
|
||||
final group = boardController.groupDatas
|
||||
.skipWhile((item) => item.id != lastFocusedCard.groupId)
|
||||
.skip(1)
|
||||
.firstWhereOrNull((groupData) => groupData.items.isNotEmpty);
|
||||
if (group != null) {
|
||||
_focusedCards
|
||||
..clear()
|
||||
..add(
|
||||
GroupedRowId(
|
||||
rowId: group.items.first.id,
|
||||
groupId: group.id,
|
||||
),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// focus on the next card in the same group
|
||||
_focusedCards
|
||||
..clear()
|
||||
..add(
|
||||
GroupedRowId(
|
||||
rowId: iterable.elementAt(1).id,
|
||||
groupId: lastFocusedCard.groupId,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void focusPrevious() {
|
||||
_deepCopy();
|
||||
|
||||
// if no card is focused, focus on the last card in the board
|
||||
if (_focusedCards.isEmpty) {
|
||||
_focusLastCard();
|
||||
notifyListeners();
|
||||
return;
|
||||
}
|
||||
|
||||
final lastFocusedCard = _focusedCards.last;
|
||||
final groupController = boardController.controller(lastFocusedCard.groupId);
|
||||
final iterable = groupController?.items.reversed
|
||||
.skipWhile((item) => item.id != lastFocusedCard.rowId);
|
||||
|
||||
// if the last-focused card's group cannot be found or if the last-focused card cannot be found in the group, focus on the last card in the board
|
||||
if (iterable == null || iterable.isEmpty) {
|
||||
_focusLastCard();
|
||||
notifyListeners();
|
||||
return;
|
||||
}
|
||||
|
||||
if (iterable.length == 1) {
|
||||
// focus on the last card in the previous group
|
||||
final group = boardController.groupDatas.reversed
|
||||
.skipWhile((item) => item.id != lastFocusedCard.groupId)
|
||||
.skip(1)
|
||||
.firstWhereOrNull((groupData) => groupData.items.isNotEmpty);
|
||||
if (group != null) {
|
||||
_focusedCards
|
||||
..clear()
|
||||
..add(
|
||||
GroupedRowId(
|
||||
rowId: group.items.last.id,
|
||||
groupId: group.id,
|
||||
),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// focus on the next card in the same group
|
||||
_focusedCards
|
||||
..clear()
|
||||
..add(
|
||||
GroupedRowId(
|
||||
rowId: iterable.elementAt(1).id,
|
||||
groupId: lastFocusedCard.groupId,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void adjustRangeDown() {
|
||||
_deepCopy();
|
||||
|
||||
// if no card is focused, focus on the first card in the board
|
||||
if (_focusedCards.isEmpty) {
|
||||
_focusFirstCard();
|
||||
notifyListeners();
|
||||
return;
|
||||
}
|
||||
|
||||
final firstFocusedCard = _focusedCards.first;
|
||||
final lastFocusedCard = _focusedCards.last;
|
||||
|
||||
// determine whether to shrink or expand the selection
|
||||
bool isExpand = false;
|
||||
if (_focusedCards.length == 1) {
|
||||
isExpand = true;
|
||||
} else {
|
||||
final firstGroupIndex = boardController.groupDatas
|
||||
.indexWhere((element) => element.id == firstFocusedCard.groupId);
|
||||
final lastGroupIndex = boardController.groupDatas
|
||||
.indexWhere((element) => element.id == lastFocusedCard.groupId);
|
||||
|
||||
if (firstGroupIndex == -1 || lastGroupIndex == -1) {
|
||||
_focusFirstCard();
|
||||
notifyListeners();
|
||||
return;
|
||||
}
|
||||
|
||||
if (firstGroupIndex < lastGroupIndex) {
|
||||
isExpand = true;
|
||||
} else if (firstGroupIndex > lastGroupIndex) {
|
||||
isExpand = false;
|
||||
} else {
|
||||
final groupItems =
|
||||
boardController.groupDatas.elementAt(firstGroupIndex).items;
|
||||
final firstCardIndex =
|
||||
groupItems.indexWhere((item) => item.id == firstFocusedCard.rowId);
|
||||
final lastCardIndex =
|
||||
groupItems.indexWhere((item) => item.id == lastFocusedCard.rowId);
|
||||
|
||||
if (firstCardIndex == -1 || lastCardIndex == -1) {
|
||||
_focusFirstCard();
|
||||
notifyListeners();
|
||||
return;
|
||||
}
|
||||
|
||||
isExpand = firstCardIndex < lastCardIndex;
|
||||
}
|
||||
}
|
||||
|
||||
if (isExpand) {
|
||||
final groupController =
|
||||
boardController.controller(lastFocusedCard.groupId);
|
||||
|
||||
if (groupController == null) {
|
||||
_focusFirstCard();
|
||||
notifyListeners();
|
||||
return;
|
||||
}
|
||||
|
||||
final iterable = groupController.items
|
||||
.skipWhile((item) => item.id != lastFocusedCard.rowId);
|
||||
|
||||
if (iterable.length == 1) {
|
||||
// focus on the first card in the next group
|
||||
final group = boardController.groupDatas
|
||||
.skipWhile((item) => item.id != lastFocusedCard.groupId)
|
||||
.skip(1)
|
||||
.firstWhereOrNull((groupData) => groupData.items.isNotEmpty);
|
||||
if (group != null) {
|
||||
_focusedCards.add(
|
||||
GroupedRowId(
|
||||
rowId: group.items.first.id,
|
||||
groupId: group.id,
|
||||
),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
_focusedCards.add(
|
||||
GroupedRowId(
|
||||
rowId: iterable.elementAt(1).id,
|
||||
groupId: lastFocusedCard.groupId,
|
||||
),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
_focusedCards.removeLast();
|
||||
}
|
||||
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void adjustRangeUp() {
|
||||
_deepCopy();
|
||||
|
||||
// if no card is focused, focus on the first card in the board
|
||||
if (_focusedCards.isEmpty) {
|
||||
_focusLastCard();
|
||||
notifyListeners();
|
||||
return;
|
||||
}
|
||||
|
||||
final firstFocusedCard = _focusedCards.first;
|
||||
final lastFocusedCard = _focusedCards.last;
|
||||
|
||||
// determine whether to shrink or expand the selection
|
||||
bool isExpand = false;
|
||||
if (_focusedCards.length == 1) {
|
||||
isExpand = true;
|
||||
} else {
|
||||
final firstGroupIndex = boardController.groupDatas
|
||||
.indexWhere((element) => element.id == firstFocusedCard.groupId);
|
||||
final lastGroupIndex = boardController.groupDatas
|
||||
.indexWhere((element) => element.id == lastFocusedCard.groupId);
|
||||
|
||||
if (firstGroupIndex == -1 || lastGroupIndex == -1) {
|
||||
_focusLastCard();
|
||||
notifyListeners();
|
||||
return;
|
||||
}
|
||||
|
||||
if (firstGroupIndex < lastGroupIndex) {
|
||||
isExpand = false;
|
||||
} else if (firstGroupIndex > lastGroupIndex) {
|
||||
isExpand = true;
|
||||
} else {
|
||||
final groupItems =
|
||||
boardController.groupDatas.elementAt(firstGroupIndex).items;
|
||||
final firstCardIndex =
|
||||
groupItems.indexWhere((item) => item.id == firstFocusedCard.rowId);
|
||||
final lastCardIndex =
|
||||
groupItems.indexWhere((item) => item.id == lastFocusedCard.rowId);
|
||||
|
||||
if (firstCardIndex == -1 || lastCardIndex == -1) {
|
||||
_focusLastCard();
|
||||
notifyListeners();
|
||||
return;
|
||||
}
|
||||
|
||||
isExpand = firstCardIndex > lastCardIndex;
|
||||
}
|
||||
}
|
||||
|
||||
if (isExpand) {
|
||||
final groupController =
|
||||
boardController.controller(lastFocusedCard.groupId);
|
||||
|
||||
if (groupController == null) {
|
||||
_focusLastCard();
|
||||
notifyListeners();
|
||||
return;
|
||||
}
|
||||
|
||||
final iterable = groupController.items.reversed
|
||||
.skipWhile((item) => item.id != lastFocusedCard.rowId);
|
||||
|
||||
if (iterable.length == 1) {
|
||||
// focus on the last card in the previous group
|
||||
final group = boardController.groupDatas.reversed
|
||||
.skipWhile((item) => item.id != lastFocusedCard.groupId)
|
||||
.skip(1)
|
||||
.firstWhereOrNull((groupData) => groupData.items.isNotEmpty);
|
||||
if (group != null) {
|
||||
_focusedCards.add(
|
||||
GroupedRowId(
|
||||
rowId: group.items.last.id,
|
||||
groupId: group.id,
|
||||
),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
_focusedCards.add(
|
||||
GroupedRowId(
|
||||
rowId: iterable.elementAt(1).id,
|
||||
groupId: lastFocusedCard.groupId,
|
||||
),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
_focusedCards.removeLast();
|
||||
}
|
||||
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void clear() {
|
||||
_deepCopy();
|
||||
_focusedCards.clear();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void _focusFirstCard() {
|
||||
_focusedCards.clear();
|
||||
final firstGroup = boardController.groupDatas
|
||||
.firstWhereOrNull((group) => group.items.isNotEmpty);
|
||||
final firstCard = firstGroup?.items.firstOrNull;
|
||||
if (firstCard != null) {
|
||||
_focusedCards
|
||||
.add(GroupedRowId(rowId: firstCard.id, groupId: firstGroup!.id));
|
||||
}
|
||||
}
|
||||
|
||||
void _focusLastCard() {
|
||||
_focusedCards.clear();
|
||||
final lastGroup = boardController.groupDatas
|
||||
.lastWhereOrNull((group) => group.items.isNotEmpty);
|
||||
final lastCard = lastGroup?.items.lastOrNull;
|
||||
if (lastCard != null) {
|
||||
_focusedCards
|
||||
.add(GroupedRowId(rowId: lastCard.id, groupId: lastGroup!.id));
|
||||
}
|
||||
}
|
||||
|
||||
void _deepCopy() {
|
||||
_focusedCards = [..._focusedCards];
|
||||
}
|
||||
}
|
@ -11,7 +11,6 @@ import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/cell/card_cell_builder.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/cell/card_cell_skeleton/text_card_cell.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/row/row_detail.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_bloc.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
@ -23,7 +22,10 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class HiddenGroupsColumn extends StatelessWidget {
|
||||
const HiddenGroupsColumn({super.key, required this.margin});
|
||||
const HiddenGroupsColumn({
|
||||
super.key,
|
||||
required this.margin,
|
||||
});
|
||||
|
||||
final EdgeInsets margin;
|
||||
|
||||
@ -31,7 +33,10 @@ class HiddenGroupsColumn extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
final databaseController = context.read<BoardBloc>().databaseController;
|
||||
return BlocSelector<BoardBloc, BoardState, BoardLayoutSettingPB?>(
|
||||
selector: (state) => state.layoutSettings,
|
||||
selector: (state) => state.maybeMap(
|
||||
orElse: () => null,
|
||||
ready: (value) => value.layoutSettings,
|
||||
),
|
||||
builder: (context, layoutSettings) {
|
||||
if (layoutSettings == null) {
|
||||
return const SizedBox.shrink();
|
||||
@ -126,43 +131,48 @@ class HiddenGroupList extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
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(),
|
||||
builder: (context, state) {
|
||||
return state.maybeMap(
|
||||
orElse: () => const SizedBox.shrink(),
|
||||
ready: (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: context.read<BoardBloc>(),
|
||||
),
|
||||
),
|
||||
onReorder: (oldIndex, newIndex) {
|
||||
if (oldIndex < newIndex) {
|
||||
newIndex--;
|
||||
}
|
||||
final fromGroupId = state.hiddenGroups[oldIndex].groupId;
|
||||
final toGroupId = state.hiddenGroups[newIndex].groupId;
|
||||
context
|
||||
.read<BoardBloc>()
|
||||
.add(BoardEvent.reorderGroup(fromGroupId, toGroupId));
|
||||
},
|
||||
),
|
||||
),
|
||||
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: context.read<BoardBloc>(),
|
||||
),
|
||||
),
|
||||
onReorder: (oldIndex, newIndex) {
|
||||
if (oldIndex < newIndex) {
|
||||
newIndex--;
|
||||
}
|
||||
final fromGroupId = state.hiddenGroups[oldIndex].groupId;
|
||||
final toGroupId = state.hiddenGroups[newIndex].groupId;
|
||||
context
|
||||
.read<BoardBloc>()
|
||||
.add(BoardEvent.reorderGroup(fromGroupId, toGroupId));
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -248,57 +258,63 @@ class HiddenGroupButtonContent extends StatelessWidget {
|
||||
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 state.maybeMap(
|
||||
orElse: () => const SizedBox.shrink(),
|
||||
ready: (state) {
|
||||
final group = state.hiddenGroups.firstWhereOrNull(
|
||||
(g) => g.groupId == groupId,
|
||||
);
|
||||
if (group == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return SizedBox(
|
||||
height: 32,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 4,
|
||||
vertical: 3,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
HiddenGroupCardActions(
|
||||
isVisible: isHovering,
|
||||
index: index,
|
||||
return SizedBox(
|
||||
height: 32,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 4,
|
||||
vertical: 3,
|
||||
),
|
||||
const HSpace(4),
|
||||
FlowyText.medium(
|
||||
bloc.generateGroupNameFromGroup(group),
|
||||
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,
|
||||
child: Row(
|
||||
children: [
|
||||
HiddenGroupCardActions(
|
||||
isVisible: isHovering,
|
||||
index: index,
|
||||
),
|
||||
onPressed: () => context.read<BoardBloc>().add(
|
||||
BoardEvent.toggleGroupVisibility(
|
||||
group,
|
||||
true,
|
||||
),
|
||||
const HSpace(4),
|
||||
FlowyText.medium(
|
||||
bloc.generateGroupNameFromGroup(group),
|
||||
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.setGroupVisibility(
|
||||
group,
|
||||
true,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
@ -360,68 +376,71 @@ class HiddenGroupPopupItemList extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return 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(
|
||||
context.read<BoardBloc>().generateGroupNameFromGroup(group),
|
||||
fontSize: 10,
|
||||
color: Theme.of(context).hintColor,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
...group.rows.map(
|
||||
(item) {
|
||||
final rowController = RowController(
|
||||
rowMeta: item,
|
||||
viewId: viewId,
|
||||
rowCache: rowCache,
|
||||
);
|
||||
|
||||
final databaseController =
|
||||
context.read<BoardBloc>().databaseController;
|
||||
|
||||
return HiddenGroupPopupItem(
|
||||
cellContext: rowCache.loadCells(item).firstWhere(
|
||||
(cellContext) => cellContext.fieldId == primaryFieldId,
|
||||
),
|
||||
rowController: rowController,
|
||||
rowMeta: item,
|
||||
cellBuilder: CardCellBuilder(
|
||||
databaseController: databaseController,
|
||||
return state.maybeMap(
|
||||
orElse: () => const SizedBox.shrink(),
|
||||
ready: (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(
|
||||
context.read<BoardBloc>().generateGroupNameFromGroup(group),
|
||||
fontSize: 10,
|
||||
color: Theme.of(context).hintColor,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
onPressed: () {
|
||||
FlowyOverlay.show(
|
||||
context: context,
|
||||
builder: (_) {
|
||||
return BlocProvider.value(
|
||||
value: context.read<ViewBloc>(),
|
||||
child: RowDetailPage(
|
||||
databaseController: databaseController,
|
||||
rowController: rowController,
|
||||
),
|
||||
...group.rows.map(
|
||||
(item) {
|
||||
final rowController = RowController(
|
||||
rowMeta: item,
|
||||
viewId: viewId,
|
||||
rowCache: rowCache,
|
||||
);
|
||||
|
||||
final databaseController =
|
||||
context.read<BoardBloc>().databaseController;
|
||||
|
||||
return HiddenGroupPopupItem(
|
||||
cellContext: rowCache.loadCells(item).firstWhere(
|
||||
(cellContext) =>
|
||||
cellContext.fieldId == primaryFieldId,
|
||||
),
|
||||
rowController: rowController,
|
||||
rowMeta: item,
|
||||
cellBuilder: CardCellBuilder(
|
||||
databaseController: databaseController,
|
||||
),
|
||||
onPressed: () {
|
||||
FlowyOverlay.show(
|
||||
context: context,
|
||||
builder: (_) {
|
||||
return RowDetailPage(
|
||||
databaseController: databaseController,
|
||||
rowController: rowController,
|
||||
);
|
||||
},
|
||||
);
|
||||
PopoverContainer.of(context).close();
|
||||
},
|
||||
);
|
||||
PopoverContainer.of(context).close();
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
];
|
||||
),
|
||||
];
|
||||
|
||||
return ListView.separated(
|
||||
itemBuilder: (context, index) => cells[index],
|
||||
itemCount: cells.length,
|
||||
separatorBuilder: (context, index) =>
|
||||
VSpace(GridSize.typeOptionSeparatorHeight),
|
||||
shrinkWrap: true,
|
||||
return ListView.separated(
|
||||
itemBuilder: (context, index) => cells[index],
|
||||
itemCount: cells.length,
|
||||
separatorBuilder: (context, index) =>
|
||||
VSpace(GridSize.typeOptionSeparatorHeight),
|
||||
shrinkWrap: true,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
|
@ -0,0 +1,146 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:appflowy/plugins/database/board/application/board_actions_bloc.dart';
|
||||
import 'package:appflowy/plugins/database/board/application/board_bloc.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import 'board_focus_scope.dart';
|
||||
|
||||
class BoardShortcutContainer extends StatelessWidget {
|
||||
const BoardShortcutContainer({
|
||||
super.key,
|
||||
required this.focusScope,
|
||||
required this.child,
|
||||
});
|
||||
|
||||
final BoardFocusScope focusScope;
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CallbackShortcuts(
|
||||
bindings: {
|
||||
const SingleActivator(LogicalKeyboardKey.arrowUp):
|
||||
focusScope.focusPrevious,
|
||||
const SingleActivator(LogicalKeyboardKey.arrowDown):
|
||||
focusScope.focusNext,
|
||||
const SingleActivator(LogicalKeyboardKey.arrowUp, shift: true):
|
||||
focusScope.adjustRangeUp,
|
||||
const SingleActivator(LogicalKeyboardKey.arrowDown, shift: true):
|
||||
focusScope.adjustRangeDown,
|
||||
const SingleActivator(LogicalKeyboardKey.escape): focusScope.clear,
|
||||
const SingleActivator(LogicalKeyboardKey.keyE): () {
|
||||
if (focusScope.value.length != 1) {
|
||||
return;
|
||||
}
|
||||
context
|
||||
.read<BoardActionsCubit>()
|
||||
.startEditingRow(focusScope.value.first);
|
||||
},
|
||||
const SingleActivator(LogicalKeyboardKey.keyN): () {
|
||||
if (focusScope.value.length != 1) {
|
||||
return;
|
||||
}
|
||||
context
|
||||
.read<BoardActionsCubit>()
|
||||
.startCreateBottomRow(focusScope.value.first.groupId);
|
||||
focusScope.clear();
|
||||
},
|
||||
const SingleActivator(LogicalKeyboardKey.delete): () =>
|
||||
_removeHandler(context),
|
||||
const SingleActivator(LogicalKeyboardKey.backspace): () =>
|
||||
_removeHandler(context),
|
||||
SingleActivator(
|
||||
LogicalKeyboardKey.arrowUp,
|
||||
shift: true,
|
||||
meta: Platform.isMacOS,
|
||||
control: !Platform.isMacOS,
|
||||
): () => _shiftCmdUpHandler(context),
|
||||
const SingleActivator(LogicalKeyboardKey.enter): () =>
|
||||
_enterHandler(context),
|
||||
const SingleActivator(LogicalKeyboardKey.numpadEnter): () =>
|
||||
_enterHandler(context),
|
||||
const SingleActivator(LogicalKeyboardKey.enter, shift: true): () =>
|
||||
_shitEnterHandler(context),
|
||||
const SingleActivator(LogicalKeyboardKey.comma): () =>
|
||||
_moveGroupToAdjacentGroup(context, true),
|
||||
const SingleActivator(LogicalKeyboardKey.period): () =>
|
||||
_moveGroupToAdjacentGroup(context, false),
|
||||
},
|
||||
child: FocusScope(
|
||||
child: Focus(
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () {
|
||||
final focusNode = Focus.of(context);
|
||||
focusNode.requestFocus();
|
||||
focusScope.clear();
|
||||
},
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _enterHandler(BuildContext context) {
|
||||
if (focusScope.value.length != 1) {
|
||||
return;
|
||||
}
|
||||
context
|
||||
.read<BoardActionsCubit>()
|
||||
.openCardWithRowId(focusScope.value.first.rowId);
|
||||
}
|
||||
|
||||
void _shitEnterHandler(BuildContext context) {
|
||||
if (focusScope.value.isEmpty) {
|
||||
context
|
||||
.read<BoardActionsCubit>()
|
||||
.createRow(null, CreateBoardCardRelativePosition.after);
|
||||
} else if (focusScope.value.length == 1) {
|
||||
context.read<BoardActionsCubit>().createRow(
|
||||
focusScope.value.first,
|
||||
CreateBoardCardRelativePosition.after,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _shiftCmdUpHandler(BuildContext context) {
|
||||
if (focusScope.value.isEmpty) {
|
||||
context
|
||||
.read<BoardActionsCubit>()
|
||||
.createRow(null, CreateBoardCardRelativePosition.before);
|
||||
} else if (focusScope.value.length == 1) {
|
||||
context.read<BoardActionsCubit>().createRow(
|
||||
focusScope.value.first,
|
||||
CreateBoardCardRelativePosition.before,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _removeHandler(BuildContext context) {
|
||||
if (focusScope.value.isEmpty) {
|
||||
return;
|
||||
}
|
||||
context.read<BoardBloc>().add(BoardEvent.deleteCards(focusScope.value));
|
||||
}
|
||||
|
||||
void _moveGroupToAdjacentGroup(BuildContext context, bool toPrevious) {
|
||||
if (focusScope.value.length != 1) {
|
||||
return;
|
||||
}
|
||||
context.read<BoardBloc>().add(
|
||||
BoardEvent.moveGroupToAdjacentGroup(
|
||||
focusScope.value.first,
|
||||
toPrevious,
|
||||
),
|
||||
);
|
||||
focusScope.clear();
|
||||
}
|
||||
}
|
@ -72,7 +72,7 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
|
||||
);
|
||||
},
|
||||
deleteEvent: (String viewId, String rowId) async {
|
||||
final result = await RowBackendService.deleteRow(viewId, rowId);
|
||||
final result = await RowBackendService.deleteRows(viewId, [rowId]);
|
||||
result.fold(
|
||||
(_) => null,
|
||||
(e) => Log.error('Failed to delete event: $e', e),
|
||||
|
@ -46,9 +46,9 @@ class CalendarEventEditorBloc
|
||||
emit(state.copyWith(cells: cells));
|
||||
},
|
||||
delete: () async {
|
||||
final result = await RowBackendService.deleteRow(
|
||||
final result = await RowBackendService.deleteRows(
|
||||
rowController.viewId,
|
||||
rowController.rowId,
|
||||
[rowController.rowId],
|
||||
);
|
||||
result.fold((l) => null, (err) => Log.error(err));
|
||||
},
|
||||
|
@ -80,7 +80,7 @@ class _EventCardState extends State<EventCard> {
|
||||
rowCache: rowCache,
|
||||
isEditing: false,
|
||||
cellBuilder: cellBuilder,
|
||||
openCard: (context) {
|
||||
onTap: (context) {
|
||||
if (PlatformExtension.isMobile) {
|
||||
context.push(
|
||||
MobileRowDetailPage.routeName,
|
||||
|
@ -51,7 +51,7 @@ class GridBloc extends Bloc<GridEvent, GridState> {
|
||||
emit(state.copyWith(createdRow: null, openRowDetail: false));
|
||||
},
|
||||
deleteRow: (rowInfo) async {
|
||||
await RowBackendService.deleteRow(rowInfo.viewId, rowInfo.rowId);
|
||||
await RowBackendService.deleteRows(viewId, [rowInfo.rowId]);
|
||||
},
|
||||
moveRow: (int from, int to) {
|
||||
final List<RowInfo> rows = [...state.rowInfos];
|
||||
|
@ -102,7 +102,7 @@ enum RowAction {
|
||||
RowBackendService.duplicateRow(viewId, rowId);
|
||||
break;
|
||||
case delete:
|
||||
RowBackendService.deleteRow(viewId, rowId);
|
||||
RowBackendService.deleteRows(viewId, [rowId]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -29,10 +29,11 @@ class RowCard extends StatefulWidget {
|
||||
required this.isEditing,
|
||||
required this.rowCache,
|
||||
required this.cellBuilder,
|
||||
required this.openCard,
|
||||
required this.onTap,
|
||||
required this.onStartEditing,
|
||||
required this.onEndEditing,
|
||||
required this.styleConfiguration,
|
||||
this.onShiftTap,
|
||||
this.groupingFieldId,
|
||||
this.groupId,
|
||||
});
|
||||
@ -50,7 +51,9 @@ class RowCard extends StatefulWidget {
|
||||
final CardCellBuilder cellBuilder;
|
||||
|
||||
/// Called when the user taps on the card.
|
||||
final void Function(BuildContext) openCard;
|
||||
final void Function(BuildContext context) onTap;
|
||||
|
||||
final void Function(BuildContext context)? onShiftTap;
|
||||
|
||||
/// Called when the user starts editing the card.
|
||||
final VoidCallback onStartEditing;
|
||||
@ -67,12 +70,10 @@ class RowCard extends StatefulWidget {
|
||||
class _RowCardState extends State<RowCard> {
|
||||
final popoverController = PopoverController();
|
||||
late final CardBloc _cardBloc;
|
||||
late final EditableRowNotifier rowNotifier;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
rowNotifier = EditableRowNotifier(isEditing: widget.isEditing);
|
||||
_cardBloc = CardBloc(
|
||||
fieldController: widget.fieldController,
|
||||
viewId: widget.viewId,
|
||||
@ -81,22 +82,18 @@ class _RowCardState extends State<RowCard> {
|
||||
rowMeta: widget.rowMeta,
|
||||
rowCache: widget.rowCache,
|
||||
)..add(const CardEvent.initial());
|
||||
}
|
||||
|
||||
rowNotifier.isEditing.addListener(() {
|
||||
if (!mounted) return;
|
||||
_cardBloc.add(CardEvent.setIsEditing(rowNotifier.isEditing.value));
|
||||
|
||||
if (rowNotifier.isEditing.value) {
|
||||
widget.onStartEditing();
|
||||
} else {
|
||||
widget.onEndEditing();
|
||||
}
|
||||
});
|
||||
@override
|
||||
void didUpdateWidget(covariant oldWidget) {
|
||||
if (widget.isEditing != _cardBloc.state.isEditing) {
|
||||
_cardBloc.add(CardEvent.setIsEditing(widget.isEditing));
|
||||
}
|
||||
super.didUpdateWidget(oldWidget);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
rowNotifier.dispose();
|
||||
_cardBloc.close();
|
||||
super.dispose();
|
||||
}
|
||||
@ -105,7 +102,14 @@ class _RowCardState extends State<RowCard> {
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider.value(
|
||||
value: _cardBloc,
|
||||
child: BlocBuilder<CardBloc, CardState>(
|
||||
child: BlocConsumer<CardBloc, CardState>(
|
||||
listenWhen: (previous, current) =>
|
||||
previous.isEditing != current.isEditing,
|
||||
listener: (context, state) {
|
||||
if (!state.isEditing) {
|
||||
widget.onEndEditing();
|
||||
}
|
||||
},
|
||||
builder: (context, state) =>
|
||||
PlatformExtension.isMobile ? _mobile(state) : _desktop(state),
|
||||
),
|
||||
@ -114,7 +118,7 @@ class _RowCardState extends State<RowCard> {
|
||||
|
||||
Widget _mobile(CardState state) {
|
||||
return GestureDetector(
|
||||
onTap: () => widget.openCard(context),
|
||||
onTap: () => widget.onTap(context),
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: MobileCardContent(
|
||||
rowMeta: state.rowMeta,
|
||||
@ -127,9 +131,9 @@ class _RowCardState extends State<RowCard> {
|
||||
|
||||
Widget _desktop(CardState state) {
|
||||
final accessories = widget.styleConfiguration.showAccessory
|
||||
? <CardAccessory>[
|
||||
EditCardAccessory(rowNotifier: rowNotifier),
|
||||
const MoreCardOptionsAccessory(),
|
||||
? const <CardAccessory>[
|
||||
EditCardAccessory(),
|
||||
MoreCardOptionsAccessory(),
|
||||
]
|
||||
: null;
|
||||
return AppFlowyPopover(
|
||||
@ -148,10 +152,10 @@ class _RowCardState extends State<RowCard> {
|
||||
buildAccessoryWhen: () => state.isEditing == false,
|
||||
accessories: accessories ?? [],
|
||||
openAccessory: _handleOpenAccessory,
|
||||
openCard: widget.openCard,
|
||||
onTap: widget.onTap,
|
||||
onShiftTap: widget.onShiftTap,
|
||||
child: _CardContent(
|
||||
rowMeta: state.rowMeta,
|
||||
rowNotifier: rowNotifier,
|
||||
cellBuilder: widget.cellBuilder,
|
||||
styleConfiguration: widget.styleConfiguration,
|
||||
cells: state.cells,
|
||||
@ -163,6 +167,7 @@ class _RowCardState extends State<RowCard> {
|
||||
void _handleOpenAccessory(AccessoryType newAccessoryType) {
|
||||
switch (newAccessoryType) {
|
||||
case AccessoryType.edit:
|
||||
widget.onStartEditing();
|
||||
break;
|
||||
case AccessoryType.more:
|
||||
popoverController.show();
|
||||
@ -174,14 +179,12 @@ class _RowCardState extends State<RowCard> {
|
||||
class _CardContent extends StatelessWidget {
|
||||
const _CardContent({
|
||||
required this.rowMeta,
|
||||
required this.rowNotifier,
|
||||
required this.cellBuilder,
|
||||
required this.cells,
|
||||
required this.styleConfiguration,
|
||||
});
|
||||
|
||||
final RowMetaPB rowMeta;
|
||||
final EditableRowNotifier rowNotifier;
|
||||
final CardCellBuilder cellBuilder;
|
||||
final List<CellContext> cells;
|
||||
final RowCardStyleConfiguration styleConfiguration;
|
||||
@ -199,7 +202,7 @@ class _CardContent extends StatelessWidget {
|
||||
? child
|
||||
: FlowyHover(
|
||||
style: styleConfiguration.hoverStyle,
|
||||
buildWhenOnHover: () => !rowNotifier.isEditing.value,
|
||||
buildWhenOnHover: () => !context.read<CardBloc>().state.isEditing,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
@ -209,16 +212,16 @@ class _CardContent extends StatelessWidget {
|
||||
RowMetaPB rowMeta,
|
||||
List<CellContext> cells,
|
||||
) {
|
||||
// Remove all the cell listeners.
|
||||
rowNotifier.unbind();
|
||||
|
||||
return cells.mapIndexed((int index, CellContext cellContext) {
|
||||
EditableCardNotifier? cellNotifier;
|
||||
|
||||
if (index == 0) {
|
||||
cellNotifier =
|
||||
EditableCardNotifier(isEditing: rowNotifier.isEditing.value);
|
||||
rowNotifier.bindCell(cellContext, cellNotifier);
|
||||
final bloc = context.read<CardBloc>();
|
||||
cellNotifier = EditableCardNotifier(isEditing: bloc.state.isEditing);
|
||||
cellNotifier.isCellEditing.addListener(() {
|
||||
final isEditing = cellNotifier!.isCellEditing.value;
|
||||
bloc.add(CardEvent.setIsEditing(isEditing));
|
||||
});
|
||||
}
|
||||
|
||||
return cellBuilder.build(
|
||||
@ -231,6 +234,24 @@ class _CardContent extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class EditCardAccessory extends StatelessWidget with CardAccessory {
|
||||
const EditCardAccessory({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(3.0),
|
||||
child: FlowySvg(
|
||||
FlowySvgs.edit_s,
|
||||
color: Theme.of(context).hintColor,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
AccessoryType get type => AccessoryType.edit;
|
||||
}
|
||||
|
||||
class MoreCardOptionsAccessory extends StatelessWidget with CardAccessory {
|
||||
const MoreCardOptionsAccessory({super.key});
|
||||
|
||||
@ -249,29 +270,6 @@ class MoreCardOptionsAccessory extends StatelessWidget with CardAccessory {
|
||||
AccessoryType get type => AccessoryType.more;
|
||||
}
|
||||
|
||||
class EditCardAccessory extends StatelessWidget with CardAccessory {
|
||||
const EditCardAccessory({super.key, required this.rowNotifier});
|
||||
|
||||
final EditableRowNotifier rowNotifier;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(3.0),
|
||||
child: FlowySvg(
|
||||
FlowySvgs.edit_s,
|
||||
color: Theme.of(context).hintColor,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void onTap(BuildContext context) => rowNotifier.becomeFirstResponder();
|
||||
|
||||
@override
|
||||
AccessoryType get type => AccessoryType.edit;
|
||||
}
|
||||
|
||||
class RowCardStyleConfiguration {
|
||||
const RowCardStyleConfiguration({
|
||||
required this.cellStyleMap,
|
||||
|
@ -1,4 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import 'accessory.dart';
|
||||
@ -7,14 +8,16 @@ class RowCardContainer extends StatelessWidget {
|
||||
const RowCardContainer({
|
||||
super.key,
|
||||
required this.child,
|
||||
required this.openCard,
|
||||
required this.onTap,
|
||||
required this.openAccessory,
|
||||
required this.accessories,
|
||||
this.buildAccessoryWhen,
|
||||
this.onShiftTap,
|
||||
});
|
||||
|
||||
final Widget child;
|
||||
final void Function(BuildContext) openCard;
|
||||
final void Function(BuildContext) onTap;
|
||||
final void Function(BuildContext)? onShiftTap;
|
||||
final void Function(AccessoryType) openAccessory;
|
||||
final List<CardAccessory> accessories;
|
||||
final bool Function()? buildAccessoryWhen;
|
||||
@ -41,7 +44,13 @@ class RowCardContainer extends StatelessWidget {
|
||||
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () => openCard(context),
|
||||
onTap: () {
|
||||
if (HardwareKeyboard.instance.isShiftPressed) {
|
||||
onShiftTap?.call(context);
|
||||
} else {
|
||||
onTap(context);
|
||||
}
|
||||
},
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(minHeight: 30),
|
||||
child: container,
|
||||
|
@ -1,4 +1,3 @@
|
||||
import 'package:appflowy/plugins/database/application/cell/cell_controller.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
abstract class CardCell<T extends CardCellStyle> extends StatefulWidget {
|
||||
@ -32,61 +31,6 @@ class EditableCardNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
class EditableRowNotifier {
|
||||
EditableRowNotifier({required bool isEditing})
|
||||
: isEditing = ValueNotifier(isEditing);
|
||||
|
||||
final Map<CellContext, EditableCardNotifier> _cells = {};
|
||||
final ValueNotifier<bool> isEditing;
|
||||
|
||||
void bindCell(
|
||||
CellContext cellIdentifier,
|
||||
EditableCardNotifier notifier,
|
||||
) {
|
||||
assert(
|
||||
_cells.values.isEmpty,
|
||||
'Only one cell can receive the notification',
|
||||
);
|
||||
_cells[cellIdentifier]?.dispose();
|
||||
|
||||
notifier.isCellEditing.addListener(() {
|
||||
isEditing.value = notifier.isCellEditing.value;
|
||||
});
|
||||
|
||||
_cells[cellIdentifier] = notifier;
|
||||
}
|
||||
|
||||
void becomeFirstResponder() {
|
||||
if (_cells.values.isEmpty) return;
|
||||
assert(
|
||||
_cells.values.length == 1,
|
||||
'Only one cell can receive the notification',
|
||||
);
|
||||
_cells.values.first.isCellEditing.value = true;
|
||||
}
|
||||
|
||||
void resignFirstResponder() {
|
||||
if (_cells.values.isEmpty) return;
|
||||
assert(
|
||||
_cells.values.length == 1,
|
||||
'Only one cell can receive the notification',
|
||||
);
|
||||
_cells.values.first.isCellEditing.value = false;
|
||||
}
|
||||
|
||||
void unbind() {
|
||||
for (final notifier in _cells.values) {
|
||||
notifier.dispose();
|
||||
}
|
||||
_cells.clear();
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
unbind();
|
||||
isEditing.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
abstract mixin class EditableCell {
|
||||
// Each cell notifier will be bind to the [EditableRowNotifier], which enable
|
||||
// the row notifier receive its cells event. For example: begin editing the
|
||||
|
@ -8,6 +8,7 @@ import 'package:easy_localization/easy_localization.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';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import '../editable_cell_builder.dart';
|
||||
@ -108,18 +109,11 @@ class _TextCellState extends State<TextCardCell> {
|
||||
return BlocProvider.value(
|
||||
value: cellBloc,
|
||||
child: BlocConsumer<TextCellBloc, TextCellState>(
|
||||
listenWhen: (previous, current) =>
|
||||
previous.content != current.content && !current.enableEdit,
|
||||
listenWhen: (previous, current) => previous.content != current.content,
|
||||
listener: (context, state) {
|
||||
_textEditingController.text = state.content;
|
||||
},
|
||||
buildWhen: (previous, current) {
|
||||
if (previous.content != current.content &&
|
||||
_textEditingController.text == current.content) {
|
||||
return false;
|
||||
if (!state.enableEdit) {
|
||||
_textEditingController.text = state.content;
|
||||
}
|
||||
|
||||
return previous != current;
|
||||
},
|
||||
builder: (context, state) {
|
||||
final isTitle = cellBloc.cellController.fieldInfo.isPrimary;
|
||||
@ -196,31 +190,39 @@ class _TextCellState extends State<TextCardCell> {
|
||||
widget.style.padding.add(const EdgeInsets.symmetric(vertical: 4.0));
|
||||
return IgnorePointer(
|
||||
ignoring: !isEditing,
|
||||
child: TextField(
|
||||
controller: _textEditingController,
|
||||
focusNode: focusNode,
|
||||
onChanged: (_) {
|
||||
if (_textEditingController.value.composing.isCollapsed) {
|
||||
cellBloc.add(TextCellEvent.updateText(_textEditingController.text));
|
||||
}
|
||||
child: CallbackShortcuts(
|
||||
bindings: {
|
||||
const SingleActivator(LogicalKeyboardKey.escape): () =>
|
||||
focusNode.unfocus(),
|
||||
},
|
||||
onEditingComplete: () => focusNode.unfocus(),
|
||||
maxLines: isEditing ? null : 2,
|
||||
minLines: 1,
|
||||
textInputAction: TextInputAction.done,
|
||||
readOnly: !isEditing,
|
||||
enableInteractiveSelection: isEditing,
|
||||
style: widget.style.titleTextStyle,
|
||||
decoration: InputDecoration(
|
||||
contentPadding: padding,
|
||||
border: InputBorder.none,
|
||||
enabledBorder: InputBorder.none,
|
||||
isDense: true,
|
||||
isCollapsed: true,
|
||||
hintText: LocaleKeys.grid_row_titlePlaceholder.tr(),
|
||||
hintStyle: widget.style.titleTextStyle.copyWith(
|
||||
color: Theme.of(context).hintColor,
|
||||
child: TextField(
|
||||
controller: _textEditingController,
|
||||
focusNode: focusNode,
|
||||
onChanged: (_) {
|
||||
if (_textEditingController.value.composing.isCollapsed) {
|
||||
cellBloc
|
||||
.add(TextCellEvent.updateText(_textEditingController.text));
|
||||
}
|
||||
},
|
||||
onEditingComplete: () => focusNode.unfocus(),
|
||||
maxLines: isEditing ? null : 2,
|
||||
minLines: 1,
|
||||
textInputAction: TextInputAction.done,
|
||||
readOnly: !isEditing,
|
||||
enableInteractiveSelection: isEditing,
|
||||
style: widget.style.titleTextStyle,
|
||||
decoration: InputDecoration(
|
||||
contentPadding: padding,
|
||||
border: InputBorder.none,
|
||||
enabledBorder: InputBorder.none,
|
||||
isDense: true,
|
||||
isCollapsed: true,
|
||||
hintText: LocaleKeys.grid_row_titlePlaceholder.tr(),
|
||||
hintStyle: widget.style.titleTextStyle.copyWith(
|
||||
color: Theme.of(context).hintColor,
|
||||
),
|
||||
),
|
||||
onTapOutside: (_) {},
|
||||
),
|
||||
),
|
||||
);
|
||||
|
@ -80,7 +80,9 @@ class _TextCellState extends GridEditableTextCell<EditableTextCell> {
|
||||
value: cellBloc,
|
||||
child: BlocListener<TextCellBloc, TextCellState>(
|
||||
listener: (context, state) {
|
||||
_textEditingController.text = state.content;
|
||||
if (!focusNode.hasFocus) {
|
||||
_textEditingController.text = state.content;
|
||||
}
|
||||
},
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
|
@ -53,7 +53,7 @@ class RowDetailPageDeleteButton extends StatelessWidget {
|
||||
text: FlowyText.regular(LocaleKeys.grid_row_delete.tr()),
|
||||
leftIcon: const FlowySvg(FlowySvgs.trash_m),
|
||||
onTap: () {
|
||||
RowBackendService.deleteRow(viewId, rowId);
|
||||
RowBackendService.deleteRows(viewId, [rowId]);
|
||||
FlowyOverlay.pop(context);
|
||||
},
|
||||
),
|
||||
|
@ -288,7 +288,6 @@ class _TitleSkin extends IEditableTextCellSkin {
|
||||
return TextField(
|
||||
controller: textEditingController,
|
||||
focusNode: focusNode,
|
||||
maxLines: null,
|
||||
autofocus: true,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(fontSize: 28),
|
||||
decoration: InputDecoration(
|
||||
|
@ -0,0 +1,87 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class ConditionalListenableBuilder<T> extends StatefulWidget {
|
||||
const ConditionalListenableBuilder({
|
||||
super.key,
|
||||
required this.valueListenable,
|
||||
required this.buildWhen,
|
||||
required this.builder,
|
||||
this.child,
|
||||
});
|
||||
|
||||
/// The [ValueListenable] whose value you depend on in order to build.
|
||||
///
|
||||
/// This widget does not ensure that the [ValueListenable]'s value is not
|
||||
/// null, therefore your [builder] may need to handle null values.
|
||||
final ValueListenable<T> valueListenable;
|
||||
|
||||
/// The [buildWhen] function will be called on each value change of the
|
||||
/// [valueListenable]. If the [buildWhen] function returns true, the [builder]
|
||||
/// will be called with the new value of the [valueListenable].
|
||||
///
|
||||
final bool Function(T previous, T current) buildWhen;
|
||||
|
||||
/// A [ValueWidgetBuilder] which builds a widget depending on the
|
||||
/// [valueListenable]'s value.
|
||||
///
|
||||
/// Can incorporate a [valueListenable] value-independent widget subtree
|
||||
/// from the [child] parameter into the returned widget tree.
|
||||
final ValueWidgetBuilder<T> builder;
|
||||
|
||||
/// A [valueListenable]-independent widget which is passed back to the [builder].
|
||||
///
|
||||
/// This argument is optional and can be null if the entire widget subtree the
|
||||
/// [builder] builds depends on the value of the [valueListenable]. For
|
||||
/// example, in the case where the [valueListenable] is a [String] and the
|
||||
/// [builder] returns a [Text] widget with the current [String] value, there
|
||||
/// would be no useful [child].
|
||||
final Widget? child;
|
||||
|
||||
@override
|
||||
State<ConditionalListenableBuilder> createState() =>
|
||||
_ConditionalListenableBuilderState<T>();
|
||||
}
|
||||
|
||||
class _ConditionalListenableBuilderState<T>
|
||||
extends State<ConditionalListenableBuilder<T>> {
|
||||
late T value;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
value = widget.valueListenable.value;
|
||||
widget.valueListenable.addListener(_valueChanged);
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(ConditionalListenableBuilder<T> oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.valueListenable != widget.valueListenable) {
|
||||
oldWidget.valueListenable.removeListener(_valueChanged);
|
||||
value = widget.valueListenable.value;
|
||||
widget.valueListenable.addListener(_valueChanged);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
widget.valueListenable.removeListener(_valueChanged);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _valueChanged() {
|
||||
if (widget.buildWhen(value, widget.valueListenable.value)) {
|
||||
setState(() {
|
||||
value = widget.valueListenable.value;
|
||||
});
|
||||
} else {
|
||||
value = widget.valueListenable.value;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return widget.builder(context, value, widget.child);
|
||||
}
|
||||
}
|
@ -44,8 +44,8 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "."
|
||||
ref: "404262fca4369bc35ff305316e4d59341a732f56"
|
||||
resolved-ref: "404262fca4369bc35ff305316e4d59341a732f56"
|
||||
ref: "8a6434ae3d02624b614a010af80f775db11bf22e"
|
||||
resolved-ref: "8a6434ae3d02624b614a010af80f775db11bf22e"
|
||||
url: "https://github.com/AppFlowy-IO/appflowy-board.git"
|
||||
source: git
|
||||
version: "0.1.2"
|
||||
|
@ -44,7 +44,7 @@ dependencies:
|
||||
# path: ../../../appflowy-board
|
||||
git:
|
||||
url: https://github.com/AppFlowy-IO/appflowy-board.git
|
||||
ref: 404262fca4369bc35ff305316e4d59341a732f56
|
||||
ref: 8a6434ae3d02624b614a010af80f775db11bf22e
|
||||
appflowy_result:
|
||||
path: packages/appflowy_result
|
||||
appflowy_editor_plugins: ^0.0.2
|
||||
|
@ -1,5 +1,6 @@
|
||||
import 'package:appflowy/plugins/database/application/database_controller.dart';
|
||||
import 'package:appflowy/plugins/database/board/application/board_bloc.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import 'util.dart';
|
||||
@ -15,26 +16,42 @@ void main() {
|
||||
final context = await boardTest.createTestBoard();
|
||||
final databaseController = DatabaseController(view: context.gridView);
|
||||
final boardBloc = BoardBloc(
|
||||
view: context.gridView,
|
||||
databaseController: databaseController,
|
||||
)..add(const BoardEvent.initial());
|
||||
await boardResponseFuture();
|
||||
|
||||
final groupId = boardBloc.state.groupIds.last;
|
||||
List<String> groupIds = boardBloc.state.maybeMap(
|
||||
orElse: () => const [],
|
||||
ready: (value) => value.groupIds,
|
||||
);
|
||||
String lastGroupId = groupIds.last;
|
||||
|
||||
// the group at index 3 is the 'No status' group;
|
||||
assert(boardBloc.groupControllers[groupId]!.group.rows.isEmpty);
|
||||
assert(boardBloc.groupControllers[lastGroupId]!.group.rows.isEmpty);
|
||||
assert(
|
||||
boardBloc.state.groupIds.length == 4,
|
||||
'but receive ${boardBloc.state.groupIds.length}',
|
||||
groupIds.length == 4,
|
||||
'but receive ${groupIds.length}',
|
||||
);
|
||||
|
||||
boardBloc.add(BoardEvent.createBottomRow(boardBloc.state.groupIds[3]));
|
||||
boardBloc.add(
|
||||
BoardEvent.createRow(
|
||||
groupIds[3],
|
||||
OrderObjectPositionTypePB.End,
|
||||
null,
|
||||
null,
|
||||
),
|
||||
);
|
||||
await boardResponseFuture();
|
||||
|
||||
groupIds = boardBloc.state.maybeMap(
|
||||
orElse: () => [],
|
||||
ready: (value) => value.groupIds,
|
||||
);
|
||||
lastGroupId = groupIds.last;
|
||||
|
||||
assert(
|
||||
boardBloc.groupControllers[groupId]!.group.rows.length == 1,
|
||||
'but receive ${boardBloc.groupControllers[groupId]!.group.rows.length}',
|
||||
boardBloc.groupControllers[lastGroupId]!.group.rows.length == 1,
|
||||
'but receive ${boardBloc.groupControllers[lastGroupId]!.group.rows.length}',
|
||||
);
|
||||
});
|
||||
}
|
||||
|
@ -15,7 +15,6 @@ void main() {
|
||||
test('create build-in kanban board test', () async {
|
||||
final context = await boardTest.createTestBoard();
|
||||
final boardBloc = BoardBloc(
|
||||
view: context.gridView,
|
||||
databaseController: DatabaseController(view: context.gridView),
|
||||
)..add(const BoardEvent.initial());
|
||||
await boardResponseFuture();
|
||||
@ -27,7 +26,6 @@ void main() {
|
||||
test('edit kanban board field name test', () async {
|
||||
final context = await boardTest.createTestBoard();
|
||||
final boardBloc = BoardBloc(
|
||||
view: context.gridView,
|
||||
databaseController: DatabaseController(view: context.gridView),
|
||||
)..add(const BoardEvent.initial());
|
||||
await boardResponseFuture();
|
||||
@ -59,7 +57,6 @@ void main() {
|
||||
test('create a new field in kanban board test', () async {
|
||||
final context = await boardTest.createTestBoard();
|
||||
final boardBloc = BoardBloc(
|
||||
view: context.gridView,
|
||||
databaseController: DatabaseController(view: context.gridView),
|
||||
)..add(const BoardEvent.initial());
|
||||
await boardResponseFuture();
|
||||
|
@ -17,7 +17,6 @@ void main() {
|
||||
test('group by checkbox field test', () async {
|
||||
final context = await boardTest.createTestBoard();
|
||||
final boardBloc = BoardBloc(
|
||||
view: context.gridView,
|
||||
databaseController: DatabaseController(view: context.gridView),
|
||||
)..add(const BoardEvent.initial());
|
||||
await boardResponseFuture();
|
||||
|
@ -41,7 +41,6 @@ void main() {
|
||||
|
||||
// assert only have the 'No status' group
|
||||
final boardBloc = BoardBloc(
|
||||
view: context.gridView,
|
||||
databaseController: DatabaseController(view: context.gridView),
|
||||
)..add(const BoardEvent.initial());
|
||||
await boardResponseFuture();
|
||||
@ -91,7 +90,6 @@ void main() {
|
||||
|
||||
// assert there are only three group
|
||||
final boardBloc = BoardBloc(
|
||||
view: context.gridView,
|
||||
databaseController: DatabaseController(view: context.gridView),
|
||||
)..add(const BoardEvent.initial());
|
||||
await boardResponseFuture();
|
||||
|
@ -38,7 +38,6 @@ void main() {
|
||||
blocTest<BoardBloc, BoardState>(
|
||||
'assert the number of groups is 1',
|
||||
build: () => BoardBloc(
|
||||
view: context.gridView,
|
||||
databaseController: DatabaseController(view: context.gridView),
|
||||
)..add(
|
||||
const BoardEvent.initial(),
|
||||
|
@ -236,11 +236,10 @@ impl EventIntegrationTest {
|
||||
|
||||
pub async fn delete_row(&self, view_id: &str, row_id: &str) -> Option<FlowyError> {
|
||||
EventBuilder::new(self.clone())
|
||||
.event(DatabaseEvent::DeleteRow)
|
||||
.payload(RowIdPB {
|
||||
.event(DatabaseEvent::DeleteRows)
|
||||
.payload(RepeatedRowIdPB {
|
||||
view_id: view_id.to_string(),
|
||||
row_id: row_id.to_string(),
|
||||
group_id: None,
|
||||
row_ids: vec![row_id.to_string()],
|
||||
})
|
||||
.async_send()
|
||||
.await
|
||||
@ -523,7 +522,7 @@ impl EventIntegrationTest {
|
||||
) -> Vec<RelatedRowDataPB> {
|
||||
EventBuilder::new(self.clone())
|
||||
.event(DatabaseEvent::GetRelatedRowDatas)
|
||||
.payload(RepeatedRowIdPB {
|
||||
.payload(GetRelatedRowDataPB {
|
||||
database_id,
|
||||
row_ids,
|
||||
})
|
||||
|
@ -314,9 +314,12 @@ async fn delete_row_event_with_invalid_row_id_test() {
|
||||
.create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![])
|
||||
.await;
|
||||
|
||||
// delete the row with empty row_id. It should return an error.
|
||||
// delete the row with empty row_id. It should do nothing
|
||||
let error = test.delete_row(&grid_view.id, "").await;
|
||||
assert!(error.is_some());
|
||||
assert!(error.is_none());
|
||||
|
||||
let database = test.get_database(&grid_view.id).await;
|
||||
assert_eq!(database.rows.len(), 3);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
@ -338,6 +338,15 @@ impl TryInto<RowIdParams> for RowIdPB {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, ProtoBuf)]
|
||||
pub struct RepeatedRowIdPB {
|
||||
#[pb(index = 1)]
|
||||
pub view_id: String,
|
||||
|
||||
#[pb(index = 2)]
|
||||
pub row_ids: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(ProtoBuf, Default, Validate)]
|
||||
pub struct CreateRowPayloadPB {
|
||||
#[pb(index = 1)]
|
||||
|
@ -78,7 +78,7 @@ pub struct RepeatedRelatedRowDataPB {
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, ProtoBuf)]
|
||||
pub struct RepeatedRowIdPB {
|
||||
pub struct GetRelatedRowDataPB {
|
||||
#[pb(index = 1)]
|
||||
pub database_id: String,
|
||||
|
||||
|
@ -385,14 +385,19 @@ pub(crate) async fn update_row_meta_handler(
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "debug", skip(data, manager), err)]
|
||||
pub(crate) async fn delete_row_handler(
|
||||
data: AFPluginData<RowIdPB>,
|
||||
pub(crate) async fn delete_rows_handler(
|
||||
data: AFPluginData<RepeatedRowIdPB>,
|
||||
manager: AFPluginState<Weak<DatabaseManager>>,
|
||||
) -> Result<(), FlowyError> {
|
||||
let manager = upgrade_manager(manager)?;
|
||||
let params: RowIdParams = data.into_inner().try_into()?;
|
||||
let params: RepeatedRowIdPB = data.into_inner();
|
||||
let database_editor = manager.get_database_with_view_id(¶ms.view_id).await?;
|
||||
database_editor.delete_row(¶ms.row_id).await;
|
||||
let row_ids = params
|
||||
.row_ids
|
||||
.into_iter()
|
||||
.map(RowId::from)
|
||||
.collect::<Vec<_>>();
|
||||
database_editor.delete_rows(&row_ids).await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -1062,11 +1067,11 @@ pub(crate) async fn update_relation_cell_handler(
|
||||
}
|
||||
|
||||
pub(crate) async fn get_related_row_datas_handler(
|
||||
data: AFPluginData<RepeatedRowIdPB>,
|
||||
data: AFPluginData<GetRelatedRowDataPB>,
|
||||
manager: AFPluginState<Weak<DatabaseManager>>,
|
||||
) -> DataResult<RepeatedRelatedRowDataPB, FlowyError> {
|
||||
let manager = upgrade_manager(manager)?;
|
||||
let params: RepeatedRowIdPB = data.into_inner();
|
||||
let params: GetRelatedRowDataPB = data.into_inner();
|
||||
let database_editor = manager.get_database(¶ms.database_id).await?;
|
||||
let row_datas = database_editor
|
||||
.get_related_rows(Some(¶ms.row_ids))
|
||||
|
@ -37,7 +37,7 @@ pub fn init(database_manager: Weak<DatabaseManager>) -> AFPlugin {
|
||||
.event(DatabaseEvent::GetRow, get_row_handler)
|
||||
.event(DatabaseEvent::GetRowMeta, get_row_meta_handler)
|
||||
.event(DatabaseEvent::UpdateRowMeta, update_row_meta_handler)
|
||||
.event(DatabaseEvent::DeleteRow, delete_row_handler)
|
||||
.event(DatabaseEvent::DeleteRows, delete_rows_handler)
|
||||
.event(DatabaseEvent::DuplicateRow, duplicate_row_handler)
|
||||
.event(DatabaseEvent::MoveRow, move_row_handler)
|
||||
// Cell
|
||||
@ -223,8 +223,8 @@ pub enum DatabaseEvent {
|
||||
#[event(input = "RowIdPB", output = "OptionalRowPB")]
|
||||
GetRow = 51,
|
||||
|
||||
#[event(input = "RowIdPB")]
|
||||
DeleteRow = 52,
|
||||
#[event(input = "RepeatedRowIdPB")]
|
||||
DeleteRows = 52,
|
||||
|
||||
#[event(input = "RowIdPB")]
|
||||
DuplicateRow = 53,
|
||||
@ -364,7 +364,7 @@ pub enum DatabaseEvent {
|
||||
UpdateRelationCell = 171,
|
||||
|
||||
/// Get the names of the linked rows in a relation cell.
|
||||
#[event(input = "RepeatedRowIdPB", output = "RepeatedRelatedRowDataPB")]
|
||||
#[event(input = "GetRelatedRowDataPB", output = "RepeatedRelatedRowDataPB")]
|
||||
GetRelatedRowDatas = 172,
|
||||
|
||||
/// Get the names of all the rows in a related database.
|
||||
|
@ -654,9 +654,10 @@ impl DatabaseEditor {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn delete_row(&self, row_id: &RowId) {
|
||||
let row = self.database.lock().remove_row(row_id);
|
||||
if let Some(row) = row {
|
||||
pub async fn delete_rows(&self, row_ids: &[RowId]) {
|
||||
let rows = self.database.lock().remove_rows(row_ids);
|
||||
|
||||
for row in rows {
|
||||
tracing::trace!("Did delete row:{:?}", row);
|
||||
for view in self.database_views.editors().await {
|
||||
view.v_did_delete_row(&row).await;
|
||||
|
@ -148,8 +148,8 @@ impl DatabaseGroupTest {
|
||||
row_index,
|
||||
} => {
|
||||
let row = self.row_at_index(group_index, row_index).await;
|
||||
let row_id = RowId::from(row.id);
|
||||
self.editor.delete_row(&row_id).await;
|
||||
let row_ids = vec![RowId::from(row.id)];
|
||||
self.editor.delete_rows(&row_ids).await;
|
||||
},
|
||||
GroupScript::UpdateGroupedCell {
|
||||
from_group_index,
|
||||
|
Loading…
Reference in New Issue
Block a user