feat: shrinkWrap grid in document (#6925)

* feat: shrinkWrap grid in document

* fix: clean up code and minor fixes

* test: add test w/ load more option

* fix: reinstate pageview & clean unused code

* fix: clean database tab bar view
This commit is contained in:
Mathias Mogensen 2024-12-12 02:21:06 +01:00 committed by GitHub
parent 0bf706f438
commit 1d46923c47
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 478 additions and 300 deletions

View File

@ -1,12 +1,15 @@
import 'package:appflowy/plugins/database/board/presentation/board_page.dart';
import 'package:appflowy/plugins/database/calendar/presentation/calendar_page.dart';
import 'package:appflowy/plugins/database/grid/presentation/grid_page.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/footer/grid_footer.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/row/row.dart';
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/text.dart';
import 'package:appflowy/plugins/inline_actions/widgets/inline_actions_handler.dart';
import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flowy_infra/uuid.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
@ -174,9 +177,110 @@ void main() {
findsOneWidget,
);
});
testWidgets('insert a referenced grid with many rows (load more option)',
(tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await insertLinkedDatabase(tester, ViewLayoutPB.Grid);
// validate the referenced grid is inserted
expect(
find.descendant(
of: find.byType(AppFlowyEditor),
matching: find.byType(GridPage),
),
findsOneWidget,
);
// https://github.com/AppFlowy-IO/AppFlowy/issues/3533
// test: the selection of editor should be clear when editing the grid
await tester.editor.updateSelection(
Selection.collapsed(
Position(path: [1]),
),
);
final gridTextCell = find.byType(EditableTextCell).first;
await tester.tapButton(gridTextCell);
expect(tester.editor.getCurrentEditorState().selection, isNull);
final editorScrollable = find
.descendant(
of: find.byType(AppFlowyEditor),
matching: find.byWidgetPredicate(
(w) => w is Scrollable && w.axis == Axis.vertical,
),
)
.first;
// Add 100 Rows to the linked database
final addRowFinder = find.byType(GridAddRowButton);
for (var i = 0; i < 100; i++) {
await tester.scrollUntilVisible(
addRowFinder,
100,
scrollable: editorScrollable,
);
await tester.tapButton(addRowFinder);
await tester.pumpAndSettle();
}
// Since all rows visible are those we added, we should see all of them
expect(find.byType(GridRow), findsNWidgets(103));
// Navigate to getting started
await tester.openPage(gettingStarted);
// Navigate back to the document
await tester.openPage('insert_a_reference_${ViewLayoutPB.Grid.name}');
// We see only 25 Grid Rows
expect(find.byType(GridRow), findsNWidgets(25));
// We see Add row and load more button
expect(find.byType(GridAddRowButton), findsOneWidget);
expect(find.byType(GridRowLoadMoreButton), findsOneWidget);
// Load more rows, expect 50 visible
await _loadMoreRows(tester, editorScrollable, 50);
// Load more rows, expect 75 visible
await _loadMoreRows(tester, editorScrollable, 75);
// Load more rows, expect 100 visible
await _loadMoreRows(tester, editorScrollable, 100);
// Load more rows, expect 103 visible
await _loadMoreRows(tester, editorScrollable, 103);
// We no longer see load more option
expect(find.byType(GridRowLoadMoreButton), findsNothing);
});
});
}
Future<void> _loadMoreRows(
WidgetTester tester,
Finder scrollable, [
int? expectedRows,
]) async {
await tester.scrollUntilVisible(
find.byType(GridRowLoadMoreButton),
100,
scrollable: scrollable,
);
await tester.pumpAndSettle();
await tester.tap(find.byType(GridRowLoadMoreButton));
await tester.pumpAndSettle();
if (expectedRows != null) {
expect(find.byType(GridRow), findsNWidgets(expectedRows));
}
}
/// Insert a referenced database of [layout] into the document
Future<void> insertLinkedDatabase(
WidgetTester tester,

View File

@ -54,6 +54,7 @@ class BoardPageTabBarBuilderImpl extends DatabaseTabBarItemBuilder {
key: _makeValueKey(controller),
view: view,
databaseController: controller,
shrinkWrap: shrinkWrap,
)
: MobileBoardPage(
key: _makeValueKey(controller),
@ -98,6 +99,7 @@ class DesktopBoardPage extends StatefulWidget {
required this.view,
required this.databaseController,
this.onEditStateChanged,
this.shrinkWrap = false,
});
final ViewPB view;
@ -107,6 +109,9 @@ class DesktopBoardPage extends StatefulWidget {
/// Called when edit state changed
final VoidCallback? onEditStateChanged;
/// If true, the board will shrink wrap its content
final bool shrinkWrap;
@override
State<DesktopBoardPage> createState() => _DesktopBoardPageState();
}
@ -187,24 +192,17 @@ class _DesktopBoardPageState extends State<DesktopBoardPage> {
Widget build(BuildContext context) {
return MultiBlocProvider(
providers: [
BlocProvider.value(
value: _boardBloc,
),
BlocProvider.value(
value: _boardActionsCubit,
),
BlocProvider<BoardBloc>.value(value: _boardBloc),
BlocProvider.value(value: _boardActionsCubit),
],
child: BlocBuilder<BoardBloc, BoardState>(
builder: (context, state) => state.maybeMap(
loading: (_) => const Center(
child: CircularProgressIndicator.adaptive(),
),
error: (err) => Center(
child: AppFlowyErrorPage(
error: err.error,
),
),
error: (err) => Center(child: AppFlowyErrorPage(error: err.error)),
orElse: () => _BoardContent(
shrinkWrap: widget.shrinkWrap,
onEditStateChanged: widget.onEditStateChanged,
focusScope: _focusScope,
boardController: _boardController,
@ -243,11 +241,13 @@ class _BoardContent extends StatefulWidget {
required this.boardController,
required this.focusScope,
this.onEditStateChanged,
this.shrinkWrap = false,
});
final AppFlowyBoardController boardController;
final BoardFocusScope focusScope;
final VoidCallback? onEditStateChanged;
final bool shrinkWrap;
@override
State<_BoardContent> createState() => _BoardContentState();
@ -358,12 +358,8 @@ class _BoardContentState extends State<_BoardContent> {
),
footerBuilder: (_, groupData) => MultiBlocProvider(
providers: [
BlocProvider.value(
value: context.read<BoardBloc>(),
),
BlocProvider.value(
value: context.read<BoardActionsCubit>(),
),
BlocProvider.value(value: context.read<BoardBloc>()),
BlocProvider.value(value: context.read<BoardActionsCubit>()),
],
child: BoardColumnFooter(
columnData: groupData,

View File

@ -20,13 +20,20 @@ import '../../application/database_controller.dart';
part 'grid_bloc.freezed.dart';
class GridBloc extends Bloc<GridEvent, GridState> {
GridBloc({required ViewPB view, required this.databaseController})
: super(GridState.initial(view.id)) {
GridBloc({
required ViewPB view,
required this.databaseController,
this.shrinkWrapped = false,
}) : super(GridState.initial(view.id)) {
_dispatch();
}
final DatabaseController databaseController;
/// When true will emit the count of visible rows to show
///
final bool shrinkWrapped;
String get viewId => databaseController.viewId;
UserProfilePB? _userProfile;
@ -64,12 +71,22 @@ class GridBloc extends Bloc<GridEvent, GridState> {
);
},
createRow: (openRowDetail) async {
final result = await RowBackendService.createRow(viewId: viewId);
final lastVisibleRowId =
shrinkWrapped ? state.lastVisibleRow?.rowId : null;
final result = await RowBackendService.createRow(
viewId: viewId,
position: lastVisibleRowId != null
? OrderObjectPositionTypePB.After
: null,
targetRowId: lastVisibleRowId,
);
result.fold(
(createdRow) => emit(
state.copyWith(
createdRow: createdRow,
openRowDetail: openRowDetail ?? false,
visibleRows: state.visibleRows + 1,
),
),
(err) => Log.error(err),
@ -93,11 +110,7 @@ class GridBloc extends Bloc<GridEvent, GridState> {
databaseController.moveRow(fromRowId: fromRow, toRowId: toRow);
},
didReceiveFieldUpdate: (fields) {
emit(
state.copyWith(
fields: fields,
),
);
emit(state.copyWith(fields: fields));
},
didLoadRows: (newRowInfos, reason) {
emit(
@ -109,17 +122,13 @@ class GridBloc extends Bloc<GridEvent, GridState> {
);
},
didReceveFilters: (filters) {
emit(
state.copyWith(filters: filters),
);
emit(state.copyWith(filters: filters));
},
didReceveSorts: (sorts) {
emit(
state.copyWith(
reorderable: sorts.isEmpty,
sorts: sorts,
),
);
emit(state.copyWith(reorderable: sorts.isEmpty, sorts: sorts));
},
loadMoreRows: () {
emit(state.copyWith(visibleRows: state.visibleRows + 25));
},
);
},
@ -204,11 +213,11 @@ class GridEvent with _$GridEvent {
const factory GridEvent.didReceiveFieldUpdate(
List<FieldInfo> fields,
) = _DidReceiveFieldUpdate;
const factory GridEvent.didReceveFilters(List<DatabaseFilter> filters) =
_DidReceiveFilters;
const factory GridEvent.didReceveSorts(List<DatabaseSort> sorts) =
_DidReceiveSorts;
const factory GridEvent.loadMoreRows() = _LoadMoreRows;
}
@freezed
@ -225,6 +234,7 @@ class GridState with _$GridState {
required List<DatabaseSort> sorts,
required List<DatabaseFilter> filters,
required bool openRowDetail,
@Default(0) int visibleRows,
}) = _GridState;
factory GridState.initial(String viewId) => GridState(
@ -239,5 +249,19 @@ class GridState with _$GridState {
filters: [],
sorts: [],
openRowDetail: false,
visibleRows: 25,
);
}
extension _LastVisibleRow on GridState {
/// Returns the last visible [RowInfo] in the list of [rowInfos].
/// Only returns if the visibleRows is less than the rowCount, otherwise returns null.
///
RowInfo? get lastVisibleRow {
if (visibleRows < rowCount) {
return rowInfos[visibleRows - 1];
}
return null;
}
}

View File

@ -1,6 +1,7 @@
import 'dart:async';
import 'dart:math';
import 'package:appflowy/plugins/database/grid/presentation/widgets/toolbar/grid_setting_bar.dart';
import 'package:appflowy/plugins/database/tab_bar/desktop/setting_menu.dart';
import 'package:flutter/material.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
@ -8,8 +9,6 @@ import 'package:appflowy/plugins/database/application/row/row_service.dart';
import 'package:appflowy/plugins/database/application/tab_bar_bloc.dart';
import 'package:appflowy/plugins/database/domain/sort_service.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/calculations/calculations_row.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/toolbar/grid_setting_bar.dart';
import 'package:appflowy/plugins/database/tab_bar/desktop/setting_menu.dart';
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_builder.dart';
import 'package:appflowy/shared/flowy_error_page.dart';
import 'package:appflowy/workspace/application/action_navigation/action_navigation_bloc.dart';
@ -67,6 +66,7 @@ class DesktopGridTabBarBuilderImpl extends DatabaseTabBarItemBuilder {
view: view,
databaseController: controller,
initialRowId: initialRowId,
shrinkWrap: shrinkWrap,
);
}
@ -110,12 +110,14 @@ class GridPage extends StatefulWidget {
required this.databaseController,
this.onDeleted,
this.initialRowId,
this.shrinkWrap = false,
});
final ViewPB view;
final DatabaseController databaseController;
final VoidCallback? onDeleted;
final String? initialRowId;
final bool shrinkWrap;
@override
State<GridPage> createState() => _GridPageState();
@ -124,13 +126,22 @@ class GridPage extends StatefulWidget {
class _GridPageState extends State<GridPage> {
bool _didOpenInitialRow = false;
late final GridBloc gridBloc = GridBloc(
view: widget.view,
databaseController: widget.databaseController,
shrinkWrapped: widget.shrinkWrap,
)..add(const GridEvent.initial());
@override
void dispose() {
gridBloc.close();
super.dispose();
}
@override
Widget build(BuildContext context) {
return BlocProvider<GridBloc>(
create: (context) => GridBloc(
view: widget.view,
databaseController: widget.databaseController,
)..add(const GridEvent.initial()),
create: (_) => gridBloc,
child: BlocListener<ActionNavigationBloc, ActionNavigationState>(
listener: (context, state) {
final action = state.action;
@ -147,6 +158,7 @@ class _GridPageState extends State<GridPage> {
child: BlocConsumer<GridBloc, GridState>(
listener: listener,
builder: (context, state) => state.loadingState.map(
idle: (_) => const SizedBox.shrink(),
loading: (_) => const Center(
child: CircularProgressIndicator.adaptive(),
),
@ -155,25 +167,18 @@ class _GridPageState extends State<GridPage> {
child: GridPageContent(
key: ValueKey(widget.view.id),
view: widget.view,
shrinkWrap: widget.shrinkWrap,
),
),
(err) => Center(
child: AppFlowyErrorPage(
error: err,
),
),
(err) => Center(child: AppFlowyErrorPage(error: err)),
),
idle: (_) => const SizedBox.shrink(),
),
),
),
);
}
void _openRow(
BuildContext context,
String rowId,
) {
void _openRow(BuildContext context, String rowId) {
WidgetsBinding.instance.addPostFrameCallback((_) {
final gridBloc = context.read<GridBloc>();
final rowCache = gridBloc.rowCache;
@ -249,9 +254,11 @@ class GridPageContent extends StatefulWidget {
const GridPageContent({
super.key,
required this.view,
this.shrinkWrap = false,
});
final ViewPB view;
final bool shrinkWrap;
@override
State<GridPageContent> createState() => _GridPageContentState();
@ -279,13 +286,13 @@ class _GridPageContentState extends State<GridPageContent> {
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
_GridHeader(
headerScrollController: headerScrollController,
),
_GridHeader(headerScrollController: headerScrollController),
_GridRows(
viewId: widget.view.id,
scrollController: _scrollController,
shrinkWrap: widget.shrinkWrap,
),
],
);
@ -300,12 +307,10 @@ class _GridHeader extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocBuilder<GridBloc, GridState>(
builder: (context, state) {
return GridHeaderSliverAdaptor(
viewId: state.viewId,
anchorScrollController: headerScrollController,
);
},
builder: (_, state) => GridHeaderSliverAdaptor(
viewId: state.viewId,
anchorScrollController: headerScrollController,
),
);
}
}
@ -314,11 +319,17 @@ class _GridRows extends StatefulWidget {
const _GridRows({
required this.viewId,
required this.scrollController,
this.shrinkWrap = false,
});
final String viewId;
final GridScrollController scrollController;
/// When [shrinkWrap] is active, the Grid will show items according to
/// GridState.visibleRows and will not have a vertical scroll area.
///
final bool shrinkWrap;
@override
State<_GridRows> createState() => _GridRowsState();
}
@ -330,8 +341,10 @@ class _GridRowsState extends State<_GridRows> {
@override
void initState() {
super.initState();
_evaluateFloatingCalculations();
widget.scrollController.verticalController.addListener(_onScrollChanged);
if (!widget.shrinkWrap) {
_evaluateFloatingCalculations();
widget.scrollController.verticalController.addListener(_onScrollChanged);
}
}
void _onScrollChanged() {
@ -345,13 +358,16 @@ class _GridRowsState extends State<_GridRows> {
@override
void dispose() {
widget.scrollController.verticalController.removeListener(_onScrollChanged);
if (!widget.shrinkWrap) {
widget.scrollController.verticalController
.removeListener(_onScrollChanged);
}
super.dispose();
}
void _evaluateFloatingCalculations() {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
if (mounted && !widget.shrinkWrap) {
setState(() {
final verticalController = widget.scrollController.verticalController;
// maxScrollExtent is 0.0 if scrolling is not possible
@ -367,121 +383,59 @@ class _GridRowsState extends State<_GridRows> {
@override
Widget build(BuildContext context) {
return BlocBuilder<GridBloc, GridState>(
buildWhen: (previous, current) => previous.fields != current.fields,
builder: (context, state) {
return Flexible(
child: LayoutBuilder(
builder: (BuildContext context, BoxConstraints layoutConstraits) {
return _WrapScrollView(
scrollController: widget.scrollController,
contentWidth: GridLayout.headerWidth(
context
.read<DatabasePluginWidgetBuilderSize>()
.horizontalPadding,
state.fields,
),
child: BlocConsumer<GridBloc, GridState>(
listenWhen: (previous, current) =>
previous.rowCount != current.rowCount,
listener: (context, state) => _evaluateFloatingCalculations(),
builder: (context, state) {
return ScrollConfiguration(
behavior: ScrollConfiguration.of(context).copyWith(
scrollbars: false,
),
child: _renderList(context, state, layoutConstraits),
);
},
),
);
},
),
);
},
);
}
Widget _renderList(
BuildContext context,
GridState state,
BoxConstraints layoutConstraints,
) {
// 1. GridRowBottomBar
// 2. GridCalculationsRow
final itemCount =
state.rowInfos.length + (showFloatingCalculations ? 1 : 2);
return Column(
children: [
Expanded(
child: ReorderableListView.builder(
/// This is a workaround related to
/// https://github.com/flutter/flutter/issues/25652
cacheExtent: max(layoutConstraints.maxHeight, 500),
scrollController: widget.scrollController.verticalController,
physics: const ClampingScrollPhysics(),
buildDefaultDragHandles: false,
proxyDecorator: (child, _, __) => Provider.value(
value: context.read<DatabasePluginWidgetBuilderSize>(),
child: Material(
color: Colors.white.withOpacity(.1),
child: Opacity(opacity: .5, child: child),
),
Widget child;
if (widget.shrinkWrap) {
child = SingleChildScrollView(
scrollDirection: Axis.horizontal,
controller: widget.scrollController.horizontalController,
child: ConstrainedBox(
constraints: BoxConstraints(
maxWidth: GridLayout.headerWidth(
context.read<DatabasePluginWidgetBuilderSize>().horizontalPadding,
context.read<GridBloc>().state.fields,
),
onReorder: (fromIndex, newIndex) {
void moveRow() {
final toIndex = newIndex > fromIndex ? newIndex - 1 : newIndex;
if (fromIndex != toIndex) {
context
.read<GridBloc>()
.add(GridEvent.moveRow(fromIndex, toIndex));
}
}
if (state.sorts.isNotEmpty) {
showCancelAndDeleteDialog(
context: context,
title: LocaleKeys.grid_sort_sortsActive.tr(
namedArgs: {
'intention':
LocaleKeys.grid_row_reorderRowDescription.tr(),
},
),
description: LocaleKeys.grid_sort_removeSorting.tr(),
confirmLabel: LocaleKeys.button_remove.tr(),
closeOnAction: true,
onDelete: () {
SortBackendService(viewId: widget.viewId).deleteAllSorts();
moveRow();
},
);
} else {
moveRow();
}
},
itemCount: itemCount,
itemBuilder: (context, index) {
if (index == state.rowInfos.length) {
return const GridRowBottomBar(key: Key('grid_footer'));
}
if (index == state.rowInfos.length + 1 &&
!showFloatingCalculations) {
return GridCalculationsRow(
key: const Key('grid_calculations'),
viewId: widget.viewId,
);
}
return _renderRow(
context,
state.rowInfos[index].rowId,
index: index,
);
},
),
child: _renderList(context),
),
);
} else {
child = _WrapScrollView(
scrollController: widget.scrollController,
contentWidth: GridLayout.headerWidth(
context.read<DatabasePluginWidgetBuilderSize>().horizontalPadding,
context.read<GridBloc>().state.fields,
),
child: BlocListener<GridBloc, GridState>(
listenWhen: (previous, current) =>
previous.rowCount != current.rowCount,
listener: (context, state) => _evaluateFloatingCalculations(),
child: ScrollConfiguration(
behavior:
ScrollConfiguration.of(context).copyWith(scrollbars: false),
child: _renderList(context),
),
),
if (showFloatingCalculations) ...[
);
}
if (widget.shrinkWrap) {
return child;
}
return Flexible(child: child);
}
Widget _renderList(BuildContext context) {
final state = context.read<GridBloc>().state;
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
widget.shrinkWrap
? _reorderableListView(state)
: Expanded(child: _reorderableListView(state)),
if (showFloatingCalculations && !widget.shrinkWrap) ...[
_PositionedCalculationsRow(
viewId: widget.viewId,
isAtBottom: isAtBottom,
@ -491,6 +445,77 @@ class _GridRowsState extends State<_GridRows> {
);
}
Widget _reorderableListView(GridState state) {
final List<Widget> footer = [
const GridRowBottomBar(),
if (widget.shrinkWrap && state.visibleRows < state.rowInfos.length)
const GridRowLoadMoreButton(),
if (!showFloatingCalculations) GridCalculationsRow(viewId: widget.viewId),
];
// If we are using shrinkWrap, we need to show at most
// state.visibleRows + 1 items. The visibleRows can be larger
// than the actual rowInfos length.
final itemCount = widget.shrinkWrap
? (state.visibleRows + 1).clamp(0, state.rowInfos.length + 1)
: state.rowInfos.length + 1;
return ReorderableListView.builder(
cacheExtent: 500,
scrollController: widget.scrollController.verticalController,
physics: const ClampingScrollPhysics(),
buildDefaultDragHandles: false,
shrinkWrap: widget.shrinkWrap,
proxyDecorator: (child, _, __) => Provider.value(
value: context.read<DatabasePluginWidgetBuilderSize>(),
child: Material(
color: Colors.white.withOpacity(.1),
child: Opacity(opacity: .5, child: child),
),
),
onReorder: (fromIndex, newIndex) {
final toIndex = newIndex > fromIndex ? newIndex - 1 : newIndex;
if (state.sorts.isNotEmpty) {
showCancelAndDeleteDialog(
context: context,
title: LocaleKeys.grid_sort_sortsActive.tr(
namedArgs: {
'intention': LocaleKeys.grid_row_reorderRowDescription.tr(),
},
),
description: LocaleKeys.grid_sort_removeSorting.tr(),
confirmLabel: LocaleKeys.button_remove.tr(),
closeOnAction: true,
onDelete: () {
SortBackendService(viewId: widget.viewId).deleteAllSorts();
moveRow(fromIndex, toIndex);
},
);
} else {
moveRow(fromIndex, toIndex);
}
},
itemCount: itemCount,
itemBuilder: (context, index) {
if (index == itemCount - 1) {
return Column(
key: const Key('grid_footer'),
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: footer,
);
}
return _renderRow(
context,
state.rowInfos[index].rowId,
index: index,
);
},
);
}
Widget _renderRow(
BuildContext context,
RowId rowId, {
@ -509,6 +534,7 @@ class _GridRowsState extends State<_GridRows> {
final child = GridRow(
key: ValueKey("grid_row_$rowId"),
shrinkWrap: widget.shrinkWrap,
fieldController: databaseController.fieldController,
rowId: rowId,
viewId: viewId,
@ -523,20 +549,22 @@ class _GridRowsState extends State<_GridRows> {
context: rowDetailContext,
builder: (_) {
final rowMeta = rowCache.getRow(rowId)?.rowMeta;
return rowMeta == null
? const SizedBox.shrink()
: BlocProvider.value(
value: context.read<ViewBloc>(),
child: RowDetailPage(
rowController: RowController(
viewId: viewId,
rowMeta: rowMeta,
rowCache: rowCache,
),
databaseController: databaseController,
userProfile: context.read<GridBloc>().userProfile,
),
);
if (rowMeta == null) {
return const SizedBox.shrink();
}
return BlocProvider.value(
value: context.read<ViewBloc>(),
child: RowDetailPage(
rowController: RowController(
viewId: viewId,
rowMeta: rowMeta,
rowCache: rowCache,
),
databaseController: databaseController,
userProfile: context.read<GridBloc>().userProfile,
),
);
},
),
);
@ -547,6 +575,12 @@ class _GridRowsState extends State<_GridRows> {
return child;
}
void moveRow(int from, int to) {
if (from != to) {
context.read<GridBloc>().add(GridEvent.moveRow(from, to));
}
}
}
class _WrapScrollView extends StatelessWidget {

View File

@ -57,3 +57,44 @@ class GridRowBottomBar extends StatelessWidget {
);
}
}
class GridRowLoadMoreButton extends StatelessWidget {
const GridRowLoadMoreButton({super.key});
@override
Widget build(BuildContext context) {
final padding =
context.read<DatabasePluginWidgetBuilderSize>().horizontalPadding;
final color = Theme.of(context).brightness == Brightness.light
? const Color(0xFF171717).withOpacity(0.4)
: const Color(0xFFFFFFFF).withOpacity(0.4);
return Container(
padding: GridSize.footerContentInsets.copyWith(left: 0) +
EdgeInsets.only(left: padding),
height: GridSize.footerHeight,
child: FlowyButton(
radius: BorderRadius.zero,
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(color: AFThemeExtension.of(context).borderColor),
),
),
text: FlowyText(
lineHeight: 1.0,
LocaleKeys.grid_row_loadMore.tr(),
color: color,
),
margin: const EdgeInsets.symmetric(horizontal: 12),
hoverColor: AFThemeExtension.of(context).lightGreyHover,
onTap: () => context.read<GridBloc>().add(
const GridEvent.loadMoreRows(),
),
leftIcon: FlowySvg(
FlowySvgs.load_more_s,
color: color,
),
),
);
}
}

View File

@ -34,6 +34,7 @@ class GridRow extends StatelessWidget {
required this.cellBuilder,
required this.openDetailPage,
required this.index,
this.shrinkWrap = false,
});
final FieldController fieldController;
@ -43,9 +44,20 @@ class GridRow extends StatelessWidget {
final EditableCellBuilder cellBuilder;
final void Function(BuildContext context) openDetailPage;
final int index;
final bool shrinkWrap;
@override
Widget build(BuildContext context) {
Widget rowContent = RowContent(
fieldController: fieldController,
cellBuilder: cellBuilder,
onExpand: () => openDetailPage(context),
);
if (!shrinkWrap) {
rowContent = Expanded(child: rowContent);
}
return BlocProvider(
create: (_) => RowBloc(
fieldController: fieldController,
@ -56,17 +68,8 @@ class GridRow extends StatelessWidget {
child: _RowEnterRegion(
child: Row(
children: [
_RowLeading(
viewId: viewId,
index: index,
),
Expanded(
child: RowContent(
fieldController: fieldController,
cellBuilder: cellBuilder,
onExpand: () => openDetailPage(context),
),
),
_RowLeading(viewId: viewId, index: index),
rowContent,
],
),
),

View File

@ -55,7 +55,7 @@ abstract class DatabaseTabBarItemBuilder {
void dispose() {}
}
class DatabaseTabBarView extends StatefulWidget {
class DatabaseTabBarView extends StatelessWidget {
const DatabaseTabBarView({
super.key,
required this.view,
@ -70,108 +70,84 @@ class DatabaseTabBarView extends StatefulWidget {
///
final String? initialRowId;
@override
State<DatabaseTabBarView> createState() => _DatabaseTabBarViewState();
}
class _DatabaseTabBarViewState extends State<DatabaseTabBarView> {
final PageController _pageController = PageController();
late String? _initialRowId = widget.initialRowId;
@override
void dispose() {
_pageController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return MultiBlocProvider(
providers: [
BlocProvider<DatabaseTabBarBloc>(
create: (_) => DatabaseTabBarBloc(view: widget.view)
create: (_) => DatabaseTabBarBloc(view: view)
..add(const DatabaseTabBarEvent.initial()),
),
BlocProvider<ViewBloc>(
create: (_) =>
ViewBloc(view: widget.view)..add(const ViewEvent.initial()),
create: (_) => ViewBloc(view: view)..add(const ViewEvent.initial()),
),
],
child: MultiBlocListener(
listeners: [
BlocListener<DatabaseTabBarBloc, DatabaseTabBarState>(
listenWhen: (p, c) => p.selectedIndex != c.selectedIndex,
listener: (_, state) {
_initialRowId = null;
_pageController.jumpToPage(state.selectedIndex);
},
),
],
child: Column(
children: [
if (UniversalPlatform.isMobile) const VSpace(12),
BlocBuilder<DatabaseTabBarBloc, DatabaseTabBarState>(
builder: (context, state) {
return ValueListenableBuilder<bool>(
valueListenable: state
.tabBarControllerByViewId[state.parentView.id]!
.controller
.isLoading,
builder: (_, value, ___) {
if (value) {
return const SizedBox.shrink();
}
child: BlocBuilder<DatabaseTabBarBloc, DatabaseTabBarState>(
builder: (_, state) {
final layout = state.tabBars[state.selectedIndex].layout;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (UniversalPlatform.isMobile) const VSpace(12),
ValueListenableBuilder<bool>(
valueListenable: state
.tabBarControllerByViewId[state.parentView.id]!
.controller
.isLoading,
builder: (_, value, ___) {
if (value) {
return const SizedBox.shrink();
}
return UniversalPlatform.isDesktop
? const TabBarHeader()
: const MobileTabBarHeader();
},
);
},
),
BlocBuilder<DatabaseTabBarBloc, DatabaseTabBarState>(
builder: (context, state) =>
pageSettingBarExtensionFromState(state),
),
BlocBuilder<DatabaseTabBarBloc, DatabaseTabBarState>(
builder: (context, state) {
final content = PageView(
controller: _pageController,
pageSnapping: false,
physics: const NeverScrollableScrollPhysics(),
children: pageContentFromState(state),
);
if (widget.shrinkWrap) {
final layout = state.tabBars[state.selectedIndex].layout;
return SizedBox(height: layout.pluginHeight, child: content);
}
return Expanded(child: content);
},
),
],
),
return UniversalPlatform.isDesktop
? const TabBarHeader()
: const MobileTabBarHeader();
},
),
pageSettingBarExtensionFromState(context, state),
wrapContent(
layout: layout,
child: pageContentFromState(context, state),
),
],
);
},
),
);
}
List<Widget> pageContentFromState(DatabaseTabBarState state) {
return state.tabBars.map((tabBar) {
final controller =
state.tabBarControllerByViewId[tabBar.viewId]!.controller;
Widget wrapContent({required ViewLayoutPB layout, required Widget child}) {
if (shrinkWrap) {
if (layout.shrinkWrappable) {
return child;
}
return tabBar.builder.content(
context,
tabBar.view,
controller,
widget.shrinkWrap,
_initialRowId,
return SizedBox(
height: layout.pluginHeight,
child: child,
);
}).toList();
}
return Expanded(child: child);
}
Widget pageSettingBarExtensionFromState(DatabaseTabBarState state) {
Widget pageContentFromState(BuildContext context, DatabaseTabBarState state) {
final tab = state.tabBars[state.selectedIndex];
final controller = state.tabBarControllerByViewId[tab.viewId]!.controller;
return tab.builder.content(
context,
tab.view,
controller,
shrinkWrap,
initialRowId,
);
}
Widget pageSettingBarExtensionFromState(
BuildContext context,
DatabaseTabBarState state,
) {
if (state.tabBars.length < state.selectedIndex) {
return const SizedBox.shrink();
}

View File

@ -75,15 +75,16 @@ class _BuiltInPageWidgetState extends State<BuiltInPageWidget> {
);
}
Widget _build(BuildContext context, ViewPB view) {
Widget _build(BuildContext context, ViewPB viewPB) {
return MouseRegion(
onEnter: (_) => widget.editorState.service.scrollService?.disable(),
onExit: (_) => widget.editorState.service.scrollService?.enable(),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildMenu(context, view),
_buildPage(context, view),
_buildMenu(context, viewPB),
Flexible(child: _buildPage(context, viewPB)),
],
),
);

View File

@ -71,12 +71,7 @@ class _DatabaseBlockComponentWidgetState
Widget child = BuiltInPageWidget(
node: widget.node,
editorState: editorState,
builder: (viewPB) {
return DatabaseViewWidget(
key: ValueKey(viewPB.id),
view: viewPB,
);
},
builder: (view) => DatabaseViewWidget(key: ValueKey(view.id), view: view),
);
child = Padding(

View File

@ -325,13 +325,15 @@ extension ViewLayoutExtension on ViewLayoutPB {
_ => LocaleKeys.menuAppHeader_defaultNewPageName.tr(),
};
bool get shrinkWrappable => switch (this) {
ViewLayoutPB.Grid => true,
_ => false,
};
double get pluginHeight => switch (this) {
ViewLayoutPB.Grid ||
ViewLayoutPB.Board ||
ViewLayoutPB.Document ||
ViewLayoutPB.Chat =>
450,
ViewLayoutPB.Document || ViewLayoutPB.Board || ViewLayoutPB.Chat => 450,
ViewLayoutPB.Calendar => 650,
ViewLayoutPB.Grid => double.infinity,
_ => throw UnimplementedError(),
};
}

View File

@ -0,0 +1 @@
<svg viewBox="-0.5 -0.5 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" id="Arrow-Down--Streamline-Solar-Ar.svg" height="16" width="16"><desc>Arrow Down Streamline Icon: https://streamlinehq.com</desc><path d="m7.5 2.5 0 10m0 0 3.75 -3.75m-3.75 3.75 -3.75 -3.75" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"></path></svg>

After

Width:  |  Height:  |  Size: 363 B

View File

@ -1513,6 +1513,7 @@
"copyProperty": "Copied property to clipboard",
"count": "Count",
"newRow": "New row",
"loadMore": "Load more",
"action": "Action",
"add": "Click add to below",
"drag": "Drag to move",