fix: launch review 0.5.1 (#4855)

* feat: floating calculations row

* fix: calculate cell overflow + tooltip

* fix: empty text cell should be counted as empty

* fix: empty text cell sohuld not be counted as not empty

* fix: conversion of some field types for calculations

* fix: tooltip + size of duplicate event button

* fix: minor view meta info changes

* fix: apply number format on word/char count

* fix: dart format

* fix: hide arrow for calc values

---------

Co-authored-by: Richard Shiue <71320345+richardshiue@users.noreply.github.com>
This commit is contained in:
Mathias Mogensen 2024-03-11 10:41:51 +01:00 committed by GitHub
parent 7afae69fe4
commit c48001bd74
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 403 additions and 231 deletions

View File

@ -20,6 +20,7 @@ import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.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:flowy_infra_ui/widget/flowy_tooltip.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
class CalendarEventEditor extends StatelessWidget { class CalendarEventEditor extends StatelessWidget {
@ -86,16 +87,22 @@ class EventEditorControls extends StatelessWidget {
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.end,
children: [ children: [
FlowyIconButton( FlowyTooltip(
width: 20, message: LocaleKeys.calendar_duplicateEvent.tr(),
icon: const FlowySvg(FlowySvgs.m_duplicate_s), child: FlowyIconButton(
iconColorOnHover: Theme.of(context).colorScheme.onSecondary, width: 20,
onPressed: () => context.read<CalendarBloc>().add( icon: const FlowySvg(
CalendarEvent.duplicateEvent( FlowySvgs.m_duplicate_s,
rowController.viewId, size: Size.square(17),
rowController.rowId, ),
iconColorOnHover: Theme.of(context).colorScheme.onSecondary,
onPressed: () => context.read<CalendarBloc>().add(
CalendarEvent.duplicateEvent(
rowController.viewId,
rowController.rowId,
),
), ),
), ),
), ),
const HSpace(8.0), const HSpace(8.0),
FlowyIconButton( FlowyIconButton(

View File

@ -6,9 +6,15 @@ extension AvailableCalculations on FieldType {
CalculationType.Count, CalculationType.Count,
]; ];
// These FieldTypes cannot be empty, no need to count empty/non-empty // These FieldTypes cannot be empty, or might hold secondary
if (![FieldType.Checkbox, FieldType.LastEditedTime, FieldType.CreatedTime] // data causing them to be seen as not empty when in fact they
.contains(this)) { // are empty.
if (![
FieldType.URL,
FieldType.Checkbox,
FieldType.LastEditedTime,
FieldType.CreatedTime,
].contains(this)) {
calculationTypes.addAll([ calculationTypes.addAll([
CalculationType.CountEmpty, CalculationType.CountEmpty,
CalculationType.CountNonEmpty, CalculationType.CountNonEmpty,

View File

@ -19,7 +19,6 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:linked_scroll_controller/linked_scroll_controller.dart'; import 'package:linked_scroll_controller/linked_scroll_controller.dart';
import '../../application/database_controller.dart'; import '../../application/database_controller.dart';
import '../../application/row/row_cache.dart';
import '../../application/row/row_controller.dart'; import '../../application/row/row_controller.dart';
import '../../tab_bar/tab_bar_view.dart'; import '../../tab_bar/tab_bar_view.dart';
import '../../widgets/row/row_detail.dart'; import '../../widgets/row/row_detail.dart';
@ -259,7 +258,7 @@ class _GridHeader extends StatelessWidget {
} }
} }
class _GridRows extends StatelessWidget { class _GridRows extends StatefulWidget {
const _GridRows({ const _GridRows({
required this.viewId, required this.viewId,
required this.scrollController, required this.scrollController,
@ -268,6 +267,30 @@ class _GridRows extends StatelessWidget {
final String viewId; final String viewId;
final GridScrollController scrollController; final GridScrollController scrollController;
@override
State<_GridRows> createState() => _GridRowsState();
}
class _GridRowsState extends State<_GridRows> {
bool showFloatingCalculations = false;
@override
void initState() {
super.initState();
_evaluateFloatingCalculations();
}
void _evaluateFloatingCalculations() {
WidgetsBinding.instance.addPostFrameCallback((_) {
setState(() {
// maxScrollExtent is 0.0 if scrolling is not possible
showFloatingCalculations = widget
.scrollController.verticalController.position.maxScrollExtent >
0;
});
});
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocBuilder<GridBloc, GridState>( return BlocBuilder<GridBloc, GridState>(
@ -275,24 +298,18 @@ class _GridRows extends StatelessWidget {
builder: (context, state) { builder: (context, state) {
return Flexible( return Flexible(
child: _WrapScrollView( child: _WrapScrollView(
scrollController: scrollController, scrollController: widget.scrollController,
contentWidth: GridLayout.headerWidth(state.fields), contentWidth: GridLayout.headerWidth(state.fields),
child: BlocBuilder<GridBloc, GridState>( child: BlocConsumer<GridBloc, GridState>(
buildWhen: (previous, current) => current.reason.maybeWhen( listenWhen: (previous, current) =>
reorderRows: () => true, previous.rowCount != current.rowCount,
reorderSingleRow: (reorderRow, rowInfo) => true, listener: (context, state) => _evaluateFloatingCalculations(),
delete: (item) => true,
insert: (item) => true,
orElse: () => true,
),
builder: (context, state) { builder: (context, state) {
final rowInfos = state.rowInfos;
final behavior = ScrollConfiguration.of(context).copyWith(
scrollbars: false,
);
return ScrollConfiguration( return ScrollConfiguration(
behavior: behavior, behavior: ScrollConfiguration.of(context).copyWith(
child: _renderList(context, state, rowInfos), scrollbars: false,
),
child: _renderList(context, state),
); );
}, },
), ),
@ -305,9 +322,8 @@ class _GridRows extends StatelessWidget {
Widget _renderList( Widget _renderList(
BuildContext context, BuildContext context,
GridState state, GridState state,
List<RowInfo> rowInfos,
) { ) {
final children = rowInfos.mapIndexed((index, rowInfo) { final children = state.rowInfos.mapIndexed((index, rowInfo) {
return _renderRow( return _renderRow(
context, context,
rowInfo.rowId, rowInfo.rowId,
@ -315,32 +331,56 @@ class _GridRows extends StatelessWidget {
index: index, index: index,
); );
}).toList() }).toList()
..add(const GridRowBottomBar(key: Key('grid_footer'))) ..add(const GridRowBottomBar(key: Key('grid_footer')));
..add(
GridCalculationsRow( if (showFloatingCalculations) {
key: const Key('grid_calculations'), children.add(
viewId: viewId, const SizedBox(
key: Key('calculations_bottom_padding'),
height: 36,
), ),
); );
} else {
children.add(
GridCalculationsRow(
key: const Key('grid_calculations'),
viewId: widget.viewId,
),
);
}
return ReorderableListView.builder( children.add(const SizedBox(key: Key('footer_padding'), height: 10));
/// This is a workaround related to
/// https://github.com/flutter/flutter/issues/25652 return Stack(
cacheExtent: 5000, children: [
scrollController: scrollController.verticalController, Positioned.fill(
buildDefaultDragHandles: false, child: ReorderableListView.builder(
proxyDecorator: (child, index, animation) => Material( /// This is a workaround related to
color: Colors.white.withOpacity(.1), /// https://github.com/flutter/flutter/issues/25652
child: Opacity(opacity: .5, child: child), cacheExtent: 5000,
), scrollController: widget.scrollController.verticalController,
onReorder: (fromIndex, newIndex) { physics: const ClampingScrollPhysics(),
final toIndex = newIndex > fromIndex ? newIndex - 1 : newIndex; buildDefaultDragHandles: false,
if (fromIndex != toIndex) { proxyDecorator: (child, index, animation) => Material(
context.read<GridBloc>().add(GridEvent.moveRow(fromIndex, toIndex)); color: Colors.white.withOpacity(.1),
} child: Opacity(opacity: .5, child: child),
}, ),
itemCount: children.length, onReorder: (fromIndex, newIndex) {
itemBuilder: (context, index) => children[index], final toIndex = newIndex > fromIndex ? newIndex - 1 : newIndex;
if (fromIndex != toIndex) {
context
.read<GridBloc>()
.add(GridEvent.moveRow(fromIndex, toIndex));
}
},
itemCount: children.length,
itemBuilder: (context, index) => children[index],
),
),
if (showFloatingCalculations) ...[
_PositionedCalculationsRow(viewId: widget.viewId),
],
],
); );
} }
@ -378,21 +418,16 @@ class _GridRows extends StatelessWidget {
openDetailPage: (context, cellBuilder) { openDetailPage: (context, cellBuilder) {
FlowyOverlay.show( FlowyOverlay.show(
context: context, context: context,
builder: (BuildContext context) { builder: (_) => RowDetailPage(
return RowDetailPage( rowController: rowController,
rowController: rowController, databaseController: databaseController,
databaseController: databaseController, ),
);
},
); );
}, },
); );
if (animation != null) { if (animation != null) {
return SizeTransition( return SizeTransition(sizeFactor: animation, child: child);
sizeFactor: animation,
child: child,
);
} }
return child; return child;
@ -413,12 +448,14 @@ class _WrapScrollView extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ScrollbarListStack( return ScrollbarListStack(
includeInsets: false,
axis: Axis.vertical, axis: Axis.vertical,
controller: scrollController.verticalController, controller: scrollController.verticalController,
barSize: GridSize.scrollBarSize, barSize: GridSize.scrollBarSize,
autoHideScrollbar: false, autoHideScrollbar: false,
child: StyledSingleChildScrollView( child: StyledSingleChildScrollView(
autoHideScrollbar: false, autoHideScrollbar: false,
includeInsets: false,
controller: scrollController.horizontalController, controller: scrollController.horizontalController,
axis: Axis.horizontal, axis: Axis.horizontal,
child: SizedBox( child: SizedBox(
@ -429,3 +466,49 @@ class _WrapScrollView extends StatelessWidget {
); );
} }
} }
/// This Widget is used to show the Calculations Row at the bottom of the Grids ScrollView
/// when the ScrollView is scrollable.
///
class _PositionedCalculationsRow extends StatefulWidget {
const _PositionedCalculationsRow({
required this.viewId,
});
final String viewId;
@override
State<_PositionedCalculationsRow> createState() =>
_PositionedCalculationsRowState();
}
class _PositionedCalculationsRowState
extends State<_PositionedCalculationsRow> {
@override
Widget build(BuildContext context) {
return Positioned(
bottom: 0,
left: 0,
right: 0,
child: Container(
margin: EdgeInsets.only(left: GridSize.horizontalHeaderPadding),
padding: const EdgeInsets.only(bottom: 10),
decoration: BoxDecoration(
color: Theme.of(context).canvasColor,
border: Border(
top: BorderSide(color: Theme.of(context).dividerColor),
),
),
child: SizedBox(
height: 36,
width: double.infinity,
child: GridCalculationsRow(
key: const Key('floating_grid_calculations'),
viewId: widget.viewId,
includeDefaultInsets: false,
),
),
),
);
}
}

View File

@ -1,6 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/plugins/database/application/calculations/calculation_type_ext.dart'; import 'package:appflowy/plugins/database/application/calculations/calculation_type_ext.dart';
import 'package:appflowy/plugins/database/application/field/field_info.dart'; import 'package:appflowy/plugins/database/application/field/field_info.dart';
import 'package:appflowy/plugins/database/application/field/type_option/number_format_bloc.dart'; import 'package:appflowy/plugins/database/application/field/type_option/number_format_bloc.dart';
@ -16,6 +15,7 @@ import 'package:appflowy_backend/protobuf/flowy-database2/number_entities.pb.dar
import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra/theme_extension.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:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
class CalculateCell extends StatefulWidget { class CalculateCell extends StatefulWidget {
@ -35,7 +35,38 @@ class CalculateCell extends StatefulWidget {
} }
class _CalculateCellState extends State<CalculateCell> { class _CalculateCellState extends State<CalculateCell> {
final _cellScrollController = ScrollController();
bool isSelected = false; bool isSelected = false;
bool isScrollable = false;
@override
void initState() {
super.initState();
_checkScrollable();
}
@override
void didUpdateWidget(covariant CalculateCell oldWidget) {
_checkScrollable();
super.didUpdateWidget(oldWidget);
}
void _checkScrollable() {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (_cellScrollController.hasClients) {
setState(
() =>
isScrollable = _cellScrollController.position.maxScrollExtent > 0,
);
}
});
}
@override
void dispose() {
_cellScrollController.dispose();
super.dispose();
}
void setIsSelected(bool selected) => setState(() => isSelected = selected); void setIsSelected(bool selected) => setState(() => isSelected = selected);
@ -98,36 +129,62 @@ class _CalculateCellState extends State<CalculateCell> {
Widget _showCalculateValue(BuildContext context, String? prefix) { Widget _showCalculateValue(BuildContext context, String? prefix) {
prefix = prefix != null ? '$prefix ' : ''; prefix = prefix != null ? '$prefix ' : '';
final calculateValue =
'$prefix${_withoutTrailingZeros(widget.calculation!.value)}';
return FlowyButton( return FlowyTooltip(
radius: BorderRadius.zero, message: !isScrollable ? "" : null,
hoverColor: AFThemeExtension.of(context).lightGreyHover, richMessage: !isScrollable
text: Row( ? null
mainAxisAlignment: MainAxisAlignment.end, : TextSpan(
children: [ children: [
Flexible( TextSpan(
child: FlowyText( text: widget.calculation!.calculationType.shortLabel
widget.calculation!.calculationType.shortLabel, .toUpperCase(),
color: Theme.of(context).hintColor, ),
overflow: TextOverflow.ellipsis, const TextSpan(text: ' '),
TextSpan(
text: calculateValue,
style: const TextStyle(fontWeight: FontWeight.w500),
),
],
), ),
), child: FlowyButton(
if (widget.calculation!.value.isNotEmpty) ...[ radius: BorderRadius.zero,
const HSpace(8), hoverColor: AFThemeExtension.of(context).lightGreyHover,
Flexible( text: Row(
child: FlowyText( children: [
'$prefix${_withoutTrailingZeros(widget.calculation!.value)}', Expanded(
color: AFThemeExtension.of(context).textColor, child: SingleChildScrollView(
overflow: TextOverflow.ellipsis, controller: _cellScrollController,
key: ValueKey(widget.calculation!.id),
reverse: true,
physics: const NeverScrollableScrollPhysics(),
scrollDirection: Axis.horizontal,
child: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.end,
children: [
FlowyText(
widget.calculation!.calculationType.shortLabel
.toUpperCase(),
color: Theme.of(context).hintColor,
fontSize: 10,
),
if (widget.calculation!.value.isNotEmpty) ...[
const HSpace(8),
FlowyText(
calculateValue,
color: AFThemeExtension.of(context).textColor,
overflow: TextOverflow.ellipsis,
),
],
],
),
), ),
), ),
], ],
const HSpace(8), ),
FlowySvg(
FlowySvgs.arrow_down_s,
color: Theme.of(context).hintColor,
),
],
), ),
); );
} }

View File

@ -7,9 +7,14 @@ import 'package:appflowy/plugins/database/grid/presentation/widgets/calculations
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
class GridCalculationsRow extends StatelessWidget { class GridCalculationsRow extends StatelessWidget {
const GridCalculationsRow({super.key, required this.viewId}); const GridCalculationsRow({
super.key,
required this.viewId,
this.includeDefaultInsets = true,
});
final String viewId; final String viewId;
final bool includeDefaultInsets;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -23,7 +28,8 @@ class GridCalculationsRow extends StatelessWidget {
child: BlocBuilder<CalculationsBloc, CalculationsState>( child: BlocBuilder<CalculationsBloc, CalculationsState>(
builder: (context, state) { builder: (context, state) {
return Padding( return Padding(
padding: GridSize.contentInsets, padding:
includeDefaultInsets ? GridSize.contentInsets : EdgeInsets.zero,
child: Row( child: Row(
children: [ children: [
...state.fields.map( ...state.fields.map(

View File

@ -66,7 +66,7 @@ class _MoreViewActionsState extends State<MoreViewActions> {
builder: (context, state) { builder: (context, state) {
return AppFlowyPopover( return AppFlowyPopover(
mutex: popoverMutex, mutex: popoverMutex,
constraints: BoxConstraints.loose(const Size(210, 400)), constraints: BoxConstraints.loose(const Size(215, 400)),
offset: const Offset(0, 30), offset: const Offset(0, 30),
popupBuilder: (_) { popupBuilder: (_) {
final actions = [ final actions = [

View File

@ -24,6 +24,8 @@ class ViewMetaInfo extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final numberFormat = NumberFormat();
// If more info is added to this Widget, use a separated ListView // If more info is added to this Widget, use a separated ListView
return Padding( return Padding(
padding: const EdgeInsets.symmetric(horizontal: 6), padding: const EdgeInsets.symmetric(horizontal: 6),
@ -33,15 +35,21 @@ class ViewMetaInfo extends StatelessWidget {
if (documentCounters != null) ...[ if (documentCounters != null) ...[
FlowyText.regular( FlowyText.regular(
LocaleKeys.moreAction_wordCount.tr( LocaleKeys.moreAction_wordCount.tr(
args: [documentCounters!.wordCount.toString()], args: [
numberFormat.format(documentCounters!.wordCount).toString(),
],
), ),
fontSize: 11,
color: Theme.of(context).hintColor, color: Theme.of(context).hintColor,
), ),
const VSpace(2), const VSpace(2),
FlowyText.regular( FlowyText.regular(
LocaleKeys.moreAction_charCount.tr( LocaleKeys.moreAction_charCount.tr(
args: [documentCounters!.charCount.toString()], args: [
numberFormat.format(documentCounters!.charCount).toString(),
],
), ),
fontSize: 11,
color: Theme.of(context).hintColor, color: Theme.of(context).hintColor,
), ),
], ],
@ -51,6 +59,7 @@ class ViewMetaInfo extends StatelessWidget {
LocaleKeys.moreAction_createdAt.tr( LocaleKeys.moreAction_createdAt.tr(
args: [dateFormat.formatDate(createdAt!, true, timeFormat)], args: [dateFormat.formatDate(createdAt!, true, timeFormat)],
), ),
fontSize: 11,
maxLines: 2, maxLines: 2,
color: Theme.of(context).hintColor, color: Theme.of(context).hintColor,
), ),

View File

@ -1,13 +1,28 @@
import 'dart:math';
import 'dart:async'; import 'dart:async';
import 'package:async/async.dart'; import 'dart:math';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra/size.dart';
import 'package:flowy_infra_ui/widget/mouse_hover_builder.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:async/async.dart';
import 'package:flowy_infra/size.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/widget/mouse_hover_builder.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
class StyledScrollbar extends StatefulWidget { class StyledScrollbar extends StatefulWidget {
const StyledScrollbar({
super.key,
this.size,
required this.axis,
required this.controller,
this.onDrag,
this.contentSize,
this.showTrack = false,
this.autoHideScrollbar = true,
this.handleColor,
this.trackColor,
});
final double? size; final double? size;
final Axis axis; final Axis axis;
final ScrollController controller; final ScrollController controller;
@ -22,18 +37,6 @@ class StyledScrollbar extends StatefulWidget {
// https://stackoverflow.com/questions/60855712/flutter-how-to-force-scrollcontroller-to-recalculate-position-maxextents // https://stackoverflow.com/questions/60855712/flutter-how-to-force-scrollcontroller-to-recalculate-position-maxextents
final double? contentSize; final double? contentSize;
const StyledScrollbar(
{super.key,
this.size,
required this.axis,
required this.controller,
this.onDrag,
this.contentSize,
this.showTrack = false,
this.autoHideScrollbar = true,
this.handleColor,
this.trackColor});
@override @override
ScrollbarState createState() => ScrollbarState(); ScrollbarState createState() => ScrollbarState();
} }
@ -46,25 +49,29 @@ class ScrollbarState extends State<StyledScrollbar> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
widget.controller.addListener(() => setState(() {})); widget.controller.addListener(_onScrollChanged);
widget.controller.position.isScrollingNotifier
widget.controller.position.isScrollingNotifier.addListener( .addListener(_hideScrollbarInTime);
_hideScrollbarInTime,
);
} }
@override @override
void didUpdateWidget(StyledScrollbar oldWidget) { void dispose() {
if (oldWidget.contentSize != widget.contentSize) setState(() {}); if (widget.controller.hasClients) {
super.didUpdateWidget(oldWidget); widget.controller.removeListener(_onScrollChanged);
widget.controller.position.isScrollingNotifier
.removeListener(_hideScrollbarInTime);
}
super.dispose();
} }
void _onScrollChanged() => setState(() {});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return LayoutBuilder( return LayoutBuilder(
builder: (_, BoxConstraints constraints) { builder: (_, BoxConstraints constraints) {
double maxExtent; double maxExtent;
final contentSize = widget.contentSize; final double? contentSize = widget.contentSize;
switch (widget.axis) { switch (widget.axis) {
case Axis.vertical: case Axis.vertical:
@ -109,11 +116,9 @@ class ScrollbarState extends State<StyledScrollbar> {
} }
// Hide the handle if content is < the viewExtent // Hide the handle if content is < the viewExtent
var showHandle = contentExtent > _viewExtent && contentExtent > 0; var showHandle = hideHandler
? false
if (hideHandler) { : contentExtent > _viewExtent && contentExtent > 0;
showHandle = false;
}
// Handle color // Handle color
var handleColor = widget.handleColor ?? var handleColor = widget.handleColor ??
@ -184,7 +189,7 @@ class ScrollbarState extends State<StyledScrollbar> {
if (!widget.controller.position.isScrollingNotifier.value) { if (!widget.controller.position.isScrollingNotifier.value) {
_hideScrollbarOperation = CancelableOperation.fromFuture( _hideScrollbarOperation = CancelableOperation.fromFuture(
Future.delayed(const Duration(seconds: 2), () {}), Future.delayed(const Duration(seconds: 2)),
).then((_) { ).then((_) {
hideHandler = true; hideHandler = true;
if (mounted) { if (mounted) {
@ -216,17 +221,6 @@ class ScrollbarState extends State<StyledScrollbar> {
} }
class ScrollbarListStack extends StatelessWidget { class ScrollbarListStack extends StatelessWidget {
final double barSize;
final Axis axis;
final Widget child;
final ScrollController controller;
final double? contentSize;
final EdgeInsets? scrollbarPadding;
final Color? handleColor;
final Color? trackColor;
final bool showTrack;
final bool autoHideScrollbar;
const ScrollbarListStack({ const ScrollbarListStack({
super.key, super.key,
required this.barSize, required this.barSize,
@ -239,20 +233,37 @@ class ScrollbarListStack extends StatelessWidget {
this.autoHideScrollbar = true, this.autoHideScrollbar = true,
this.trackColor, this.trackColor,
this.showTrack = false, this.showTrack = false,
this.includeInsets = true,
}); });
final double barSize;
final Axis axis;
final Widget child;
final ScrollController controller;
final double? contentSize;
final EdgeInsets? scrollbarPadding;
final Color? handleColor;
final Color? trackColor;
final bool showTrack;
final bool autoHideScrollbar;
final bool includeInsets;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Stack( return Stack(
children: [ children: [
/// LIST /// Wrap with a bit of padding on the right or bottom to make room for the scrollbar
/// Wrap with a bit of padding on the right Padding(
child.padding( padding: !includeInsets
right: axis == Axis.vertical ? barSize + Insets.m : 0, ? EdgeInsets.zero
bottom: axis == Axis.horizontal ? barSize + Insets.m : 0, : EdgeInsets.only(
right: axis == Axis.vertical ? barSize + Insets.m : 0,
bottom: axis == Axis.horizontal ? barSize + Insets.m : 0,
),
child: child,
), ),
/// SCROLLBAR /// Display the scrollbar
Padding( Padding(
padding: scrollbarPadding ?? EdgeInsets.zero, padding: scrollbarPadding ?? EdgeInsets.zero,
child: StyledScrollbar( child: StyledScrollbar(
@ -266,7 +277,7 @@ class ScrollbarListStack extends StatelessWidget {
showTrack: showTrack, showTrack: showTrack,
), ),
) )
// The animate will be used by the children that using styled_widget. // The animate will be used by the children that are using styled_widget.
.animate(const Duration(milliseconds: 250), Curves.easeOut), .animate(const Duration(milliseconds: 250), Curves.easeOut),
], ],
); );

View File

@ -4,17 +4,6 @@ import 'styled_list.dart';
import 'styled_scroll_bar.dart'; import 'styled_scroll_bar.dart';
class StyledSingleChildScrollView extends StatefulWidget { class StyledSingleChildScrollView extends StatefulWidget {
final double? contentSize;
final Axis axis;
final Color? trackColor;
final Color? handleColor;
final ScrollController? controller;
final EdgeInsets? scrollbarPadding;
final double barSize;
final bool autoHideScrollbar;
final Widget? child;
const StyledSingleChildScrollView({ const StyledSingleChildScrollView({
super.key, super.key,
required this.child, required this.child,
@ -26,8 +15,20 @@ class StyledSingleChildScrollView extends StatefulWidget {
this.scrollbarPadding, this.scrollbarPadding,
this.barSize = 8, this.barSize = 8,
this.autoHideScrollbar = true, this.autoHideScrollbar = true,
this.includeInsets = true,
}); });
final Widget? child;
final double? contentSize;
final Axis axis;
final Color? trackColor;
final Color? handleColor;
final ScrollController? controller;
final EdgeInsets? scrollbarPadding;
final double barSize;
final bool autoHideScrollbar;
final bool includeInsets;
@override @override
State<StyledSingleChildScrollView> createState() => State<StyledSingleChildScrollView> createState() =>
StyledSingleChildScrollViewState(); StyledSingleChildScrollViewState();
@ -35,13 +36,8 @@ class StyledSingleChildScrollView extends StatefulWidget {
class StyledSingleChildScrollViewState class StyledSingleChildScrollViewState
extends State<StyledSingleChildScrollView> { extends State<StyledSingleChildScrollView> {
late ScrollController scrollController; late final ScrollController scrollController =
widget.controller ?? ScrollController();
@override
void initState() {
scrollController = widget.controller ?? ScrollController();
super.initState();
}
@override @override
void dispose() { void dispose() {
@ -51,14 +47,6 @@ class StyledSingleChildScrollViewState
super.dispose(); super.dispose();
} }
@override
void didUpdateWidget(StyledSingleChildScrollView oldWidget) {
if (oldWidget.child != widget.child) {
setState(() {});
}
super.didUpdateWidget(oldWidget);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ScrollbarListStack( return ScrollbarListStack(
@ -70,6 +58,7 @@ class StyledSingleChildScrollViewState
barSize: widget.barSize, barSize: widget.barSize,
trackColor: widget.trackColor, trackColor: widget.trackColor,
handleColor: widget.handleColor, handleColor: widget.handleColor,
includeInsets: widget.includeInsets,
child: SingleChildScrollView( child: SingleChildScrollView(
scrollDirection: widget.axis, scrollDirection: widget.axis,
physics: StyledScrollPhysics(), physics: StyledScrollPhysics(),
@ -81,13 +70,6 @@ class StyledSingleChildScrollViewState
} }
class StyledCustomScrollView extends StatefulWidget { class StyledCustomScrollView extends StatefulWidget {
final Axis axis;
final Color? trackColor;
final Color? handleColor;
final ScrollController? verticalController;
final List<Widget> slivers;
final double barSize;
const StyledCustomScrollView({ const StyledCustomScrollView({
super.key, super.key,
this.axis = Axis.vertical, this.axis = Axis.vertical,
@ -98,32 +80,20 @@ class StyledCustomScrollView extends StatefulWidget {
this.barSize = 8, this.barSize = 8,
}); });
final Axis axis;
final Color? trackColor;
final Color? handleColor;
final ScrollController? verticalController;
final List<Widget> slivers;
final double barSize;
@override @override
StyledCustomScrollViewState createState() => StyledCustomScrollViewState(); StyledCustomScrollViewState createState() => StyledCustomScrollViewState();
} }
class StyledCustomScrollViewState extends State<StyledCustomScrollView> { class StyledCustomScrollViewState extends State<StyledCustomScrollView> {
late ScrollController controller; late final ScrollController controller =
widget.verticalController ?? ScrollController();
@override
void initState() {
controller = widget.verticalController ?? ScrollController();
super.initState();
}
@override
void dispose() {
super.dispose();
}
@override
void didUpdateWidget(StyledCustomScrollView oldWidget) {
if (oldWidget.slivers != widget.slivers) {
setState(() {});
}
super.didUpdateWidget(oldWidget);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {

View File

@ -93,7 +93,7 @@
"moreOptions": "More options", "moreOptions": "More options",
"wordCount": "Word count: {}", "wordCount": "Word count: {}",
"charCount": "Character count: {}", "charCount": "Character count: {}",
"createdAt": "Created at: {}", "createdAt": "Created: {}",
"deleteView": "Delete", "deleteView": "Delete",
"duplicateView": "Duplicate" "duplicateView": "Duplicate"
}, },
@ -743,9 +743,9 @@
"sum": "Sum", "sum": "Sum",
"count": "Count", "count": "Count",
"countEmpty": "Count empty", "countEmpty": "Count empty",
"countEmptyShort": "Empty", "countEmptyShort": "EMPTY",
"countNonEmpty": "Count non empty", "countNonEmpty": "Count not empty",
"countNonEmptyShort": "Not empty" "countNonEmptyShort": "FILLED"
} }
}, },
"document": { "document": {
@ -1051,7 +1051,8 @@
"name": "Calendar settings" "name": "Calendar settings"
}, },
"referencedCalendarPrefix": "View of", "referencedCalendarPrefix": "View of",
"quickJumpYear": "Jump to" "quickJumpYear": "Jump to",
"duplicateEvent": "Duplicate event"
}, },
"errorDialog": { "errorDialog": {
"title": "AppFlowy Error", "title": "AppFlowy Error",

View File

@ -116,8 +116,13 @@ impl CalculationType {
| CalculationType::Sum => { | CalculationType::Sum => {
matches!(field_type, FieldType::Number) matches!(field_type, FieldType::Number)
}, },
// Exclude some fields from CountNotEmpty & CountEmpty
CalculationType::CountEmpty | CalculationType::CountNonEmpty => !matches!(
field_type,
FieldType::URL | FieldType::Checkbox | FieldType::CreatedTime | FieldType::LastEditedTime
),
// All fields // All fields
CalculationType::Count | CalculationType::CountEmpty | CalculationType::CountNonEmpty => true, CalculationType::Count => true,
} }
} }
} }

View File

@ -27,8 +27,8 @@ impl CalculationsService {
CalculationType::Min => self.calculate_min(field, row_cells), CalculationType::Min => self.calculate_min(field, row_cells),
CalculationType::Sum => self.calculate_sum(field, row_cells), CalculationType::Sum => self.calculate_sum(field, row_cells),
CalculationType::Count => self.calculate_count(row_cells), CalculationType::Count => self.calculate_count(row_cells),
CalculationType::CountEmpty => self.calculate_count_empty(row_cells), CalculationType::CountEmpty => self.calculate_count_empty(field, row_cells),
CalculationType::CountNonEmpty => self.calculate_count_non_empty(row_cells), CalculationType::CountNonEmpty => self.calculate_count_non_empty(field, row_cells),
} }
} }
@ -129,34 +129,51 @@ impl CalculationsService {
} }
} }
fn calculate_count_empty(&self, row_cells: Vec<Arc<RowCell>>) -> String { fn calculate_count_empty(&self, field: &Field, row_cells: Vec<Arc<RowCell>>) -> String {
if !row_cells.is_empty() { let field_type = FieldType::from(field.field_type);
format!( if let Some(handler) = TypeOptionCellExt::new_with_cell_data_cache(field, None)
"{}", .get_type_option_cell_data_handler(&field_type)
row_cells {
.iter() if !row_cells.is_empty() {
.filter(|c| c.is_none()) return format!(
.collect::<Vec<_>>() "{}",
.len() row_cells
) .iter()
} else { .filter(|c| c.is_none()
String::new() || handler
.handle_stringify_cell(&c.cell.clone().unwrap_or_default(), &field_type, field)
.is_empty())
.collect::<Vec<_>>()
.len()
);
}
} }
String::new()
} }
fn calculate_count_non_empty(&self, row_cells: Vec<Arc<RowCell>>) -> String { fn calculate_count_non_empty(&self, field: &Field, row_cells: Vec<Arc<RowCell>>) -> String {
if !row_cells.is_empty() { let field_type = FieldType::from(field.field_type);
format!( if let Some(handler) = TypeOptionCellExt::new_with_cell_data_cache(field, None)
"{}", .get_type_option_cell_data_handler(&field_type)
row_cells {
.iter() if !row_cells.is_empty() {
.filter(|c| c.is_some()) return format!(
.collect::<Vec<_>>() "{}",
.len() row_cells
) .iter()
} else { // Check the Cell has data and that the stringified version is not empty
String::new() .filter(|c| c.is_some()
&& !handler
.handle_stringify_cell(&c.cell.clone().unwrap_or_default(), &field_type, field)
.is_empty())
.collect::<Vec<_>>()
.len()
);
}
} }
String::new()
} }
fn reduce_values_f64<F, T>(&self, field: &Field, row_cells: Vec<Arc<RowCell>>, f: F) -> T fn reduce_values_f64<F, T>(&self, field: &Field, row_cells: Vec<Arc<RowCell>>, f: F) -> T