mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00: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/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/board/presentation/widgets/board_column_header.dart';
|
||||||
import 'package:appflowy/plugins/database/widgets/card/card.dart';
|
import 'package:appflowy/plugins/database/widgets/card/card.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||||
@ -82,23 +83,19 @@ void main() {
|
|||||||
findsOneWidget,
|
findsOneWidget,
|
||||||
);
|
);
|
||||||
|
|
||||||
await tester.tap(
|
await tester.tapButton(
|
||||||
find
|
find.byType(BoardColumnFooter).at(1),
|
||||||
.descendant(
|
|
||||||
of: find.byType(AppFlowyGroupFooter),
|
|
||||||
matching: find.byType(FlowySvg),
|
|
||||||
)
|
|
||||||
.at(1),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const newCardName = 'Card 4';
|
const newCardName = 'Card 4';
|
||||||
await tester.enterText(
|
await tester.enterText(
|
||||||
find.descendant(
|
find.descendant(
|
||||||
of: lastCard,
|
of: find.byType(BoardColumnFooter),
|
||||||
matching: find.byType(TextField),
|
matching: find.byType(TextField),
|
||||||
),
|
),
|
||||||
newCardName,
|
newCardName,
|
||||||
);
|
);
|
||||||
|
await tester.testTextInput.receiveAction(TextInputAction.done);
|
||||||
await tester.pumpAndSettle(const Duration(milliseconds: 500));
|
await tester.pumpAndSettle(const Duration(milliseconds: 500));
|
||||||
|
|
||||||
await tester.tap(find.byType(AppFlowyBoard));
|
await tester.tap(find.byType(AppFlowyBoard));
|
||||||
|
@ -56,7 +56,7 @@ void main() {
|
|||||||
expect(
|
expect(
|
||||||
find.descendant(
|
find.descendant(
|
||||||
of: find.byType(AppFlowyEditor),
|
of: find.byType(AppFlowyEditor),
|
||||||
matching: find.byType(BoardPage),
|
matching: find.byType(DesktopBoardPage),
|
||||||
),
|
),
|
||||||
findsOneWidget,
|
findsOneWidget,
|
||||||
);
|
);
|
||||||
@ -104,7 +104,7 @@ void main() {
|
|||||||
expect(
|
expect(
|
||||||
find.descendant(
|
find.descendant(
|
||||||
of: find.byType(AppFlowyEditor),
|
of: find.byType(AppFlowyEditor),
|
||||||
matching: find.byType(BoardPage),
|
matching: find.byType(DesktopBoardPage),
|
||||||
),
|
),
|
||||||
findsOneWidget,
|
findsOneWidget,
|
||||||
);
|
);
|
||||||
|
@ -61,7 +61,7 @@ void main() {
|
|||||||
expect(find.byType(GridPage), findsOneWidget);
|
expect(find.byType(GridPage), findsOneWidget);
|
||||||
break;
|
break;
|
||||||
case ViewLayoutPB.Board:
|
case ViewLayoutPB.Board:
|
||||||
expect(find.byType(BoardPage), findsOneWidget);
|
expect(find.byType(DesktopBoardPage), findsOneWidget);
|
||||||
break;
|
break;
|
||||||
case ViewLayoutPB.Calendar:
|
case ViewLayoutPB.Calendar:
|
||||||
expect(find.byType(CalendarPage), findsOneWidget);
|
expect(find.byType(CalendarPage), findsOneWidget);
|
||||||
|
@ -1463,7 +1463,7 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
|||||||
|
|
||||||
void assertCurrentDatabaseTagIs(DatabaseLayoutPB layout) => switch (layout) {
|
void assertCurrentDatabaseTagIs(DatabaseLayoutPB layout) => switch (layout) {
|
||||||
DatabaseLayoutPB.Board =>
|
DatabaseLayoutPB.Board =>
|
||||||
expect(find.byType(BoardPage), findsOneWidget),
|
expect(find.byType(DesktopBoardPage), findsOneWidget),
|
||||||
DatabaseLayoutPB.Calendar =>
|
DatabaseLayoutPB.Calendar =>
|
||||||
expect(find.byType(CalendarPage), findsOneWidget),
|
expect(find.byType(CalendarPage), findsOneWidget),
|
||||||
DatabaseLayoutPB.Grid => expect(find.byType(GridPage), findsOneWidget),
|
DatabaseLayoutPB.Grid => expect(find.byType(GridPage), findsOneWidget),
|
||||||
@ -1521,7 +1521,7 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Finder finderForDatabaseLayoutType(DatabaseLayoutPB layout) => switch (layout) {
|
Finder finderForDatabaseLayoutType(DatabaseLayoutPB layout) => switch (layout) {
|
||||||
DatabaseLayoutPB.Board => find.byType(BoardPage),
|
DatabaseLayoutPB.Board => find.byType(DesktopBoardPage),
|
||||||
DatabaseLayoutPB.Calendar => find.byType(CalendarPage),
|
DatabaseLayoutPB.Calendar => find.byType(CalendarPage),
|
||||||
DatabaseLayoutPB.Grid => find.byType(GridPage),
|
DatabaseLayoutPB.Grid => find.byType(GridPage),
|
||||||
_ => throw Exception('Unknown database layout type: $layout'),
|
_ => throw Exception('Unknown database layout type: $layout'),
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
export 'mobile_board_screen.dart';
|
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_hidden_groups_column.dart';
|
||||||
export 'widgets/mobile_board_trailing.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/board.dart';
|
||||||
import 'package:appflowy/mobile/presentation/database/board/widgets/group_card_header.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/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/board/application/board_bloc.dart';
|
||||||
import 'package:appflowy/plugins/database/grid/presentation/widgets/header/field_type_extension.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/card/card.dart';
|
||||||
import 'package:appflowy/plugins/database/widgets/cell/card_cell_builder.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/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/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:appflowy_board/appflowy_board.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flowy_infra_ui/flowy_infra_ui.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:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
||||||
class MobileBoardContent extends StatefulWidget {
|
class MobileBoardPage extends StatefulWidget {
|
||||||
const MobileBoardContent({
|
const MobileBoardPage({
|
||||||
super.key,
|
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
|
@override
|
||||||
State<MobileBoardContent> createState() => _MobileBoardContentState();
|
State<MobileBoardPage> createState() => _MobileBoardPageState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _MobileBoardContentState extends State<MobileBoardContent> {
|
class _MobileBoardPageState extends State<MobileBoardPage> {
|
||||||
late final ScrollController scrollController;
|
late final ValueNotifier<DidCreateRowResult?> _didCreateRow;
|
||||||
late final AppFlowyBoardScrollController scrollManager;
|
|
||||||
|
@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
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
// mobile may not need this
|
|
||||||
// scroll to bottom when add a new card
|
|
||||||
scrollManager = AppFlowyBoardScrollController();
|
|
||||||
scrollController = ScrollController();
|
scrollController = ScrollController();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -57,29 +136,22 @@ class _MobileBoardContentState extends State<MobileBoardContent> {
|
|||||||
cardMargin: const EdgeInsets.all(4),
|
cardMargin: const EdgeInsets.all(4),
|
||||||
);
|
);
|
||||||
|
|
||||||
return BlocListener<BoardBloc, BoardState>(
|
return BlocBuilder<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,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
child: BlocBuilder<BoardBloc, BoardState>(
|
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
final showCreateGroupButton =
|
return state.maybeMap(
|
||||||
context.read<BoardBloc>().groupingFieldType.canCreateNewGroup;
|
orElse: () => const SizedBox.shrink(),
|
||||||
|
ready: (state) {
|
||||||
|
final showCreateGroupButton = context
|
||||||
|
.read<BoardBloc>()
|
||||||
|
.groupingFieldType
|
||||||
|
?.canCreateNewGroup ??
|
||||||
|
false;
|
||||||
final showHiddenGroups = state.hiddenGroups.isNotEmpty;
|
final showHiddenGroups = state.hiddenGroups.isNotEmpty;
|
||||||
return AppFlowyBoard(
|
return AppFlowyBoard(
|
||||||
boardScrollController: scrollManager,
|
|
||||||
scrollController: scrollController,
|
scrollController: scrollController,
|
||||||
controller: context.read<BoardBloc>().boardController,
|
controller: context.read<BoardBloc>().boardController,
|
||||||
groupConstraints: BoxConstraints.tightFor(width: screenWidth * 0.7),
|
groupConstraints:
|
||||||
|
BoxConstraints.tightFor(width: screenWidth * 0.7),
|
||||||
config: config,
|
config: config,
|
||||||
leading: showHiddenGroups
|
leading: showHiddenGroups
|
||||||
? MobileHiddenGroupsColumn(
|
? MobileHiddenGroupsColumn(
|
||||||
@ -104,7 +176,8 @@ class _MobileBoardContentState extends State<MobileBoardContent> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -129,9 +202,14 @@ class _MobileBoardContentState extends State<MobileBoardContent> {
|
|||||||
color: style.colorScheme.onSurface,
|
color: style.colorScheme.onSurface,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
onPressed: () => context
|
onPressed: () => context.read<BoardBloc>().add(
|
||||||
.read<BoardBloc>()
|
BoardEvent.createRow(
|
||||||
.add(BoardEvent.createBottomRow(columnData.id)),
|
columnData.id,
|
||||||
|
OrderObjectPositionTypePB.End,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -146,16 +224,9 @@ class _MobileBoardContentState extends State<MobileBoardContent> {
|
|||||||
final groupItem = afGroupItem as GroupItem;
|
final groupItem = afGroupItem as GroupItem;
|
||||||
final groupData = afGroupData.customData as GroupData;
|
final groupData = afGroupData.customData as GroupData;
|
||||||
final rowMeta = groupItem.row;
|
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 =
|
final cellBuilder =
|
||||||
CardCellBuilder(databaseController: boardBloc.databaseController);
|
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;
|
final groupItemId = groupItem.row.id + groupData.group.groupId;
|
||||||
|
|
||||||
@ -166,12 +237,12 @@ class _MobileBoardContentState extends State<MobileBoardContent> {
|
|||||||
child: RowCard(
|
child: RowCard(
|
||||||
fieldController: boardBloc.fieldController,
|
fieldController: boardBloc.fieldController,
|
||||||
rowMeta: rowMeta,
|
rowMeta: rowMeta,
|
||||||
viewId: viewId,
|
viewId: boardBloc.viewId,
|
||||||
rowCache: rowCache,
|
rowCache: boardBloc.rowCache,
|
||||||
groupingFieldId: groupItem.fieldInfo.id,
|
groupingFieldId: groupItem.fieldInfo.id,
|
||||||
isEditing: isEditing,
|
isEditing: false,
|
||||||
cellBuilder: cellBuilder,
|
cellBuilder: cellBuilder,
|
||||||
openCard: (context) {
|
onTap: (context) {
|
||||||
context.push(
|
context.push(
|
||||||
MobileRowDetailPage.routeName,
|
MobileRowDetailPage.routeName,
|
||||||
extra: {
|
extra: {
|
||||||
@ -181,10 +252,8 @@ class _MobileBoardContentState extends State<MobileBoardContent> {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
onStartEditing: () => boardBloc
|
onStartEditing: () {},
|
||||||
.add(BoardEvent.startEditingRow(groupData.group, groupItem.row)),
|
onEndEditing: () {},
|
||||||
onEndEditing: () =>
|
|
||||||
boardBloc.add(BoardEvent.endEditingRow(groupItem.row.id)),
|
|
||||||
styleConfiguration: RowCardStyleConfiguration(
|
styleConfiguration: RowCardStyleConfiguration(
|
||||||
cellStyleMap: mobileBoardCardCellStyleMap(context),
|
cellStyleMap: mobileBoardCardCellStyleMap(context),
|
||||||
showAccessory: false,
|
showAccessory: false,
|
@ -1,16 +1,14 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.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/mobile/presentation/widgets/flowy_mobile_quick_action_button.dart';
|
||||||
import 'package:appflowy/plugins/database/board/application/board_bloc.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/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/protobuf.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-database2/group.pb.dart';
|
|
||||||
import 'package:appflowy_board/appflowy_board.dart';
|
import 'package:appflowy_board/appflowy_board.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
||||||
@ -73,8 +71,12 @@ class _GroupCardHeaderState extends State<GroupCardHeader> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.isEditingHeader &&
|
final isEditing = state.maybeMap(
|
||||||
state.editingHeaderId == widget.groupData.id) {
|
ready: (value) => value.editingHeaderId == widget.groupData.id,
|
||||||
|
orElse: () => false,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isEditing) {
|
||||||
title = TextField(
|
title = TextField(
|
||||||
controller: _controller,
|
controller: _controller,
|
||||||
autofocus: true,
|
autofocus: true,
|
||||||
@ -135,7 +137,7 @@ class _GroupCardHeaderState extends State<GroupCardHeader> {
|
|||||||
icon: FlowySvgs.hide_s,
|
icon: FlowySvgs.hide_s,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
context.read<BoardBloc>().add(
|
context.read<BoardBloc>().add(
|
||||||
BoardEvent.toggleGroupVisibility(
|
BoardEvent.setGroupVisibility(
|
||||||
widget.groupData.customData.group
|
widget.groupData.customData.group
|
||||||
as GroupPB,
|
as GroupPB,
|
||||||
false,
|
false,
|
||||||
@ -154,9 +156,16 @@ class _GroupCardHeaderState extends State<GroupCardHeader> {
|
|||||||
color: Theme.of(context).colorScheme.onSurface,
|
color: Theme.of(context).colorScheme.onSurface,
|
||||||
),
|
),
|
||||||
splashRadius: 5,
|
splashRadius: 5,
|
||||||
onPressed: () => context.read<BoardBloc>().add(
|
onPressed: () {
|
||||||
BoardEvent.createHeaderRow(widget.groupData.id),
|
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) {
|
Widget build(BuildContext context) {
|
||||||
final databaseController = context.read<BoardBloc>().databaseController;
|
final databaseController = context.read<BoardBloc>().databaseController;
|
||||||
return BlocSelector<BoardBloc, BoardState, BoardLayoutSettingPB?>(
|
return BlocSelector<BoardBloc, BoardState, BoardLayoutSettingPB?>(
|
||||||
selector: (state) => state.layoutSettings,
|
selector: (state) => state.maybeMap(
|
||||||
|
orElse: () => null,
|
||||||
|
ready: (value) => value.layoutSettings,
|
||||||
|
),
|
||||||
builder: (context, layoutSettings) {
|
builder: (context, layoutSettings) {
|
||||||
if (layoutSettings == null) {
|
if (layoutSettings == null) {
|
||||||
return const SizedBox.shrink();
|
return const SizedBox.shrink();
|
||||||
@ -105,7 +108,11 @@ class MobileHiddenGroupList extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return BlocBuilder<BoardBloc, BoardState>(
|
return BlocBuilder<BoardBloc, BoardState>(
|
||||||
builder: (_, state) => ReorderableListView.builder(
|
builder: (_, state) {
|
||||||
|
return state.maybeMap(
|
||||||
|
orElse: () => const SizedBox.shrink(),
|
||||||
|
ready: (state) {
|
||||||
|
return ReorderableListView.builder(
|
||||||
itemCount: state.hiddenGroups.length,
|
itemCount: state.hiddenGroups.length,
|
||||||
itemBuilder: (_, index) => MobileHiddenGroup(
|
itemBuilder: (_, index) => MobileHiddenGroup(
|
||||||
key: ValueKey(state.hiddenGroups[index].groupId),
|
key: ValueKey(state.hiddenGroups[index].groupId),
|
||||||
@ -127,7 +134,10 @@ class MobileHiddenGroupList extends StatelessWidget {
|
|||||||
.read<BoardBloc>()
|
.read<BoardBloc>()
|
||||||
.add(BoardEvent.reorderGroup(fromGroupId, toGroupId));
|
.add(BoardEvent.reorderGroup(fromGroupId, toGroupId));
|
||||||
},
|
},
|
||||||
),
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -148,15 +158,6 @@ class MobileHiddenGroup extends StatelessWidget {
|
|||||||
final primaryField = databaseController.fieldController.fieldInfos
|
final primaryField = databaseController.fieldController.fieldInfos
|
||||||
.firstWhereOrNull((element) => element.isPrimary)!;
|
.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(
|
final cells = group.rows.map(
|
||||||
(item) {
|
(item) {
|
||||||
final cellContext =
|
final cellContext =
|
||||||
@ -171,8 +172,7 @@ class MobileHiddenGroup extends StatelessWidget {
|
|||||||
visualDensity: VisualDensity.compact,
|
visualDensity: VisualDensity.compact,
|
||||||
),
|
),
|
||||||
child: CardCellBuilder(
|
child: CardCellBuilder(
|
||||||
databaseController:
|
databaseController: context.read<BoardBloc>().databaseController,
|
||||||
context.read<BoardBloc>().databaseController,
|
|
||||||
).build(
|
).build(
|
||||||
cellContext: cellContext,
|
cellContext: cellContext,
|
||||||
styleMap: {FieldType.RichText: _titleCellStyle(context)},
|
styleMap: {FieldType.RichText: _titleCellStyle(context)},
|
||||||
@ -221,20 +221,15 @@ class MobileHiddenGroup extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
actionButtonTitle: LocaleKeys.button_yes.tr(),
|
actionButtonTitle: LocaleKeys.button_yes.tr(),
|
||||||
actionButtonColor: Theme.of(context).colorScheme.primary,
|
actionButtonColor: Theme.of(context).colorScheme.primary,
|
||||||
onActionButtonPressed: () => context.read<BoardBloc>().add(
|
onActionButtonPressed: () => context
|
||||||
BoardEvent.toggleGroupVisibility(
|
.read<BoardBloc>()
|
||||||
group,
|
.add(BoardEvent.setGroupVisibility(group, true)),
|
||||||
true,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
children: cells,
|
children: cells,
|
||||||
);
|
);
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
TextCardCellStyle _titleCellStyle(BuildContext context) {
|
TextCardCellStyle _titleCellStyle(BuildContext context) {
|
||||||
|
@ -162,7 +162,7 @@ class _MobileRowDetailPageState extends State<MobileRowDetailPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
deleteRow
|
deleteRow
|
||||||
? RowBackendService.deleteRow(viewId, rowId)
|
? RowBackendService.deleteRows(viewId, [rowId])
|
||||||
: RowBackendService.duplicateRow(viewId, rowId);
|
: RowBackendService.duplicateRow(viewId, rowId);
|
||||||
|
|
||||||
context
|
context
|
||||||
|
@ -48,7 +48,7 @@ class RelationCellBloc extends Bloc<RelationCellEvent, RelationCellState> {
|
|||||||
emit(state.copyWith(rows: const []));
|
emit(state.copyWith(rows: const []));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final payload = RepeatedRowIdPB(
|
final payload = GetRelatedRowDataPB(
|
||||||
databaseId: state.relatedDatabaseMeta!.databaseId,
|
databaseId: state.relatedDatabaseMeta!.databaseId,
|
||||||
rowIds: cellData.rowIds,
|
rowIds: cellData.rowIds,
|
||||||
);
|
);
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import 'package:appflowy/workspace/application/view/view_service.dart';
|
import 'package:appflowy/workspace/application/view/view_service.dart';
|
||||||
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
||||||
|
import 'package:appflowy_result/appflowy_result.dart';
|
||||||
import 'package:bloc/bloc.dart';
|
import 'package:bloc/bloc.dart';
|
||||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
|
|
||||||
@ -12,11 +13,9 @@ class RelationDatabaseListCubit extends Cubit<RelationDatabaseListState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _loadDatabaseMetas() async {
|
void _loadDatabaseMetas() async {
|
||||||
final getDatabaseResult = await DatabaseEventGetDatabases().send();
|
final metaPBs = await DatabaseEventGetDatabases()
|
||||||
final metaPBs = getDatabaseResult.fold<List<DatabaseMetaPB>>(
|
.send()
|
||||||
(s) => s.items,
|
.fold<List<DatabaseMetaPB>>((s) => s.items, (f) => []);
|
||||||
(f) => [],
|
|
||||||
);
|
|
||||||
final futures = metaPBs.map((meta) {
|
final futures = metaPBs.map((meta) {
|
||||||
return ViewBackendService.getView(meta.inlineViewId).then(
|
return ViewBackendService.getView(meta.inlineViewId).then(
|
||||||
(result) => result.fold(
|
(result) => result.fold(
|
||||||
|
@ -255,9 +255,7 @@ class RowCache {
|
|||||||
|
|
||||||
RowInfo buildGridRow(RowMetaPB rowMetaPB) {
|
RowInfo buildGridRow(RowMetaPB rowMetaPB) {
|
||||||
return RowInfo(
|
return RowInfo(
|
||||||
viewId: viewId,
|
|
||||||
fields: _fieldDelegate.fieldInfos,
|
fields: _fieldDelegate.fieldInfos,
|
||||||
rowId: rowMetaPB.id,
|
|
||||||
rowMeta: rowMetaPB,
|
rowMeta: rowMetaPB,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -285,12 +283,13 @@ class RowChangesetNotifier extends ChangeNotifier {
|
|||||||
|
|
||||||
@unfreezed
|
@unfreezed
|
||||||
class RowInfo with _$RowInfo {
|
class RowInfo with _$RowInfo {
|
||||||
|
const RowInfo._();
|
||||||
factory RowInfo({
|
factory RowInfo({
|
||||||
required String rowId,
|
|
||||||
required String viewId,
|
|
||||||
required UnmodifiableListView<FieldInfo> fields,
|
required UnmodifiableListView<FieldInfo> fields,
|
||||||
required RowMetaPB rowMeta,
|
required RowMetaPB rowMeta,
|
||||||
}) = _RowInfo;
|
}) = _RowInfo;
|
||||||
|
|
||||||
|
String get rowId => rowMeta.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
typedef InsertedIndexs = List<InsertedIndex>;
|
typedef InsertedIndexs = List<InsertedIndex>;
|
||||||
|
@ -96,15 +96,15 @@ class RowBackendService {
|
|||||||
return DatabaseEventUpdateRowMeta(payload).send();
|
return DatabaseEventUpdateRowMeta(payload).send();
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<FlowyResult<void, FlowyError>> deleteRow(
|
static Future<FlowyResult<void, FlowyError>> deleteRows(
|
||||||
String viewId,
|
String viewId,
|
||||||
RowId rowId,
|
List<RowId> rowIds,
|
||||||
) {
|
) {
|
||||||
final payload = RowIdPB.create()
|
final payload = RepeatedRowIdPB.create()
|
||||||
..viewId = viewId
|
..viewId = viewId
|
||||||
..rowId = rowId;
|
..rowIds.addAll(rowIds);
|
||||||
|
|
||||||
return DatabaseEventDeleteRow(payload).send();
|
return DatabaseEventDeleteRows(payload).send();
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<FlowyResult<void, FlowyError>> duplicateRow(
|
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/log.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.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-error/errors.pb.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
|
||||||
import 'package:appflowy_board/appflowy_board.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:appflowy_result/appflowy_result.dart';
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:equatable/equatable.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
@ -27,11 +28,31 @@ part 'board_bloc.freezed.dart';
|
|||||||
|
|
||||||
class BoardBloc extends Bloc<BoardEvent, BoardState> {
|
class BoardBloc extends Bloc<BoardEvent, BoardState> {
|
||||||
BoardBloc({
|
BoardBloc({
|
||||||
required ViewPB view,
|
|
||||||
required this.databaseController,
|
required this.databaseController,
|
||||||
}) : super(BoardState.initial(view.id)) {
|
this.didCreateRow,
|
||||||
|
AppFlowyBoardController? boardController,
|
||||||
|
}) : super(const BoardState.loading()) {
|
||||||
groupBackendSvc = GroupBackendService(viewId);
|
groupBackendSvc = GroupBackendService(viewId);
|
||||||
boardController = AppFlowyBoardController(
|
_initBoardController(boardController);
|
||||||
|
_dispatch();
|
||||||
|
}
|
||||||
|
|
||||||
|
final DatabaseController databaseController;
|
||||||
|
late final AppFlowyBoardController boardController;
|
||||||
|
final LinkedHashMap<String, GroupController> groupControllers =
|
||||||
|
LinkedHashMap();
|
||||||
|
final List<GroupPB> groupList = [];
|
||||||
|
|
||||||
|
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) =>
|
onMoveGroup: (fromGroupId, fromIndex, toGroupId, toIndex) =>
|
||||||
databaseController.moveGroup(
|
databaseController.moveGroup(
|
||||||
fromGroupId: fromGroupId,
|
fromGroupId: fromGroupId,
|
||||||
@ -50,7 +71,8 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
onMoveGroupItemToGroup: (fromGroupId, fromIndex, toGroupId, toIndex) {
|
onMoveGroupItemToGroup: (fromGroupId, fromIndex, toGroupId, toIndex) {
|
||||||
final fromRow = groupControllers[fromGroupId]?.rowAtIndex(fromIndex);
|
final fromRow =
|
||||||
|
groupControllers[fromGroupId]?.rowAtIndex(fromIndex);
|
||||||
final toRow = groupControllers[toGroupId]?.rowAtIndex(toIndex);
|
final toRow = groupControllers[toGroupId]?.rowAtIndex(toIndex);
|
||||||
if (fromRow != null) {
|
if (fromRow != null) {
|
||||||
databaseController.moveGroupRow(
|
databaseController.moveGroupRow(
|
||||||
@ -62,60 +84,52 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
_dispatch();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final DatabaseController databaseController;
|
|
||||||
final LinkedHashMap<String, GroupController> groupControllers =
|
|
||||||
LinkedHashMap();
|
|
||||||
final List<GroupPB> groupList = [];
|
|
||||||
|
|
||||||
late final AppFlowyBoardController boardController;
|
|
||||||
late final GroupBackendService groupBackendSvc;
|
|
||||||
|
|
||||||
FieldController get fieldController => databaseController.fieldController;
|
|
||||||
String get viewId => databaseController.viewId;
|
|
||||||
|
|
||||||
void _dispatch() {
|
void _dispatch() {
|
||||||
on<BoardEvent>(
|
on<BoardEvent>(
|
||||||
(event, emit) async {
|
(event, emit) async {
|
||||||
await event.when(
|
await event.when(
|
||||||
initial: () async {
|
initial: () async {
|
||||||
|
emit(BoardState.initial(viewId));
|
||||||
_startListening();
|
_startListening();
|
||||||
await _openGrid(emit);
|
await _openDatabase(emit);
|
||||||
},
|
},
|
||||||
createHeaderRow: (groupId) async {
|
createRow: (groupId, position, title, targetRowId) async {
|
||||||
final rowId = groupControllers[groupId]?.firstRow()?.id;
|
final primaryField = databaseController.fieldController.fieldInfos
|
||||||
final position = rowId == null
|
.firstWhereOrNull((element) => element.isPrimary)!;
|
||||||
? OrderObjectPositionTypePB.Start
|
final void Function(RowDataBuilder)? cellBuilder = title == null
|
||||||
: OrderObjectPositionTypePB.Before;
|
? null
|
||||||
|
: (builder) => builder.insertText(primaryField, title);
|
||||||
|
|
||||||
final result = await RowBackendService.createRow(
|
final result = await RowBackendService.createRow(
|
||||||
viewId: databaseController.viewId,
|
viewId: databaseController.viewId,
|
||||||
groupId: groupId,
|
groupId: groupId,
|
||||||
position: position,
|
position: position,
|
||||||
targetRowId: rowId,
|
targetRowId: targetRowId,
|
||||||
|
withCells: cellBuilder,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
final startEditing = position != OrderObjectPositionTypePB.End;
|
||||||
|
final action = PlatformExtension.isMobile
|
||||||
|
? DidCreateRowAction.openAsPage
|
||||||
|
: startEditing
|
||||||
|
? DidCreateRowAction.startEditing
|
||||||
|
: DidCreateRowAction.none;
|
||||||
|
|
||||||
result.fold(
|
result.fold(
|
||||||
(rowMeta) => emit(state.copyWith(recentAddedRowMeta: rowMeta)),
|
(rowMeta) {
|
||||||
(err) => Log.error(err),
|
state.maybeMap(
|
||||||
|
ready: (value) {
|
||||||
|
didCreateRow?.value = DidCreateRowResult(
|
||||||
|
action: action,
|
||||||
|
rowMeta: rowMeta,
|
||||||
|
groupId: groupId,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
createBottomRow: (groupId) async {
|
orElse: () {},
|
||||||
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,
|
|
||||||
);
|
);
|
||||||
|
},
|
||||||
result.fold(
|
|
||||||
(rowMeta) => emit(state.copyWith(recentAddedRowMeta: rowMeta)),
|
|
||||||
(err) => Log.error(err),
|
(err) => Log.error(err),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@ -127,47 +141,41 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
|
|||||||
final result = await groupBackendSvc.deleteGroup(groupId: groupId);
|
final result = await groupBackendSvc.deleteGroup(groupId: groupId);
|
||||||
result.fold((_) {}, (err) => Log.error(err));
|
result.fold((_) {}, (err) => Log.error(err));
|
||||||
},
|
},
|
||||||
didCreateRow: (group, row, int? index) {
|
didReceiveError: (error) {
|
||||||
emit(
|
emit(BoardState.error(error: error));
|
||||||
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));
|
|
||||||
},
|
},
|
||||||
didReceiveGroups: (List<GroupPB> groups) {
|
didReceiveGroups: (List<GroupPB> groups) {
|
||||||
final hiddenGroups = _filterHiddenGroups(hideUngrouped, groups);
|
state.maybeMap(
|
||||||
|
ready: (state) {
|
||||||
emit(
|
emit(
|
||||||
state.copyWith(
|
state.copyWith(
|
||||||
hiddenGroups: hiddenGroups,
|
hiddenGroups: _filterHiddenGroups(hideUngrouped, groups),
|
||||||
groupIds: groups.map((group) => group.groupId).toList(),
|
groupIds: groups.map((group) => group.groupId).toList(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
orElse: () {},
|
||||||
|
);
|
||||||
|
},
|
||||||
didUpdateLayoutSettings: (layoutSettings) {
|
didUpdateLayoutSettings: (layoutSettings) {
|
||||||
final hiddenGroups = _filterHiddenGroups(hideUngrouped, groupList);
|
state.maybeMap(
|
||||||
|
ready: (state) {
|
||||||
emit(
|
emit(
|
||||||
state.copyWith(
|
state.copyWith(
|
||||||
layoutSettings: layoutSettings,
|
layoutSettings: layoutSettings,
|
||||||
hiddenGroups: hiddenGroups,
|
hiddenGroups: _filterHiddenGroups(hideUngrouped, groupList),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
toggleGroupVisibility: (GroupPB group, bool isVisible) async {
|
orElse: () {},
|
||||||
await _toggleGroupVisibility(group, isVisible);
|
);
|
||||||
|
},
|
||||||
|
setGroupVisibility: (GroupPB group, bool isVisible) async {
|
||||||
|
await _setGroupVisibility(group, isVisible);
|
||||||
},
|
},
|
||||||
toggleHiddenSectionVisibility: (isVisible) async {
|
toggleHiddenSectionVisibility: (isVisible) async {
|
||||||
|
await state.maybeMap(
|
||||||
|
ready: (state) async {
|
||||||
final newLayoutSettings = state.layoutSettings!;
|
final newLayoutSettings = state.layoutSettings!;
|
||||||
newLayoutSettings.freeze();
|
newLayoutSettings.freeze();
|
||||||
|
|
||||||
@ -179,37 +187,16 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
|
|||||||
boardLayoutSetting: newLayoutSetting,
|
boardLayoutSetting: newLayoutSetting,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
orElse: () {},
|
||||||
|
);
|
||||||
|
},
|
||||||
reorderGroup: (fromGroupId, toGroupId) async {
|
reorderGroup: (fromGroupId, toGroupId) async {
|
||||||
_reorderGroup(fromGroupId, toGroupId, emit);
|
_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) {
|
startEditingHeader: (String groupId) {
|
||||||
emit(
|
state.maybeMap(
|
||||||
state.copyWith(isEditingHeader: true, editingHeaderId: groupId),
|
ready: (state) => emit(state.copyWith(editingHeaderId: groupId)),
|
||||||
|
orElse: () {},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
endEditingHeader: (String groupId, String? groupName) async {
|
endEditingHeader: (String groupId, String? groupName) async {
|
||||||
@ -218,24 +205,59 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
|
|||||||
groupId: groupId,
|
groupId: groupId,
|
||||||
name: groupName,
|
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) {
|
Future<void> _setGroupVisibility(GroupPB group, bool isVisible) async {
|
||||||
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 {
|
|
||||||
if (group.isDefault) {
|
if (group.isDefault) {
|
||||||
|
await state.maybeMap(
|
||||||
|
ready: (state) async {
|
||||||
final newLayoutSettings = state.layoutSettings!;
|
final newLayoutSettings = state.layoutSettings!;
|
||||||
newLayoutSettings.freeze();
|
newLayoutSettings.freeze();
|
||||||
|
|
||||||
@ -243,17 +265,20 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
|
|||||||
(message) => message.hideUngroupedColumn = !isVisible,
|
(message) => message.hideUngroupedColumn = !isVisible,
|
||||||
);
|
);
|
||||||
|
|
||||||
return databaseController.updateLayoutSetting(
|
await databaseController.updateLayoutSetting(
|
||||||
boardLayoutSetting: newLayoutSetting,
|
boardLayoutSetting: newLayoutSetting,
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
|
orElse: () {},
|
||||||
|
);
|
||||||
|
} else {
|
||||||
await groupBackendSvc.updateGroup(
|
await groupBackendSvc.updateGroup(
|
||||||
fieldId: groupControllers.values.first.group.fieldId,
|
fieldId: groupControllers.values.first.group.fieldId,
|
||||||
groupId: group.groupId,
|
groupId: group.groupId,
|
||||||
visible: isVisible,
|
visible: isVisible,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void _reorderGroup(
|
void _reorderGroup(
|
||||||
String fromGroupId,
|
String fromGroupId,
|
||||||
@ -277,6 +302,7 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
|
|||||||
for (final controller in groupControllers.values) {
|
for (final controller in groupControllers.values) {
|
||||||
await controller.dispose();
|
await controller.dispose();
|
||||||
}
|
}
|
||||||
|
boardController.dispose();
|
||||||
return super.close();
|
return super.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -284,11 +310,13 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
|
|||||||
databaseController.databaseLayoutSetting?.board.hideUngroupedColumn ??
|
databaseController.databaseLayoutSetting?.board.hideUngroupedColumn ??
|
||||||
false;
|
false;
|
||||||
|
|
||||||
FieldType get groupingFieldType {
|
FieldType? get groupingFieldType {
|
||||||
final fieldInfo =
|
if (groupList.isEmpty) {
|
||||||
databaseController.fieldController.getField(groupList.first.fieldId)!;
|
return null;
|
||||||
|
}
|
||||||
return fieldInfo.fieldType;
|
return databaseController.fieldController
|
||||||
|
.getField(groupList.first.fieldId)
|
||||||
|
?.fieldType;
|
||||||
}
|
}
|
||||||
|
|
||||||
void initializeGroups(List<GroupPB> groups) {
|
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() {
|
void _startListening() {
|
||||||
final onDatabaseChanged = DatabaseCallbacks(
|
|
||||||
onDatabaseChanged: (database) {
|
|
||||||
if (!isClosed) {
|
|
||||||
add(BoardEvent.didReceiveGridUpdate(database));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
final onLayoutSettingsChanged = DatabaseLayoutSettingCallbacks(
|
final onLayoutSettingsChanged = DatabaseLayoutSettingCallbacks(
|
||||||
onLayoutSettingsChanged: (layoutSettings) {
|
onLayoutSettingsChanged: (layoutSettings) {
|
||||||
if (isClosed) {
|
if (isClosed) {
|
||||||
@ -433,7 +454,6 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
|
|||||||
);
|
);
|
||||||
|
|
||||||
databaseController.addListener(
|
databaseController.addListener(
|
||||||
onDatabaseChanged: onDatabaseChanged,
|
|
||||||
onLayoutSettingsChanged: onLayoutSettingsChanged,
|
onLayoutSettingsChanged: onLayoutSettingsChanged,
|
||||||
onGroupChanged: onGroupChanged,
|
onGroupChanged: onGroupChanged,
|
||||||
);
|
);
|
||||||
@ -451,22 +471,10 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
|
|||||||
return <AppFlowyGroupItem>[...items];
|
return <AppFlowyGroupItem>[...items];
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _openGrid(Emitter<BoardState> emit) async {
|
Future<void> _openDatabase(Emitter<BoardState> emit) {
|
||||||
final result = await databaseController.open();
|
return databaseController.open().fold(
|
||||||
result.fold(
|
(datbasePB) => databaseController.setIsLoading(false),
|
||||||
(grid) {
|
(err) => emit(BoardState.error(error: err)),
|
||||||
databaseController.setIsLoading(false);
|
|
||||||
emit(
|
|
||||||
state.copyWith(
|
|
||||||
loadingState: LoadingState.finish(FlowyResult.success(null)),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
(err) => emit(
|
|
||||||
state.copyWith(
|
|
||||||
loadingState: LoadingState.finish(FlowyResult.failure(err)),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -474,8 +482,7 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
|
|||||||
final delegate = GroupControllerDelegateImpl(
|
final delegate = GroupControllerDelegateImpl(
|
||||||
controller: boardController,
|
controller: boardController,
|
||||||
fieldController: fieldController,
|
fieldController: fieldController,
|
||||||
onNewColumnItem: (groupId, row, index) =>
|
onNewColumnItem: (groupId, row, index) {},
|
||||||
add(BoardEvent.didCreateRow(group, row, index)),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
final controller = GroupController(
|
final controller = GroupController(
|
||||||
@ -579,71 +586,77 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
|
|||||||
@freezed
|
@freezed
|
||||||
class BoardEvent with _$BoardEvent {
|
class BoardEvent with _$BoardEvent {
|
||||||
const factory BoardEvent.initial() = _InitialBoard;
|
const factory BoardEvent.initial() = _InitialBoard;
|
||||||
const factory BoardEvent.createBottomRow(String groupId) = _CreateBottomRow;
|
const factory BoardEvent.createRow(
|
||||||
const factory BoardEvent.createHeaderRow(String groupId) = _CreateHeaderRow;
|
String groupId,
|
||||||
|
OrderObjectPositionTypePB position,
|
||||||
|
String? title,
|
||||||
|
String? targetRowId,
|
||||||
|
) = _CreateRow;
|
||||||
const factory BoardEvent.createGroup(String name) = _CreateGroup;
|
const factory BoardEvent.createGroup(String name) = _CreateGroup;
|
||||||
const factory BoardEvent.startEditingHeader(String groupId) =
|
const factory BoardEvent.startEditingHeader(String groupId) =
|
||||||
_StartEditingHeader;
|
_StartEditingHeader;
|
||||||
const factory BoardEvent.endEditingHeader(String groupId, String? groupName) =
|
const factory BoardEvent.endEditingHeader(String groupId, String? groupName) =
|
||||||
_EndEditingHeader;
|
_EndEditingHeader;
|
||||||
const factory BoardEvent.didCreateRow(
|
const factory BoardEvent.setGroupVisibility(
|
||||||
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(
|
|
||||||
GroupPB group,
|
GroupPB group,
|
||||||
bool isVisible,
|
bool isVisible,
|
||||||
) = _ToggleGroupVisibility;
|
) = _SetGroupVisibility;
|
||||||
const factory BoardEvent.toggleHiddenSectionVisibility(bool isVisible) =
|
const factory BoardEvent.toggleHiddenSectionVisibility(bool isVisible) =
|
||||||
_ToggleHiddenSectionVisibility;
|
_ToggleHiddenSectionVisibility;
|
||||||
const factory BoardEvent.deleteGroup(String groupId) = _DeleteGroup;
|
const factory BoardEvent.deleteGroup(String groupId) = _DeleteGroup;
|
||||||
const factory BoardEvent.reorderGroup(String fromGroupId, String toGroupId) =
|
const factory BoardEvent.reorderGroup(String fromGroupId, String toGroupId) =
|
||||||
_ReorderGroup;
|
_ReorderGroup;
|
||||||
const factory BoardEvent.didReceiveError(FlowyError error) = _DidReceiveError;
|
const factory BoardEvent.didReceiveError(FlowyError error) = _DidReceiveError;
|
||||||
const factory BoardEvent.didReceiveGridUpdate(
|
|
||||||
DatabasePB grid,
|
|
||||||
) = _DidReceiveGridUpdate;
|
|
||||||
const factory BoardEvent.didReceiveGroups(List<GroupPB> groups) =
|
const factory BoardEvent.didReceiveGroups(List<GroupPB> groups) =
|
||||||
_DidReceiveGroups;
|
_DidReceiveGroups;
|
||||||
const factory BoardEvent.didUpdateLayoutSettings(
|
const factory BoardEvent.didUpdateLayoutSettings(
|
||||||
BoardLayoutSettingPB layoutSettings,
|
BoardLayoutSettingPB layoutSettings,
|
||||||
) = _DidUpdateLayoutSettings;
|
) = _DidUpdateLayoutSettings;
|
||||||
|
const factory BoardEvent.deleteCards(List<GroupedRowId> groupedRowIds) =
|
||||||
|
_DeleteCards;
|
||||||
|
const factory BoardEvent.moveGroupToAdjacentGroup(
|
||||||
|
GroupedRowId groupedRowId,
|
||||||
|
bool toPrevious,
|
||||||
|
) = _MoveGroupToAdjacentGroup;
|
||||||
}
|
}
|
||||||
|
|
||||||
@freezed
|
@freezed
|
||||||
class BoardState with _$BoardState {
|
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 String viewId,
|
||||||
required DatabasePB? grid,
|
|
||||||
required List<String> groupIds,
|
required List<String> groupIds,
|
||||||
required bool isEditingHeader,
|
|
||||||
required bool isEditingRow,
|
|
||||||
required LoadingState loadingState,
|
required LoadingState loadingState,
|
||||||
required FlowyError? noneOrError,
|
required FlowyError? noneOrError,
|
||||||
required BoardLayoutSettingPB? layoutSettings,
|
required BoardLayoutSettingPB? layoutSettings,
|
||||||
String? editingHeaderId,
|
|
||||||
BoardEditingRow? editingRow,
|
|
||||||
RowMetaPB? recentAddedRowMeta,
|
|
||||||
required List<GroupPB> hiddenGroups,
|
required List<GroupPB> hiddenGroups,
|
||||||
}) = _BoardState;
|
String? editingHeaderId,
|
||||||
|
}) = _BoardReadyState;
|
||||||
|
|
||||||
factory BoardState.initial(String viewId) => BoardState(
|
const factory BoardState.setFocus({
|
||||||
grid: null,
|
required List<GroupedRowId> groupedRowIds,
|
||||||
|
}) = _BoardSetFocusState;
|
||||||
|
|
||||||
|
factory BoardState.initial(String viewId) => BoardState.ready(
|
||||||
viewId: viewId,
|
viewId: viewId,
|
||||||
groupIds: [],
|
groupIds: [],
|
||||||
isEditingHeader: false,
|
|
||||||
isEditingRow: false,
|
|
||||||
noneOrError: null,
|
noneOrError: null,
|
||||||
loadingState: const LoadingState.loading(),
|
loadingState: const LoadingState.loading(),
|
||||||
layoutSettings: null,
|
layoutSettings: null,
|
||||||
hiddenGroups: [],
|
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) {
|
List<GroupPB> _filterHiddenGroups(bool hideUngrouped, List<GroupPB> groups) {
|
||||||
@ -658,7 +671,7 @@ class GroupItem extends AppFlowyGroupItem {
|
|||||||
required this.fieldInfo,
|
required this.fieldInfo,
|
||||||
bool draggable = true,
|
bool draggable = true,
|
||||||
}) {
|
}) {
|
||||||
super.draggable = draggable;
|
super.draggable.value = draggable;
|
||||||
}
|
}
|
||||||
|
|
||||||
final RowMetaPB row;
|
final RowMetaPB row;
|
||||||
@ -668,6 +681,23 @@ class GroupItem extends AppFlowyGroupItem {
|
|||||||
String get id => row.id.toString();
|
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 {
|
class GroupControllerDelegateImpl extends GroupControllerDelegate {
|
||||||
GroupControllerDelegateImpl({
|
GroupControllerDelegateImpl({
|
||||||
required this.controller,
|
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 {
|
class GroupData {
|
||||||
GroupData({
|
GroupData({
|
||||||
required this.group,
|
required this.group,
|
||||||
@ -779,3 +797,21 @@ class CheckboxGroup {
|
|||||||
// pub const CHECK: &str = "Yes";
|
// pub const CHECK: &str = "Yes";
|
||||||
bool get isCheck => group.groupId == "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/flowy_svgs.g.dart';
|
||||||
import 'package:appflowy/generated/locale_keys.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/database/board/mobile_board_page.dart';
|
||||||
import 'package:appflowy/mobile/presentation/widgets/flowy_mobile_state_container.dart';
|
|
||||||
import 'package:appflowy/plugins/database/application/database_controller.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/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/board/presentation/widgets/board_column_header.dart';
|
||||||
import 'package:appflowy/plugins/database/grid/presentation/grid_page.dart';
|
import 'package:appflowy/plugins/database/grid/presentation/grid_page.dart';
|
||||||
import 'package:appflowy/plugins/database/grid/presentation/widgets/header/field_type_extension.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/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/cell/card_cell_style_maps/desktop_board_card_cell_style.dart';
|
||||||
import 'package:appflowy/plugins/database/widgets/row/row_detail.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_backend/protobuf/flowy-folder/view.pb.dart';
|
||||||
import 'package:appflowy_board/appflowy_board.dart';
|
import 'package:appflowy_board/appflowy_board.dart';
|
||||||
import 'package:appflowy_editor/appflowy_editor.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/services.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
import '../../../../workspace/application/view/view_bloc.dart';
|
|
||||||
import '../../widgets/card/card.dart';
|
import '../../widgets/card/card.dart';
|
||||||
import '../../widgets/cell/card_cell_builder.dart';
|
import '../../widgets/cell/card_cell_builder.dart';
|
||||||
import '../application/board_bloc.dart';
|
import '../application/board_bloc.dart';
|
||||||
import 'toolbar/board_setting_bar.dart';
|
import 'toolbar/board_setting_bar.dart';
|
||||||
|
import 'widgets/board_focus_scope.dart';
|
||||||
import 'widgets/board_hidden_groups.dart';
|
import 'widgets/board_hidden_groups.dart';
|
||||||
|
import 'widgets/board_shortcut_container.dart';
|
||||||
|
|
||||||
class BoardPageTabBarBuilderImpl extends DatabaseTabBarItemBuilder {
|
class BoardPageTabBarBuilderImpl extends DatabaseTabBarItemBuilder {
|
||||||
final _toggleExtension = ToggleExtensionNotifier();
|
final _toggleExtension = ToggleExtensionNotifier();
|
||||||
@ -46,7 +47,17 @@ class BoardPageTabBarBuilderImpl extends DatabaseTabBarItemBuilder {
|
|||||||
bool shrinkWrap,
|
bool shrinkWrap,
|
||||||
String? initialRowId,
|
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
|
@override
|
||||||
Widget settingBar(BuildContext context, DatabaseController controller) =>
|
Widget settingBar(BuildContext context, DatabaseController controller) =>
|
||||||
@ -79,12 +90,13 @@ class BoardPageTabBarBuilderImpl extends DatabaseTabBarItemBuilder {
|
|||||||
ValueKey(controller.viewId);
|
ValueKey(controller.viewId);
|
||||||
}
|
}
|
||||||
|
|
||||||
class BoardPage extends StatelessWidget {
|
class DesktopBoardPage extends StatefulWidget {
|
||||||
BoardPage({
|
const DesktopBoardPage({
|
||||||
|
super.key,
|
||||||
required this.view,
|
required this.view,
|
||||||
required this.databaseController,
|
required this.databaseController,
|
||||||
this.onEditStateChanged,
|
this.onEditStateChanged,
|
||||||
}) : super(key: ValueKey(view.id));
|
});
|
||||||
|
|
||||||
final ViewPB view;
|
final ViewPB view;
|
||||||
|
|
||||||
@ -93,54 +105,154 @@ class BoardPage extends StatelessWidget {
|
|||||||
/// Called when edit state changed
|
/// Called when edit state changed
|
||||||
final VoidCallback? onEditStateChanged;
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return BlocProvider<BoardBloc>(
|
return MultiBlocProvider(
|
||||||
create: (context) => BoardBloc(
|
providers: [
|
||||||
view: view,
|
BlocProvider<BoardBloc>.value(
|
||||||
databaseController: databaseController,
|
value: _boardBloc,
|
||||||
)..add(const BoardEvent.initial()),
|
),
|
||||||
|
BlocProvider.value(
|
||||||
|
value: _boardActionsCubit,
|
||||||
|
),
|
||||||
|
],
|
||||||
child: BlocBuilder<BoardBloc, BoardState>(
|
child: BlocBuilder<BoardBloc, BoardState>(
|
||||||
buildWhen: (p, c) => p.loadingState != c.loadingState,
|
builder: (context, state) => state.maybeMap(
|
||||||
builder: (context, state) => state.loadingState.when(
|
loading: (_) => const Center(
|
||||||
loading: () => const Center(
|
|
||||||
child: CircularProgressIndicator.adaptive(),
|
child: CircularProgressIndicator.adaptive(),
|
||||||
),
|
),
|
||||||
idle: () => const SizedBox.shrink(),
|
error: (err) => FlowyErrorPage.message(
|
||||||
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(),
|
err.toString(),
|
||||||
howToFix: LocaleKeys.errorDialog_howToFixFallback.tr(),
|
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 {
|
class _BoardContent extends StatefulWidget {
|
||||||
const DesktopBoardContent({
|
const _BoardContent({
|
||||||
super.key,
|
required this.boardController,
|
||||||
|
required this.focusScope,
|
||||||
this.onEditStateChanged,
|
this.onEditStateChanged,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
final AppFlowyBoardController boardController;
|
||||||
|
final BoardFocusScope focusScope;
|
||||||
final VoidCallback? onEditStateChanged;
|
final VoidCallback? onEditStateChanged;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<DesktopBoardContent> createState() => _DesktopBoardContentState();
|
State<_BoardContent> createState() => _BoardContentState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _DesktopBoardContentState extends State<DesktopBoardContent> {
|
class _BoardContentState extends State<_BoardContent> {
|
||||||
final ScrollController scrollController = ScrollController();
|
final ScrollController scrollController = ScrollController();
|
||||||
final AppFlowyBoardScrollController scrollManager =
|
final AppFlowyBoardScrollController scrollManager =
|
||||||
AppFlowyBoardScrollController();
|
AppFlowyBoardScrollController();
|
||||||
@ -148,16 +260,19 @@ class _DesktopBoardContentState extends State<DesktopBoardContent> {
|
|||||||
final config = const AppFlowyBoardConfig(
|
final config = const AppFlowyBoardConfig(
|
||||||
groupMargin: EdgeInsets.symmetric(horizontal: 4),
|
groupMargin: EdgeInsets.symmetric(horizontal: 4),
|
||||||
groupBodyPadding: 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),
|
groupHeaderPadding: EdgeInsets.symmetric(horizontal: 8),
|
||||||
cardMargin: EdgeInsets.symmetric(horizontal: 4, vertical: 3),
|
cardMargin: EdgeInsets.symmetric(horizontal: 4, vertical: 3),
|
||||||
stretchGroupHeight: false,
|
stretchGroupHeight: false,
|
||||||
);
|
);
|
||||||
|
|
||||||
late final cellBuilder = CardCellBuilder(
|
late final cellBuilder = CardCellBuilder(
|
||||||
databaseController: context.read<BoardBloc>().databaseController,
|
databaseController: databaseController,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
DatabaseController get databaseController =>
|
||||||
|
context.read<BoardBloc>().databaseController;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
scrollController.dispose();
|
scrollController.dispose();
|
||||||
@ -166,15 +281,49 @@ class _DesktopBoardContentState extends State<DesktopBoardContent> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return BlocListener<BoardBloc, BoardState>(
|
return MultiBlocListener(
|
||||||
|
listeners: [
|
||||||
|
BlocListener<BoardBloc, BoardState>(
|
||||||
listener: (context, state) {
|
listener: (context, state) {
|
||||||
|
state.maybeMap(
|
||||||
|
ready: (value) {
|
||||||
widget.onEditStateChanged?.call();
|
widget.onEditStateChanged?.call();
|
||||||
},
|
},
|
||||||
child: BlocBuilder<BoardBloc, BoardState>(
|
orElse: () {},
|
||||||
builder: (context, state) {
|
);
|
||||||
final showCreateGroupButton =
|
},
|
||||||
context.read<BoardBloc>().groupingFieldType.canCreateNewGroup;
|
),
|
||||||
return Padding(
|
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),
|
padding: const EdgeInsets.only(top: 8.0),
|
||||||
child: AppFlowyBoard(
|
child: AppFlowyBoard(
|
||||||
boardScrollController: scrollManager,
|
boardScrollController: scrollManager,
|
||||||
@ -183,7 +332,11 @@ class _DesktopBoardContentState extends State<DesktopBoardContent> {
|
|||||||
groupConstraints: const BoxConstraints.tightFor(width: 256),
|
groupConstraints: const BoxConstraints.tightFor(width: 256),
|
||||||
config: config,
|
config: config,
|
||||||
leading: HiddenGroupsColumn(margin: config.groupHeaderPadding),
|
leading: HiddenGroupsColumn(margin: config.groupHeaderPadding),
|
||||||
trailing: showCreateGroupButton
|
trailing: context
|
||||||
|
.read<BoardBloc>()
|
||||||
|
.groupingFieldType
|
||||||
|
?.canCreateNewGroup ??
|
||||||
|
false
|
||||||
? BoardTrailing(scrollController: scrollController)
|
? BoardTrailing(scrollController: scrollController)
|
||||||
: const HSpace(40),
|
: const HSpace(40),
|
||||||
headerBuilder: (_, groupData) => BlocProvider<BoardBloc>.value(
|
headerBuilder: (_, groupData) => BlocProvider<BoardBloc>.value(
|
||||||
@ -193,110 +346,367 @@ class _DesktopBoardContentState extends State<DesktopBoardContent> {
|
|||||||
margin: config.groupHeaderPadding,
|
margin: config.groupHeaderPadding,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
footerBuilder: _buildFooter,
|
footerBuilder: (_, groupData) => MultiBlocProvider(
|
||||||
cardBuilder: (_, column, columnItem) => _buildCard(
|
providers: [
|
||||||
context,
|
BlocProvider.value(
|
||||||
column,
|
value: context.read<BoardBloc>(),
|
||||||
columnItem,
|
),
|
||||||
|
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) {
|
Widget _startCreatingCardsButton() {
|
||||||
return Padding(
|
return BlocListener<BoardActionsCubit, BoardActionsState>(
|
||||||
padding: config.groupFooterPadding,
|
listener: (context, state) {
|
||||||
|
state.maybeWhen(
|
||||||
|
startCreateBottomRow: (groupId) {
|
||||||
|
if (groupId == widget.columnData.id) {
|
||||||
|
setState(() => _isCreating = true);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
orElse: () {},
|
||||||
|
);
|
||||||
|
},
|
||||||
child: FlowyTooltip(
|
child: FlowyTooltip(
|
||||||
message: LocaleKeys.board_column_addToColumnBottomTooltip.tr(),
|
message: LocaleKeys.board_column_addToColumnBottomTooltip.tr(),
|
||||||
child: FlowyHover(
|
child: SizedBox(
|
||||||
child: AppFlowyGroupFooter(
|
|
||||||
height: 36,
|
height: 36,
|
||||||
icon: FlowySvg(
|
child: FlowyButton(
|
||||||
|
leftIcon: FlowySvg(
|
||||||
FlowySvgs.add_s,
|
FlowySvgs.add_s,
|
||||||
color: Theme.of(context).hintColor,
|
color: Theme.of(context).hintColor,
|
||||||
),
|
),
|
||||||
title: FlowyText.medium(
|
text: FlowyText.medium(
|
||||||
LocaleKeys.board_column_createNewCard.tr(),
|
LocaleKeys.board_column_createNewCard.tr(),
|
||||||
color: Theme.of(context).hintColor,
|
color: Theme.of(context).hintColor,
|
||||||
),
|
),
|
||||||
onAddButtonClick: () => context
|
onTap: () {
|
||||||
.read<BoardBloc>()
|
setState(() => _isCreating = true);
|
||||||
.add(BoardEvent.createBottomRow(columnData.id)),
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Widget _buildCard(
|
class _BoardCard extends StatefulWidget {
|
||||||
BuildContext context,
|
const _BoardCard({
|
||||||
AppFlowyGroupData afGroupData,
|
required this.afGroupData,
|
||||||
AppFlowyGroupItem afGroupItem,
|
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 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.
|
final groupData = widget.afGroupData.customData as GroupData;
|
||||||
if (rowCache == null) {
|
final rowCache = boardBloc.rowCache;
|
||||||
return SizedBox.shrink(key: ObjectKey(groupItem));
|
|
||||||
}
|
|
||||||
|
|
||||||
final databaseController = boardBloc.databaseController;
|
final databaseController = boardBloc.databaseController;
|
||||||
final viewId = boardBloc.viewId;
|
final rowMeta =
|
||||||
|
rowCache.getRow(widget.groupItem.id)?.rowMeta ?? widget.groupItem.row;
|
||||||
|
|
||||||
final isEditing = boardBloc.state.isEditingRow &&
|
const nada = DoNothingAndStopPropagationIntent();
|
||||||
boardBloc.state.editingRow?.row.id == groupItem.row.id;
|
|
||||||
|
|
||||||
final groupItemId = "${groupData.group.groupId}${groupItem.row.id}";
|
return BlocListener<BoardActionsCubit, BoardActionsState>(
|
||||||
final rowMeta = rowInfo?.rowMeta ?? groupItem.row;
|
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(
|
return previousContainsFocus != currentContainsFocus;
|
||||||
key: ValueKey(groupItemId),
|
},
|
||||||
margin: config.cardMargin,
|
builder: (context, focusedItems, child) => Container(
|
||||||
decoration: _makeBoxDecoration(context),
|
margin: widget.boardConfig.cardMargin,
|
||||||
|
decoration: _makeBoxDecoration(
|
||||||
|
context,
|
||||||
|
groupData.group.groupId,
|
||||||
|
widget.groupItem.id,
|
||||||
|
),
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
child: RowCard(
|
child: RowCard(
|
||||||
fieldController: databaseController.fieldController,
|
fieldController: databaseController.fieldController,
|
||||||
rowMeta: rowMeta,
|
rowMeta: rowMeta,
|
||||||
viewId: viewId,
|
viewId: boardBloc.viewId,
|
||||||
rowCache: rowCache,
|
rowCache: rowCache,
|
||||||
groupingFieldId: groupItem.fieldInfo.id,
|
groupingFieldId: widget.groupItem.fieldInfo.id,
|
||||||
isEditing: isEditing,
|
isEditing: _isEditing,
|
||||||
cellBuilder: cellBuilder,
|
cellBuilder: widget.cellBuilder,
|
||||||
openCard: (context) => _openCard(
|
onTap: (context) => _openCard(
|
||||||
context: context,
|
context: context,
|
||||||
databaseController: databaseController,
|
databaseController: databaseController,
|
||||||
groupId: groupData.group.groupId,
|
|
||||||
rowMeta: context.read<CardBloc>().state.rowMeta,
|
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(
|
styleConfiguration: RowCardStyleConfiguration(
|
||||||
cellStyleMap: desktopBoardCardCellStyleMap(context),
|
cellStyleMap: desktopBoardCardCellStyleMap(context),
|
||||||
hoverStyle: HoverStyle(
|
hoverStyle: HoverStyle(
|
||||||
hoverColor: Theme.of(context).brightness == Brightness.light
|
hoverColor: Theme.of(context).brightness == Brightness.light
|
||||||
? const Color(0x0F1F2329)
|
? const Color(0x0F1F2329)
|
||||||
: const Color(0x0FEFF4FB),
|
: const Color(0x0FEFF4FB),
|
||||||
foregroundColorOnHover: Theme.of(context).colorScheme.onBackground,
|
foregroundColorOnHover:
|
||||||
|
Theme.of(context).colorScheme.onBackground,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
onStartEditing: () =>
|
onStartEditing: () =>
|
||||||
boardBloc.add(BoardEvent.startEditingRow(groupData.group, rowMeta)),
|
context.read<BoardActionsCubit>().startEditingRow(
|
||||||
onEndEditing: () => boardBloc.add(BoardEvent.endEditingRow(rowMeta.id)),
|
GroupedRowId(
|
||||||
|
groupId: groupData.group.groupId,
|
||||||
|
rowId: rowMeta.id,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onEndEditing: () => context.read<BoardActionsCubit>().endEditing(
|
||||||
|
GroupedRowId(
|
||||||
|
groupId: groupData.group.groupId,
|
||||||
|
rowId: rowMeta.id,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
BoxDecoration _makeBoxDecoration(BuildContext context) {
|
BoxDecoration _makeBoxDecoration(
|
||||||
|
BuildContext context,
|
||||||
|
String groupId,
|
||||||
|
String rowId,
|
||||||
|
) {
|
||||||
return BoxDecoration(
|
return BoxDecoration(
|
||||||
color: Theme.of(context).colorScheme.surface,
|
color: Theme.of(context).colorScheme.surface,
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(6)),
|
borderRadius: const BorderRadius.all(Radius.circular(6)),
|
||||||
border: Border.fromBorderSide(
|
border: Border.fromBorderSide(
|
||||||
BorderSide(
|
BorderSide(
|
||||||
color: Theme.of(context).brightness == Brightness.light
|
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(0xFF1F2329).withOpacity(0.12)
|
||||||
: const Color(0xFF59647A),
|
: const Color(0xFF59647A),
|
||||||
),
|
),
|
||||||
@ -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 {
|
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/layout/sizes.dart';
|
||||||
import 'package:appflowy/plugins/database/grid/presentation/widgets/header/field_type_extension.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/workspace/presentation/widgets/dialogs.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';
|
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-database2/group.pb.dart';
|
|
||||||
import 'package:appflowy_board/appflowy_board.dart';
|
import 'package:appflowy_board/appflowy_board.dart';
|
||||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
@ -65,7 +64,10 @@ class _BoardColumnHeaderState extends State<BoardColumnHeader> {
|
|||||||
|
|
||||||
return BlocBuilder<BoardBloc, BoardState>(
|
return BlocBuilder<BoardBloc, BoardState>(
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
if (state.isEditingHeader) {
|
return state.maybeMap(
|
||||||
|
orElse: () => const SizedBox.shrink(),
|
||||||
|
ready: (state) {
|
||||||
|
if (state.editingHeaderId != null) {
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
_focusNode.requestFocus();
|
_focusNode.requestFocus();
|
||||||
});
|
});
|
||||||
@ -87,9 +89,9 @@ class _BoardColumnHeaderState extends State<BoardColumnHeader> {
|
|||||||
child: MouseRegion(
|
child: MouseRegion(
|
||||||
cursor: SystemMouseCursors.click,
|
cursor: SystemMouseCursors.click,
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
onTap: () => context
|
onTap: () => context.read<BoardBloc>().add(
|
||||||
.read<BoardBloc>()
|
BoardEvent.startEditingHeader(widget.groupData.id),
|
||||||
.add(BoardEvent.startEditingHeader(widget.groupData.id)),
|
),
|
||||||
child: FlowyText.medium(
|
child: FlowyText.medium(
|
||||||
widget.groupData.headerData.groupName,
|
widget.groupData.headerData.groupName,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
@ -100,8 +102,7 @@ class _BoardColumnHeaderState extends State<BoardColumnHeader> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.isEditingHeader &&
|
if (state.editingHeaderId == widget.groupData.id) {
|
||||||
state.editingHeaderId == widget.groupData.id) {
|
|
||||||
title = _buildTextField(context);
|
title = _buildTextField(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -117,15 +118,22 @@ class _BoardColumnHeaderState extends State<BoardColumnHeader> {
|
|||||||
_groupOptionsButton(context),
|
_groupOptionsButton(context),
|
||||||
const HSpace(4),
|
const HSpace(4),
|
||||||
FlowyTooltip(
|
FlowyTooltip(
|
||||||
message: LocaleKeys.board_column_addToColumnTopTooltip.tr(),
|
message:
|
||||||
|
LocaleKeys.board_column_addToColumnTopTooltip.tr(),
|
||||||
preferBelow: false,
|
preferBelow: false,
|
||||||
child: FlowyIconButton(
|
child: FlowyIconButton(
|
||||||
width: 20,
|
width: 20,
|
||||||
icon: const FlowySvg(FlowySvgs.add_s),
|
icon: const FlowySvg(FlowySvgs.add_s),
|
||||||
iconColorOnHover: Theme.of(context).colorScheme.onSurface,
|
iconColorOnHover:
|
||||||
onPressed: () => context
|
Theme.of(context).colorScheme.onSurface,
|
||||||
.read<BoardBloc>()
|
onPressed: () => context.read<BoardBloc>().add(
|
||||||
.add(BoardEvent.createHeaderRow(widget.groupData.id)),
|
BoardEvent.createRow(
|
||||||
|
widget.groupData.id,
|
||||||
|
OrderObjectPositionTypePB.Start,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -134,6 +142,8 @@ class _BoardColumnHeaderState extends State<BoardColumnHeader> {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildTextField(BuildContext context) {
|
Widget _buildTextField(BuildContext context) {
|
||||||
@ -257,7 +267,7 @@ enum GroupOptions {
|
|||||||
case hide:
|
case hide:
|
||||||
context
|
context
|
||||||
.read<BoardBloc>()
|
.read<BoardBloc>()
|
||||||
.add(BoardEvent.toggleGroupVisibility(group, false));
|
.add(BoardEvent.setGroupVisibility(group, false));
|
||||||
break;
|
break;
|
||||||
case delete:
|
case delete:
|
||||||
NavigatorAlertDialog(
|
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_builder.dart';
|
||||||
import 'package:appflowy/plugins/database/widgets/cell/card_cell_skeleton/text_card_cell.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/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_backend/protobuf/flowy-database2/protobuf.dart';
|
||||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
@ -23,7 +22,10 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
class HiddenGroupsColumn extends StatelessWidget {
|
class HiddenGroupsColumn extends StatelessWidget {
|
||||||
const HiddenGroupsColumn({super.key, required this.margin});
|
const HiddenGroupsColumn({
|
||||||
|
super.key,
|
||||||
|
required this.margin,
|
||||||
|
});
|
||||||
|
|
||||||
final EdgeInsets margin;
|
final EdgeInsets margin;
|
||||||
|
|
||||||
@ -31,7 +33,10 @@ class HiddenGroupsColumn extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final databaseController = context.read<BoardBloc>().databaseController;
|
final databaseController = context.read<BoardBloc>().databaseController;
|
||||||
return BlocSelector<BoardBloc, BoardState, BoardLayoutSettingPB?>(
|
return BlocSelector<BoardBloc, BoardState, BoardLayoutSettingPB?>(
|
||||||
selector: (state) => state.layoutSettings,
|
selector: (state) => state.maybeMap(
|
||||||
|
orElse: () => null,
|
||||||
|
ready: (value) => value.layoutSettings,
|
||||||
|
),
|
||||||
builder: (context, layoutSettings) {
|
builder: (context, layoutSettings) {
|
||||||
if (layoutSettings == null) {
|
if (layoutSettings == null) {
|
||||||
return const SizedBox.shrink();
|
return const SizedBox.shrink();
|
||||||
@ -126,7 +131,10 @@ class HiddenGroupList extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return BlocBuilder<BoardBloc, BoardState>(
|
return BlocBuilder<BoardBloc, BoardState>(
|
||||||
builder: (_, state) => ReorderableListView.builder(
|
builder: (context, state) {
|
||||||
|
return state.maybeMap(
|
||||||
|
orElse: () => const SizedBox.shrink(),
|
||||||
|
ready: (state) => ReorderableListView.builder(
|
||||||
proxyDecorator: (child, index, animation) => Material(
|
proxyDecorator: (child, index, animation) => Material(
|
||||||
color: Colors.transparent,
|
color: Colors.transparent,
|
||||||
child: Stack(
|
child: Stack(
|
||||||
@ -164,6 +172,8 @@ class HiddenGroupList extends StatelessWidget {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -248,6 +258,9 @@ class HiddenGroupButtonContent extends StatelessWidget {
|
|||||||
value: bloc,
|
value: bloc,
|
||||||
child: BlocBuilder<BoardBloc, BoardState>(
|
child: BlocBuilder<BoardBloc, BoardState>(
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
|
return state.maybeMap(
|
||||||
|
orElse: () => const SizedBox.shrink(),
|
||||||
|
ready: (state) {
|
||||||
final group = state.hiddenGroups.firstWhereOrNull(
|
final group = state.hiddenGroups.firstWhereOrNull(
|
||||||
(g) => g.groupId == groupId,
|
(g) => g.groupId == groupId,
|
||||||
);
|
);
|
||||||
@ -288,8 +301,9 @@ class HiddenGroupButtonContent extends StatelessWidget {
|
|||||||
FlowySvgs.show_m,
|
FlowySvgs.show_m,
|
||||||
color: Theme.of(context).hintColor,
|
color: Theme.of(context).hintColor,
|
||||||
),
|
),
|
||||||
onPressed: () => context.read<BoardBloc>().add(
|
onPressed: () =>
|
||||||
BoardEvent.toggleGroupVisibility(
|
context.read<BoardBloc>().add(
|
||||||
|
BoardEvent.setGroupVisibility(
|
||||||
group,
|
group,
|
||||||
true,
|
true,
|
||||||
),
|
),
|
||||||
@ -301,6 +315,8 @@ class HiddenGroupButtonContent extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@ -360,6 +376,9 @@ class HiddenGroupPopupItemList extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return BlocBuilder<BoardBloc, BoardState>(
|
return BlocBuilder<BoardBloc, BoardState>(
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
|
return state.maybeMap(
|
||||||
|
orElse: () => const SizedBox.shrink(),
|
||||||
|
ready: (state) {
|
||||||
final group = state.hiddenGroups.firstWhereOrNull(
|
final group = state.hiddenGroups.firstWhereOrNull(
|
||||||
(g) => g.groupId == groupId,
|
(g) => g.groupId == groupId,
|
||||||
);
|
);
|
||||||
@ -389,7 +408,8 @@ class HiddenGroupPopupItemList extends StatelessWidget {
|
|||||||
|
|
||||||
return HiddenGroupPopupItem(
|
return HiddenGroupPopupItem(
|
||||||
cellContext: rowCache.loadCells(item).firstWhere(
|
cellContext: rowCache.loadCells(item).firstWhere(
|
||||||
(cellContext) => cellContext.fieldId == primaryFieldId,
|
(cellContext) =>
|
||||||
|
cellContext.fieldId == primaryFieldId,
|
||||||
),
|
),
|
||||||
rowController: rowController,
|
rowController: rowController,
|
||||||
rowMeta: item,
|
rowMeta: item,
|
||||||
@ -400,12 +420,9 @@ class HiddenGroupPopupItemList extends StatelessWidget {
|
|||||||
FlowyOverlay.show(
|
FlowyOverlay.show(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (_) {
|
builder: (_) {
|
||||||
return BlocProvider.value(
|
return RowDetailPage(
|
||||||
value: context.read<ViewBloc>(),
|
|
||||||
child: RowDetailPage(
|
|
||||||
databaseController: databaseController,
|
databaseController: databaseController,
|
||||||
rowController: rowController,
|
rowController: rowController,
|
||||||
),
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@ -425,6 +442,8 @@ class HiddenGroupPopupItemList extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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 {
|
deleteEvent: (String viewId, String rowId) async {
|
||||||
final result = await RowBackendService.deleteRow(viewId, rowId);
|
final result = await RowBackendService.deleteRows(viewId, [rowId]);
|
||||||
result.fold(
|
result.fold(
|
||||||
(_) => null,
|
(_) => null,
|
||||||
(e) => Log.error('Failed to delete event: $e', e),
|
(e) => Log.error('Failed to delete event: $e', e),
|
||||||
|
@ -46,9 +46,9 @@ class CalendarEventEditorBloc
|
|||||||
emit(state.copyWith(cells: cells));
|
emit(state.copyWith(cells: cells));
|
||||||
},
|
},
|
||||||
delete: () async {
|
delete: () async {
|
||||||
final result = await RowBackendService.deleteRow(
|
final result = await RowBackendService.deleteRows(
|
||||||
rowController.viewId,
|
rowController.viewId,
|
||||||
rowController.rowId,
|
[rowController.rowId],
|
||||||
);
|
);
|
||||||
result.fold((l) => null, (err) => Log.error(err));
|
result.fold((l) => null, (err) => Log.error(err));
|
||||||
},
|
},
|
||||||
|
@ -80,7 +80,7 @@ class _EventCardState extends State<EventCard> {
|
|||||||
rowCache: rowCache,
|
rowCache: rowCache,
|
||||||
isEditing: false,
|
isEditing: false,
|
||||||
cellBuilder: cellBuilder,
|
cellBuilder: cellBuilder,
|
||||||
openCard: (context) {
|
onTap: (context) {
|
||||||
if (PlatformExtension.isMobile) {
|
if (PlatformExtension.isMobile) {
|
||||||
context.push(
|
context.push(
|
||||||
MobileRowDetailPage.routeName,
|
MobileRowDetailPage.routeName,
|
||||||
|
@ -51,7 +51,7 @@ class GridBloc extends Bloc<GridEvent, GridState> {
|
|||||||
emit(state.copyWith(createdRow: null, openRowDetail: false));
|
emit(state.copyWith(createdRow: null, openRowDetail: false));
|
||||||
},
|
},
|
||||||
deleteRow: (rowInfo) async {
|
deleteRow: (rowInfo) async {
|
||||||
await RowBackendService.deleteRow(rowInfo.viewId, rowInfo.rowId);
|
await RowBackendService.deleteRows(viewId, [rowInfo.rowId]);
|
||||||
},
|
},
|
||||||
moveRow: (int from, int to) {
|
moveRow: (int from, int to) {
|
||||||
final List<RowInfo> rows = [...state.rowInfos];
|
final List<RowInfo> rows = [...state.rowInfos];
|
||||||
|
@ -102,7 +102,7 @@ enum RowAction {
|
|||||||
RowBackendService.duplicateRow(viewId, rowId);
|
RowBackendService.duplicateRow(viewId, rowId);
|
||||||
break;
|
break;
|
||||||
case delete:
|
case delete:
|
||||||
RowBackendService.deleteRow(viewId, rowId);
|
RowBackendService.deleteRows(viewId, [rowId]);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -29,10 +29,11 @@ class RowCard extends StatefulWidget {
|
|||||||
required this.isEditing,
|
required this.isEditing,
|
||||||
required this.rowCache,
|
required this.rowCache,
|
||||||
required this.cellBuilder,
|
required this.cellBuilder,
|
||||||
required this.openCard,
|
required this.onTap,
|
||||||
required this.onStartEditing,
|
required this.onStartEditing,
|
||||||
required this.onEndEditing,
|
required this.onEndEditing,
|
||||||
required this.styleConfiguration,
|
required this.styleConfiguration,
|
||||||
|
this.onShiftTap,
|
||||||
this.groupingFieldId,
|
this.groupingFieldId,
|
||||||
this.groupId,
|
this.groupId,
|
||||||
});
|
});
|
||||||
@ -50,7 +51,9 @@ class RowCard extends StatefulWidget {
|
|||||||
final CardCellBuilder cellBuilder;
|
final CardCellBuilder cellBuilder;
|
||||||
|
|
||||||
/// Called when the user taps on the card.
|
/// 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.
|
/// Called when the user starts editing the card.
|
||||||
final VoidCallback onStartEditing;
|
final VoidCallback onStartEditing;
|
||||||
@ -67,12 +70,10 @@ class RowCard extends StatefulWidget {
|
|||||||
class _RowCardState extends State<RowCard> {
|
class _RowCardState extends State<RowCard> {
|
||||||
final popoverController = PopoverController();
|
final popoverController = PopoverController();
|
||||||
late final CardBloc _cardBloc;
|
late final CardBloc _cardBloc;
|
||||||
late final EditableRowNotifier rowNotifier;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
rowNotifier = EditableRowNotifier(isEditing: widget.isEditing);
|
|
||||||
_cardBloc = CardBloc(
|
_cardBloc = CardBloc(
|
||||||
fieldController: widget.fieldController,
|
fieldController: widget.fieldController,
|
||||||
viewId: widget.viewId,
|
viewId: widget.viewId,
|
||||||
@ -81,22 +82,18 @@ class _RowCardState extends State<RowCard> {
|
|||||||
rowMeta: widget.rowMeta,
|
rowMeta: widget.rowMeta,
|
||||||
rowCache: widget.rowCache,
|
rowCache: widget.rowCache,
|
||||||
)..add(const CardEvent.initial());
|
)..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
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
rowNotifier.dispose();
|
|
||||||
_cardBloc.close();
|
_cardBloc.close();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
@ -105,7 +102,14 @@ class _RowCardState extends State<RowCard> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return BlocProvider.value(
|
return BlocProvider.value(
|
||||||
value: _cardBloc,
|
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) =>
|
builder: (context, state) =>
|
||||||
PlatformExtension.isMobile ? _mobile(state) : _desktop(state),
|
PlatformExtension.isMobile ? _mobile(state) : _desktop(state),
|
||||||
),
|
),
|
||||||
@ -114,7 +118,7 @@ class _RowCardState extends State<RowCard> {
|
|||||||
|
|
||||||
Widget _mobile(CardState state) {
|
Widget _mobile(CardState state) {
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: () => widget.openCard(context),
|
onTap: () => widget.onTap(context),
|
||||||
behavior: HitTestBehavior.opaque,
|
behavior: HitTestBehavior.opaque,
|
||||||
child: MobileCardContent(
|
child: MobileCardContent(
|
||||||
rowMeta: state.rowMeta,
|
rowMeta: state.rowMeta,
|
||||||
@ -127,9 +131,9 @@ class _RowCardState extends State<RowCard> {
|
|||||||
|
|
||||||
Widget _desktop(CardState state) {
|
Widget _desktop(CardState state) {
|
||||||
final accessories = widget.styleConfiguration.showAccessory
|
final accessories = widget.styleConfiguration.showAccessory
|
||||||
? <CardAccessory>[
|
? const <CardAccessory>[
|
||||||
EditCardAccessory(rowNotifier: rowNotifier),
|
EditCardAccessory(),
|
||||||
const MoreCardOptionsAccessory(),
|
MoreCardOptionsAccessory(),
|
||||||
]
|
]
|
||||||
: null;
|
: null;
|
||||||
return AppFlowyPopover(
|
return AppFlowyPopover(
|
||||||
@ -148,10 +152,10 @@ class _RowCardState extends State<RowCard> {
|
|||||||
buildAccessoryWhen: () => state.isEditing == false,
|
buildAccessoryWhen: () => state.isEditing == false,
|
||||||
accessories: accessories ?? [],
|
accessories: accessories ?? [],
|
||||||
openAccessory: _handleOpenAccessory,
|
openAccessory: _handleOpenAccessory,
|
||||||
openCard: widget.openCard,
|
onTap: widget.onTap,
|
||||||
|
onShiftTap: widget.onShiftTap,
|
||||||
child: _CardContent(
|
child: _CardContent(
|
||||||
rowMeta: state.rowMeta,
|
rowMeta: state.rowMeta,
|
||||||
rowNotifier: rowNotifier,
|
|
||||||
cellBuilder: widget.cellBuilder,
|
cellBuilder: widget.cellBuilder,
|
||||||
styleConfiguration: widget.styleConfiguration,
|
styleConfiguration: widget.styleConfiguration,
|
||||||
cells: state.cells,
|
cells: state.cells,
|
||||||
@ -163,6 +167,7 @@ class _RowCardState extends State<RowCard> {
|
|||||||
void _handleOpenAccessory(AccessoryType newAccessoryType) {
|
void _handleOpenAccessory(AccessoryType newAccessoryType) {
|
||||||
switch (newAccessoryType) {
|
switch (newAccessoryType) {
|
||||||
case AccessoryType.edit:
|
case AccessoryType.edit:
|
||||||
|
widget.onStartEditing();
|
||||||
break;
|
break;
|
||||||
case AccessoryType.more:
|
case AccessoryType.more:
|
||||||
popoverController.show();
|
popoverController.show();
|
||||||
@ -174,14 +179,12 @@ class _RowCardState extends State<RowCard> {
|
|||||||
class _CardContent extends StatelessWidget {
|
class _CardContent extends StatelessWidget {
|
||||||
const _CardContent({
|
const _CardContent({
|
||||||
required this.rowMeta,
|
required this.rowMeta,
|
||||||
required this.rowNotifier,
|
|
||||||
required this.cellBuilder,
|
required this.cellBuilder,
|
||||||
required this.cells,
|
required this.cells,
|
||||||
required this.styleConfiguration,
|
required this.styleConfiguration,
|
||||||
});
|
});
|
||||||
|
|
||||||
final RowMetaPB rowMeta;
|
final RowMetaPB rowMeta;
|
||||||
final EditableRowNotifier rowNotifier;
|
|
||||||
final CardCellBuilder cellBuilder;
|
final CardCellBuilder cellBuilder;
|
||||||
final List<CellContext> cells;
|
final List<CellContext> cells;
|
||||||
final RowCardStyleConfiguration styleConfiguration;
|
final RowCardStyleConfiguration styleConfiguration;
|
||||||
@ -199,7 +202,7 @@ class _CardContent extends StatelessWidget {
|
|||||||
? child
|
? child
|
||||||
: FlowyHover(
|
: FlowyHover(
|
||||||
style: styleConfiguration.hoverStyle,
|
style: styleConfiguration.hoverStyle,
|
||||||
buildWhenOnHover: () => !rowNotifier.isEditing.value,
|
buildWhenOnHover: () => !context.read<CardBloc>().state.isEditing,
|
||||||
child: child,
|
child: child,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -209,16 +212,16 @@ class _CardContent extends StatelessWidget {
|
|||||||
RowMetaPB rowMeta,
|
RowMetaPB rowMeta,
|
||||||
List<CellContext> cells,
|
List<CellContext> cells,
|
||||||
) {
|
) {
|
||||||
// Remove all the cell listeners.
|
|
||||||
rowNotifier.unbind();
|
|
||||||
|
|
||||||
return cells.mapIndexed((int index, CellContext cellContext) {
|
return cells.mapIndexed((int index, CellContext cellContext) {
|
||||||
EditableCardNotifier? cellNotifier;
|
EditableCardNotifier? cellNotifier;
|
||||||
|
|
||||||
if (index == 0) {
|
if (index == 0) {
|
||||||
cellNotifier =
|
final bloc = context.read<CardBloc>();
|
||||||
EditableCardNotifier(isEditing: rowNotifier.isEditing.value);
|
cellNotifier = EditableCardNotifier(isEditing: bloc.state.isEditing);
|
||||||
rowNotifier.bindCell(cellContext, cellNotifier);
|
cellNotifier.isCellEditing.addListener(() {
|
||||||
|
final isEditing = cellNotifier!.isCellEditing.value;
|
||||||
|
bloc.add(CardEvent.setIsEditing(isEditing));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return cellBuilder.build(
|
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 {
|
class MoreCardOptionsAccessory extends StatelessWidget with CardAccessory {
|
||||||
const MoreCardOptionsAccessory({super.key});
|
const MoreCardOptionsAccessory({super.key});
|
||||||
|
|
||||||
@ -249,29 +270,6 @@ class MoreCardOptionsAccessory extends StatelessWidget with CardAccessory {
|
|||||||
AccessoryType get type => AccessoryType.more;
|
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 {
|
class RowCardStyleConfiguration {
|
||||||
const RowCardStyleConfiguration({
|
const RowCardStyleConfiguration({
|
||||||
required this.cellStyleMap,
|
required this.cellStyleMap,
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
import 'accessory.dart';
|
import 'accessory.dart';
|
||||||
@ -7,14 +8,16 @@ class RowCardContainer extends StatelessWidget {
|
|||||||
const RowCardContainer({
|
const RowCardContainer({
|
||||||
super.key,
|
super.key,
|
||||||
required this.child,
|
required this.child,
|
||||||
required this.openCard,
|
required this.onTap,
|
||||||
required this.openAccessory,
|
required this.openAccessory,
|
||||||
required this.accessories,
|
required this.accessories,
|
||||||
this.buildAccessoryWhen,
|
this.buildAccessoryWhen,
|
||||||
|
this.onShiftTap,
|
||||||
});
|
});
|
||||||
|
|
||||||
final Widget child;
|
final Widget child;
|
||||||
final void Function(BuildContext) openCard;
|
final void Function(BuildContext) onTap;
|
||||||
|
final void Function(BuildContext)? onShiftTap;
|
||||||
final void Function(AccessoryType) openAccessory;
|
final void Function(AccessoryType) openAccessory;
|
||||||
final List<CardAccessory> accessories;
|
final List<CardAccessory> accessories;
|
||||||
final bool Function()? buildAccessoryWhen;
|
final bool Function()? buildAccessoryWhen;
|
||||||
@ -41,7 +44,13 @@ class RowCardContainer extends StatelessWidget {
|
|||||||
|
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
behavior: HitTestBehavior.opaque,
|
behavior: HitTestBehavior.opaque,
|
||||||
onTap: () => openCard(context),
|
onTap: () {
|
||||||
|
if (HardwareKeyboard.instance.isShiftPressed) {
|
||||||
|
onShiftTap?.call(context);
|
||||||
|
} else {
|
||||||
|
onTap(context);
|
||||||
|
}
|
||||||
|
},
|
||||||
child: ConstrainedBox(
|
child: ConstrainedBox(
|
||||||
constraints: const BoxConstraints(minHeight: 30),
|
constraints: const BoxConstraints(minHeight: 30),
|
||||||
child: container,
|
child: container,
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import 'package:appflowy/plugins/database/application/cell/cell_controller.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
abstract class CardCell<T extends CardCellStyle> extends StatefulWidget {
|
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 {
|
abstract mixin class EditableCell {
|
||||||
// Each cell notifier will be bind to the [EditableRowNotifier], which enable
|
// Each cell notifier will be bind to the [EditableRowNotifier], which enable
|
||||||
// the row notifier receive its cells event. For example: begin editing the
|
// 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/flowy_infra_ui.dart';
|
||||||
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
|
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
import '../editable_cell_builder.dart';
|
import '../editable_cell_builder.dart';
|
||||||
@ -108,18 +109,11 @@ class _TextCellState extends State<TextCardCell> {
|
|||||||
return BlocProvider.value(
|
return BlocProvider.value(
|
||||||
value: cellBloc,
|
value: cellBloc,
|
||||||
child: BlocConsumer<TextCellBloc, TextCellState>(
|
child: BlocConsumer<TextCellBloc, TextCellState>(
|
||||||
listenWhen: (previous, current) =>
|
listenWhen: (previous, current) => previous.content != current.content,
|
||||||
previous.content != current.content && !current.enableEdit,
|
|
||||||
listener: (context, state) {
|
listener: (context, state) {
|
||||||
|
if (!state.enableEdit) {
|
||||||
_textEditingController.text = state.content;
|
_textEditingController.text = state.content;
|
||||||
},
|
|
||||||
buildWhen: (previous, current) {
|
|
||||||
if (previous.content != current.content &&
|
|
||||||
_textEditingController.text == current.content) {
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return previous != current;
|
|
||||||
},
|
},
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
final isTitle = cellBloc.cellController.fieldInfo.isPrimary;
|
final isTitle = cellBloc.cellController.fieldInfo.isPrimary;
|
||||||
@ -196,12 +190,18 @@ class _TextCellState extends State<TextCardCell> {
|
|||||||
widget.style.padding.add(const EdgeInsets.symmetric(vertical: 4.0));
|
widget.style.padding.add(const EdgeInsets.symmetric(vertical: 4.0));
|
||||||
return IgnorePointer(
|
return IgnorePointer(
|
||||||
ignoring: !isEditing,
|
ignoring: !isEditing,
|
||||||
|
child: CallbackShortcuts(
|
||||||
|
bindings: {
|
||||||
|
const SingleActivator(LogicalKeyboardKey.escape): () =>
|
||||||
|
focusNode.unfocus(),
|
||||||
|
},
|
||||||
child: TextField(
|
child: TextField(
|
||||||
controller: _textEditingController,
|
controller: _textEditingController,
|
||||||
focusNode: focusNode,
|
focusNode: focusNode,
|
||||||
onChanged: (_) {
|
onChanged: (_) {
|
||||||
if (_textEditingController.value.composing.isCollapsed) {
|
if (_textEditingController.value.composing.isCollapsed) {
|
||||||
cellBloc.add(TextCellEvent.updateText(_textEditingController.text));
|
cellBloc
|
||||||
|
.add(TextCellEvent.updateText(_textEditingController.text));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onEditingComplete: () => focusNode.unfocus(),
|
onEditingComplete: () => focusNode.unfocus(),
|
||||||
@ -222,6 +222,8 @@ class _TextCellState extends State<TextCardCell> {
|
|||||||
color: Theme.of(context).hintColor,
|
color: Theme.of(context).hintColor,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
onTapOutside: (_) {},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -80,7 +80,9 @@ class _TextCellState extends GridEditableTextCell<EditableTextCell> {
|
|||||||
value: cellBloc,
|
value: cellBloc,
|
||||||
child: BlocListener<TextCellBloc, TextCellState>(
|
child: BlocListener<TextCellBloc, TextCellState>(
|
||||||
listener: (context, state) {
|
listener: (context, state) {
|
||||||
|
if (!focusNode.hasFocus) {
|
||||||
_textEditingController.text = state.content;
|
_textEditingController.text = state.content;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
child: Builder(
|
child: Builder(
|
||||||
builder: (context) {
|
builder: (context) {
|
||||||
|
@ -53,7 +53,7 @@ class RowDetailPageDeleteButton extends StatelessWidget {
|
|||||||
text: FlowyText.regular(LocaleKeys.grid_row_delete.tr()),
|
text: FlowyText.regular(LocaleKeys.grid_row_delete.tr()),
|
||||||
leftIcon: const FlowySvg(FlowySvgs.trash_m),
|
leftIcon: const FlowySvg(FlowySvgs.trash_m),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
RowBackendService.deleteRow(viewId, rowId);
|
RowBackendService.deleteRows(viewId, [rowId]);
|
||||||
FlowyOverlay.pop(context);
|
FlowyOverlay.pop(context);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
@ -288,7 +288,6 @@ class _TitleSkin extends IEditableTextCellSkin {
|
|||||||
return TextField(
|
return TextField(
|
||||||
controller: textEditingController,
|
controller: textEditingController,
|
||||||
focusNode: focusNode,
|
focusNode: focusNode,
|
||||||
maxLines: null,
|
|
||||||
autofocus: true,
|
autofocus: true,
|
||||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(fontSize: 28),
|
style: Theme.of(context).textTheme.bodyMedium?.copyWith(fontSize: 28),
|
||||||
decoration: InputDecoration(
|
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"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
path: "."
|
path: "."
|
||||||
ref: "404262fca4369bc35ff305316e4d59341a732f56"
|
ref: "8a6434ae3d02624b614a010af80f775db11bf22e"
|
||||||
resolved-ref: "404262fca4369bc35ff305316e4d59341a732f56"
|
resolved-ref: "8a6434ae3d02624b614a010af80f775db11bf22e"
|
||||||
url: "https://github.com/AppFlowy-IO/appflowy-board.git"
|
url: "https://github.com/AppFlowy-IO/appflowy-board.git"
|
||||||
source: git
|
source: git
|
||||||
version: "0.1.2"
|
version: "0.1.2"
|
||||||
|
@ -44,7 +44,7 @@ dependencies:
|
|||||||
# path: ../../../appflowy-board
|
# path: ../../../appflowy-board
|
||||||
git:
|
git:
|
||||||
url: https://github.com/AppFlowy-IO/appflowy-board.git
|
url: https://github.com/AppFlowy-IO/appflowy-board.git
|
||||||
ref: 404262fca4369bc35ff305316e4d59341a732f56
|
ref: 8a6434ae3d02624b614a010af80f775db11bf22e
|
||||||
appflowy_result:
|
appflowy_result:
|
||||||
path: packages/appflowy_result
|
path: packages/appflowy_result
|
||||||
appflowy_editor_plugins: ^0.0.2
|
appflowy_editor_plugins: ^0.0.2
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import 'package:appflowy/plugins/database/application/database_controller.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/board/application/board_bloc.dart';
|
||||||
|
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
import 'util.dart';
|
import 'util.dart';
|
||||||
@ -15,26 +16,42 @@ void main() {
|
|||||||
final context = await boardTest.createTestBoard();
|
final context = await boardTest.createTestBoard();
|
||||||
final databaseController = DatabaseController(view: context.gridView);
|
final databaseController = DatabaseController(view: context.gridView);
|
||||||
final boardBloc = BoardBloc(
|
final boardBloc = BoardBloc(
|
||||||
view: context.gridView,
|
|
||||||
databaseController: databaseController,
|
databaseController: databaseController,
|
||||||
)..add(const BoardEvent.initial());
|
)..add(const BoardEvent.initial());
|
||||||
await boardResponseFuture();
|
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;
|
// 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(
|
assert(
|
||||||
boardBloc.state.groupIds.length == 4,
|
groupIds.length == 4,
|
||||||
'but receive ${boardBloc.state.groupIds.length}',
|
'but receive ${groupIds.length}',
|
||||||
);
|
);
|
||||||
|
|
||||||
boardBloc.add(BoardEvent.createBottomRow(boardBloc.state.groupIds[3]));
|
boardBloc.add(
|
||||||
|
BoardEvent.createRow(
|
||||||
|
groupIds[3],
|
||||||
|
OrderObjectPositionTypePB.End,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
),
|
||||||
|
);
|
||||||
await boardResponseFuture();
|
await boardResponseFuture();
|
||||||
|
|
||||||
|
groupIds = boardBloc.state.maybeMap(
|
||||||
|
orElse: () => [],
|
||||||
|
ready: (value) => value.groupIds,
|
||||||
|
);
|
||||||
|
lastGroupId = groupIds.last;
|
||||||
|
|
||||||
assert(
|
assert(
|
||||||
boardBloc.groupControllers[groupId]!.group.rows.length == 1,
|
boardBloc.groupControllers[lastGroupId]!.group.rows.length == 1,
|
||||||
'but receive ${boardBloc.groupControllers[groupId]!.group.rows.length}',
|
'but receive ${boardBloc.groupControllers[lastGroupId]!.group.rows.length}',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -15,7 +15,6 @@ void main() {
|
|||||||
test('create build-in kanban board test', () async {
|
test('create build-in kanban board test', () async {
|
||||||
final context = await boardTest.createTestBoard();
|
final context = await boardTest.createTestBoard();
|
||||||
final boardBloc = BoardBloc(
|
final boardBloc = BoardBloc(
|
||||||
view: context.gridView,
|
|
||||||
databaseController: DatabaseController(view: context.gridView),
|
databaseController: DatabaseController(view: context.gridView),
|
||||||
)..add(const BoardEvent.initial());
|
)..add(const BoardEvent.initial());
|
||||||
await boardResponseFuture();
|
await boardResponseFuture();
|
||||||
@ -27,7 +26,6 @@ void main() {
|
|||||||
test('edit kanban board field name test', () async {
|
test('edit kanban board field name test', () async {
|
||||||
final context = await boardTest.createTestBoard();
|
final context = await boardTest.createTestBoard();
|
||||||
final boardBloc = BoardBloc(
|
final boardBloc = BoardBloc(
|
||||||
view: context.gridView,
|
|
||||||
databaseController: DatabaseController(view: context.gridView),
|
databaseController: DatabaseController(view: context.gridView),
|
||||||
)..add(const BoardEvent.initial());
|
)..add(const BoardEvent.initial());
|
||||||
await boardResponseFuture();
|
await boardResponseFuture();
|
||||||
@ -59,7 +57,6 @@ void main() {
|
|||||||
test('create a new field in kanban board test', () async {
|
test('create a new field in kanban board test', () async {
|
||||||
final context = await boardTest.createTestBoard();
|
final context = await boardTest.createTestBoard();
|
||||||
final boardBloc = BoardBloc(
|
final boardBloc = BoardBloc(
|
||||||
view: context.gridView,
|
|
||||||
databaseController: DatabaseController(view: context.gridView),
|
databaseController: DatabaseController(view: context.gridView),
|
||||||
)..add(const BoardEvent.initial());
|
)..add(const BoardEvent.initial());
|
||||||
await boardResponseFuture();
|
await boardResponseFuture();
|
||||||
|
@ -17,7 +17,6 @@ void main() {
|
|||||||
test('group by checkbox field test', () async {
|
test('group by checkbox field test', () async {
|
||||||
final context = await boardTest.createTestBoard();
|
final context = await boardTest.createTestBoard();
|
||||||
final boardBloc = BoardBloc(
|
final boardBloc = BoardBloc(
|
||||||
view: context.gridView,
|
|
||||||
databaseController: DatabaseController(view: context.gridView),
|
databaseController: DatabaseController(view: context.gridView),
|
||||||
)..add(const BoardEvent.initial());
|
)..add(const BoardEvent.initial());
|
||||||
await boardResponseFuture();
|
await boardResponseFuture();
|
||||||
|
@ -41,7 +41,6 @@ void main() {
|
|||||||
|
|
||||||
// assert only have the 'No status' group
|
// assert only have the 'No status' group
|
||||||
final boardBloc = BoardBloc(
|
final boardBloc = BoardBloc(
|
||||||
view: context.gridView,
|
|
||||||
databaseController: DatabaseController(view: context.gridView),
|
databaseController: DatabaseController(view: context.gridView),
|
||||||
)..add(const BoardEvent.initial());
|
)..add(const BoardEvent.initial());
|
||||||
await boardResponseFuture();
|
await boardResponseFuture();
|
||||||
@ -91,7 +90,6 @@ void main() {
|
|||||||
|
|
||||||
// assert there are only three group
|
// assert there are only three group
|
||||||
final boardBloc = BoardBloc(
|
final boardBloc = BoardBloc(
|
||||||
view: context.gridView,
|
|
||||||
databaseController: DatabaseController(view: context.gridView),
|
databaseController: DatabaseController(view: context.gridView),
|
||||||
)..add(const BoardEvent.initial());
|
)..add(const BoardEvent.initial());
|
||||||
await boardResponseFuture();
|
await boardResponseFuture();
|
||||||
|
@ -38,7 +38,6 @@ void main() {
|
|||||||
blocTest<BoardBloc, BoardState>(
|
blocTest<BoardBloc, BoardState>(
|
||||||
'assert the number of groups is 1',
|
'assert the number of groups is 1',
|
||||||
build: () => BoardBloc(
|
build: () => BoardBloc(
|
||||||
view: context.gridView,
|
|
||||||
databaseController: DatabaseController(view: context.gridView),
|
databaseController: DatabaseController(view: context.gridView),
|
||||||
)..add(
|
)..add(
|
||||||
const BoardEvent.initial(),
|
const BoardEvent.initial(),
|
||||||
|
@ -236,11 +236,10 @@ impl EventIntegrationTest {
|
|||||||
|
|
||||||
pub async fn delete_row(&self, view_id: &str, row_id: &str) -> Option<FlowyError> {
|
pub async fn delete_row(&self, view_id: &str, row_id: &str) -> Option<FlowyError> {
|
||||||
EventBuilder::new(self.clone())
|
EventBuilder::new(self.clone())
|
||||||
.event(DatabaseEvent::DeleteRow)
|
.event(DatabaseEvent::DeleteRows)
|
||||||
.payload(RowIdPB {
|
.payload(RepeatedRowIdPB {
|
||||||
view_id: view_id.to_string(),
|
view_id: view_id.to_string(),
|
||||||
row_id: row_id.to_string(),
|
row_ids: vec![row_id.to_string()],
|
||||||
group_id: None,
|
|
||||||
})
|
})
|
||||||
.async_send()
|
.async_send()
|
||||||
.await
|
.await
|
||||||
@ -523,7 +522,7 @@ impl EventIntegrationTest {
|
|||||||
) -> Vec<RelatedRowDataPB> {
|
) -> Vec<RelatedRowDataPB> {
|
||||||
EventBuilder::new(self.clone())
|
EventBuilder::new(self.clone())
|
||||||
.event(DatabaseEvent::GetRelatedRowDatas)
|
.event(DatabaseEvent::GetRelatedRowDatas)
|
||||||
.payload(RepeatedRowIdPB {
|
.payload(GetRelatedRowDataPB {
|
||||||
database_id,
|
database_id,
|
||||||
row_ids,
|
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![])
|
.create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![])
|
||||||
.await;
|
.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;
|
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]
|
#[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)]
|
#[derive(ProtoBuf, Default, Validate)]
|
||||||
pub struct CreateRowPayloadPB {
|
pub struct CreateRowPayloadPB {
|
||||||
#[pb(index = 1)]
|
#[pb(index = 1)]
|
||||||
|
@ -78,7 +78,7 @@ pub struct RepeatedRelatedRowDataPB {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default, Clone, ProtoBuf)]
|
#[derive(Debug, Default, Clone, ProtoBuf)]
|
||||||
pub struct RepeatedRowIdPB {
|
pub struct GetRelatedRowDataPB {
|
||||||
#[pb(index = 1)]
|
#[pb(index = 1)]
|
||||||
pub database_id: String,
|
pub database_id: String,
|
||||||
|
|
||||||
|
@ -385,14 +385,19 @@ pub(crate) async fn update_row_meta_handler(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument(level = "debug", skip(data, manager), err)]
|
#[tracing::instrument(level = "debug", skip(data, manager), err)]
|
||||||
pub(crate) async fn delete_row_handler(
|
pub(crate) async fn delete_rows_handler(
|
||||||
data: AFPluginData<RowIdPB>,
|
data: AFPluginData<RepeatedRowIdPB>,
|
||||||
manager: AFPluginState<Weak<DatabaseManager>>,
|
manager: AFPluginState<Weak<DatabaseManager>>,
|
||||||
) -> Result<(), FlowyError> {
|
) -> Result<(), FlowyError> {
|
||||||
let manager = upgrade_manager(manager)?;
|
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?;
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1062,11 +1067,11 @@ pub(crate) async fn update_relation_cell_handler(
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn get_related_row_datas_handler(
|
pub(crate) async fn get_related_row_datas_handler(
|
||||||
data: AFPluginData<RepeatedRowIdPB>,
|
data: AFPluginData<GetRelatedRowDataPB>,
|
||||||
manager: AFPluginState<Weak<DatabaseManager>>,
|
manager: AFPluginState<Weak<DatabaseManager>>,
|
||||||
) -> DataResult<RepeatedRelatedRowDataPB, FlowyError> {
|
) -> DataResult<RepeatedRelatedRowDataPB, FlowyError> {
|
||||||
let manager = upgrade_manager(manager)?;
|
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 database_editor = manager.get_database(¶ms.database_id).await?;
|
||||||
let row_datas = database_editor
|
let row_datas = database_editor
|
||||||
.get_related_rows(Some(¶ms.row_ids))
|
.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::GetRow, get_row_handler)
|
||||||
.event(DatabaseEvent::GetRowMeta, get_row_meta_handler)
|
.event(DatabaseEvent::GetRowMeta, get_row_meta_handler)
|
||||||
.event(DatabaseEvent::UpdateRowMeta, update_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::DuplicateRow, duplicate_row_handler)
|
||||||
.event(DatabaseEvent::MoveRow, move_row_handler)
|
.event(DatabaseEvent::MoveRow, move_row_handler)
|
||||||
// Cell
|
// Cell
|
||||||
@ -223,8 +223,8 @@ pub enum DatabaseEvent {
|
|||||||
#[event(input = "RowIdPB", output = "OptionalRowPB")]
|
#[event(input = "RowIdPB", output = "OptionalRowPB")]
|
||||||
GetRow = 51,
|
GetRow = 51,
|
||||||
|
|
||||||
#[event(input = "RowIdPB")]
|
#[event(input = "RepeatedRowIdPB")]
|
||||||
DeleteRow = 52,
|
DeleteRows = 52,
|
||||||
|
|
||||||
#[event(input = "RowIdPB")]
|
#[event(input = "RowIdPB")]
|
||||||
DuplicateRow = 53,
|
DuplicateRow = 53,
|
||||||
@ -364,7 +364,7 @@ pub enum DatabaseEvent {
|
|||||||
UpdateRelationCell = 171,
|
UpdateRelationCell = 171,
|
||||||
|
|
||||||
/// Get the names of the linked rows in a relation cell.
|
/// Get the names of the linked rows in a relation cell.
|
||||||
#[event(input = "RepeatedRowIdPB", output = "RepeatedRelatedRowDataPB")]
|
#[event(input = "GetRelatedRowDataPB", output = "RepeatedRelatedRowDataPB")]
|
||||||
GetRelatedRowDatas = 172,
|
GetRelatedRowDatas = 172,
|
||||||
|
|
||||||
/// Get the names of all the rows in a related database.
|
/// 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) {
|
pub async fn delete_rows(&self, row_ids: &[RowId]) {
|
||||||
let row = self.database.lock().remove_row(row_id);
|
let rows = self.database.lock().remove_rows(row_ids);
|
||||||
if let Some(row) = row {
|
|
||||||
|
for row in rows {
|
||||||
tracing::trace!("Did delete row:{:?}", row);
|
tracing::trace!("Did delete row:{:?}", row);
|
||||||
for view in self.database_views.editors().await {
|
for view in self.database_views.editors().await {
|
||||||
view.v_did_delete_row(&row).await;
|
view.v_did_delete_row(&row).await;
|
||||||
|
@ -148,8 +148,8 @@ impl DatabaseGroupTest {
|
|||||||
row_index,
|
row_index,
|
||||||
} => {
|
} => {
|
||||||
let row = self.row_at_index(group_index, row_index).await;
|
let row = self.row_at_index(group_index, row_index).await;
|
||||||
let row_id = RowId::from(row.id);
|
let row_ids = vec![RowId::from(row.id)];
|
||||||
self.editor.delete_row(&row_id).await;
|
self.editor.delete_rows(&row_ids).await;
|
||||||
},
|
},
|
||||||
GroupScript::UpdateGroupedCell {
|
GroupScript::UpdateGroupedCell {
|
||||||
from_group_index,
|
from_group_index,
|
||||||
|
Loading…
Reference in New Issue
Block a user