feat: generic calculations (#4794)

* feat: add generic calculations

* chore: remove row count at bottom of grid

* fix: code review
This commit is contained in:
Mathias Mogensen 2024-03-05 19:16:56 +01:00 committed by GitHub
parent 3b0d82287d
commit 66aea29ab7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 222 additions and 127 deletions

View File

@ -16,7 +16,7 @@ void main() {
await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid); await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);
await tester.tapCreateRowButtonInGrid(); await tester.tapCreateRowButtonInGrid();
// The initial number of rows is 3 // 3 initial rows + 1 created
await tester.assertNumberOfRowsInGridPage(4); await tester.assertNumberOfRowsInGridPage(4);
await tester.pumpAndSettle(); await tester.pumpAndSettle();
}); });
@ -31,9 +31,8 @@ void main() {
await tester.tapCreateRowButtonInRowMenuOfGrid(); await tester.tapCreateRowButtonInRowMenuOfGrid();
// The initial number of rows is 3 // 3 initial rows + 1 created
await tester.assertNumberOfRowsInGridPage(4); await tester.assertNumberOfRowsInGridPage(4);
await tester.assertRowCountInGridPage(4);
await tester.pumpAndSettle(); await tester.pumpAndSettle();
}); });
@ -48,9 +47,8 @@ void main() {
await tester.tapRowMenuButtonInGrid(); await tester.tapRowMenuButtonInGrid();
await tester.tapDeleteOnRowMenu(); await tester.tapDeleteOnRowMenu();
// The initial number of rows is 3 // 3 initial rows - 1 deleted
await tester.assertNumberOfRowsInGridPage(2); await tester.assertNumberOfRowsInGridPage(2);
await tester.assertRowCountInGridPage(2);
await tester.pumpAndSettle(); await tester.pumpAndSettle();
}); });
@ -60,7 +58,6 @@ void main() {
await tester.tapGoButton(); await tester.tapGoButton();
await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid); await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);
await tester.assertRowCountInGridPage(3);
await tester.pumpAndSettle(); await tester.pumpAndSettle();
}); });

View File

@ -1,26 +1,12 @@
import 'dart:io'; import 'dart:io';
import 'package:appflowy/plugins/database/application/calculations/calculation_type_ext.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/calculations/calculate_cell.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/calculations/calculation_type_item.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/common/type_option_separator.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/header/type_option/number.dart';
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/checkbox.dart';
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/checklist.dart';
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/date.dart';
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/number.dart';
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/select_option.dart';
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/text.dart';
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/timestamp.dart';
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/url.dart';
import 'package:appflowy/util/field_type_extension.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.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/plugins/database/application/calculations/calculation_type_ext.dart';
import 'package:appflowy/plugins/database/board/presentation/board_page.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/calendar/application/calendar_bloc.dart'; import 'package:appflowy/plugins/database/calendar/application/calendar_bloc.dart';
@ -30,6 +16,9 @@ import 'package:appflowy/plugins/database/calendar/presentation/calendar_event_e
import 'package:appflowy/plugins/database/calendar/presentation/calendar_page.dart'; import 'package:appflowy/plugins/database/calendar/presentation/calendar_page.dart';
import 'package:appflowy/plugins/database/calendar/presentation/toolbar/calendar_layout_setting.dart'; import 'package:appflowy/plugins/database/calendar/presentation/toolbar/calendar_layout_setting.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/calculations/calculate_cell.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/calculations/calculation_type_item.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/common/type_option_separator.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/choicechip/checkbox.dart'; import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/choicechip/checkbox.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/choicechip/checklist/checklist.dart'; import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/choicechip/checklist/checklist.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/choicechip/select_option/option_list.dart'; import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/choicechip/select_option/option_list.dart';
@ -43,6 +32,7 @@ import 'package:appflowy/plugins/database/grid/presentation/widgets/header/deskt
import 'package:appflowy/plugins/database/grid/presentation/widgets/header/field_editor.dart'; import 'package:appflowy/plugins/database/grid/presentation/widgets/header/field_editor.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/header/field_type_list.dart'; import 'package:appflowy/plugins/database/grid/presentation/widgets/header/field_type_list.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/header/type_option/date/date_time_format.dart'; import 'package:appflowy/plugins/database/grid/presentation/widgets/header/type_option/date/date_time_format.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/header/type_option/number.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/row/row.dart'; import 'package:appflowy/plugins/database/grid/presentation/widgets/row/row.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/sort/create_sort_list.dart'; import 'package:appflowy/plugins/database/grid/presentation/widgets/sort/create_sort_list.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/sort/order_panel.dart'; import 'package:appflowy/plugins/database/grid/presentation/widgets/sort/order_panel.dart';
@ -52,14 +42,22 @@ import 'package:appflowy/plugins/database/grid/presentation/widgets/toolbar/filt
import 'package:appflowy/plugins/database/grid/presentation/widgets/toolbar/sort_button.dart'; import 'package:appflowy/plugins/database/grid/presentation/widgets/toolbar/sort_button.dart';
import 'package:appflowy/plugins/database/tab_bar/desktop/tab_bar_add_button.dart'; import 'package:appflowy/plugins/database/tab_bar/desktop/tab_bar_add_button.dart';
import 'package:appflowy/plugins/database/tab_bar/desktop/tab_bar_header.dart'; import 'package:appflowy/plugins/database/tab_bar/desktop/tab_bar_header.dart';
import 'package:appflowy/plugins/database/widgets/database_layout_ext.dart'; import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/checkbox.dart';
import 'package:appflowy/plugins/database/widgets/row/accessory/cell_accessory.dart'; import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/checklist.dart';
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/date.dart';
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/number.dart';
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/select_option.dart';
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/text.dart';
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/timestamp.dart';
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/url.dart';
import 'package:appflowy/plugins/database/widgets/cell_editor/checklist_cell_editor.dart'; import 'package:appflowy/plugins/database/widgets/cell_editor/checklist_cell_editor.dart';
import 'package:appflowy/plugins/database/widgets/cell_editor/checklist_progress_bar.dart'; import 'package:appflowy/plugins/database/widgets/cell_editor/checklist_progress_bar.dart';
import 'package:appflowy/plugins/database/widgets/cell_editor/date_editor.dart'; import 'package:appflowy/plugins/database/widgets/cell_editor/date_editor.dart';
import 'package:appflowy/plugins/database/widgets/cell_editor/extension.dart'; import 'package:appflowy/plugins/database/widgets/cell_editor/extension.dart';
import 'package:appflowy/plugins/database/widgets/cell_editor/select_option_editor.dart'; import 'package:appflowy/plugins/database/widgets/cell_editor/select_option_editor.dart';
import 'package:appflowy/plugins/database/widgets/cell_editor/select_option_text_field.dart'; import 'package:appflowy/plugins/database/widgets/cell_editor/select_option_text_field.dart';
import 'package:appflowy/plugins/database/widgets/database_layout_ext.dart';
import 'package:appflowy/plugins/database/widgets/row/accessory/cell_accessory.dart';
import 'package:appflowy/plugins/database/widgets/row/row_action.dart'; import 'package:appflowy/plugins/database/widgets/row/row_action.dart';
import 'package:appflowy/plugins/database/widgets/row/row_banner.dart'; import 'package:appflowy/plugins/database/widgets/row/row_banner.dart';
import 'package:appflowy/plugins/database/widgets/row/row_detail.dart'; import 'package:appflowy/plugins/database/widgets/row/row_detail.dart';
@ -70,6 +68,7 @@ import 'package:appflowy/plugins/database/widgets/setting/database_setting_actio
import 'package:appflowy/plugins/database/widgets/setting/database_settings_list.dart'; import 'package:appflowy/plugins/database/widgets/setting/database_settings_list.dart';
import 'package:appflowy/plugins/database/widgets/setting/setting_button.dart'; import 'package:appflowy/plugins/database/widgets/setting/setting_button.dart';
import 'package:appflowy/plugins/database/widgets/setting/setting_property_list.dart'; import 'package:appflowy/plugins/database/widgets/setting/setting_property_list.dart';
import 'package:appflowy/util/field_type_extension.dart';
import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/clear_date_button.dart'; import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/clear_date_button.dart';
import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/date_type_option_button.dart'; import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/date_type_option_button.dart';
import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/end_time_button.dart'; import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/end_time_button.dart';
@ -77,6 +76,7 @@ import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/remi
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart'; import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';
import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart'; import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.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:calendar_view/calendar_view.dart'; import 'package:calendar_view/calendar_view.dart';
@ -86,10 +86,9 @@ import 'package:flowy_infra_ui/style_widget/text_input.dart';
import 'package:flowy_infra_ui/widget/buttons/primary_button.dart'; import 'package:flowy_infra_ui/widget/buttons/primary_button.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:path/path.dart' as p; import 'package:path/path.dart' as p;
import 'package:table_calendar/table_calendar.dart';
// Non-exported member of the table_calendar library // Non-exported member of the table_calendar library
import 'package:table_calendar/src/widgets/cell_content.dart'; import 'package:table_calendar/src/widgets/cell_content.dart';
import 'package:table_calendar/table_calendar.dart';
import 'base.dart'; import 'base.dart';
import 'common_operations.dart'; import 'common_operations.dart';
@ -974,11 +973,6 @@ extension AppFlowyDatabaseTest on WidgetTester {
await tapButtonWithName(LocaleKeys.grid_row_delete.tr()); await tapButtonWithName(LocaleKeys.grid_row_delete.tr());
} }
Future<void> assertRowCountInGridPage(int num) async {
final text = find.text('${rowCountString()} $num', findRichText: true);
expect(text, findsOneWidget);
}
Future<void> createField(FieldType fieldType, String name) async { Future<void> createField(FieldType fieldType, String name) async {
await scrollToRight(find.byType(GridPage)); await scrollToRight(find.byType(GridPage));
await tapNewPropertyButton(); await tapNewPropertyButton();

View File

@ -11,8 +11,22 @@ extension CalcTypeLabel on CalculationType {
LocaleKeys.grid_calculationTypeLabel_median.tr(), LocaleKeys.grid_calculationTypeLabel_median.tr(),
CalculationType.Min => LocaleKeys.grid_calculationTypeLabel_min.tr(), CalculationType.Min => LocaleKeys.grid_calculationTypeLabel_min.tr(),
CalculationType.Sum => LocaleKeys.grid_calculationTypeLabel_sum.tr(), CalculationType.Sum => LocaleKeys.grid_calculationTypeLabel_sum.tr(),
CalculationType.Count =>
LocaleKeys.grid_calculationTypeLabel_count.tr(),
CalculationType.CountEmpty =>
LocaleKeys.grid_calculationTypeLabel_countEmpty.tr(),
CalculationType.CountNonEmpty =>
LocaleKeys.grid_calculationTypeLabel_countNonEmpty.tr(),
_ => throw UnimplementedError( _ => throw UnimplementedError(
'Label for $this has not been implemented', 'Label for $this has not been implemented',
), ),
}; };
String get shortLabel => switch (this) {
CalculationType.CountEmpty =>
LocaleKeys.grid_calculationTypeLabel_countEmptyShort.tr(),
CalculationType.CountNonEmpty =>
LocaleKeys.grid_calculationTypeLabel_countNonEmptyShort.tr(),
_ => label,
};
} }

View File

@ -128,7 +128,7 @@ extension NumberFormatExtension on NumberFormatPB {
} }
} }
String iconSymbol() { String iconSymbol([bool defaultPrefixInc = true]) {
switch (this) { switch (this) {
case NumberFormatPB.ArgentinePeso: case NumberFormatPB.ArgentinePeso:
return "\$"; return "\$";
@ -169,7 +169,7 @@ extension NumberFormatExtension on NumberFormatPB {
case NumberFormatPB.NorwegianKrone: case NumberFormatPB.NorwegianKrone:
return "kr"; return "kr";
case NumberFormatPB.Num: case NumberFormatPB.Num:
return "#"; return defaultPrefixInc ? "#" : "";
case NumberFormatPB.Percent: case NumberFormatPB.Percent:
return "%"; return "%";
case NumberFormatPB.PhilippinePeso: case NumberFormatPB.PhilippinePeso:

View File

@ -0,0 +1,34 @@
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
extension AvailableCalculations on FieldType {
List<CalculationType> calculationsForFieldType() {
final calculationTypes = [
CalculationType.Count,
];
// These FieldTypes cannot be empty, no need to count empty/non-empty
if (![FieldType.Checkbox, FieldType.LastEditedTime, FieldType.CreatedTime]
.contains(this)) {
calculationTypes.addAll([
CalculationType.CountEmpty,
CalculationType.CountNonEmpty,
]);
}
switch (this) {
case FieldType.Number:
calculationTypes.addAll([
CalculationType.Sum,
CalculationType.Average,
CalculationType.Min,
CalculationType.Max,
CalculationType.Median,
]);
break;
default:
break;
}
return calculationTypes;
}
}

View File

@ -12,7 +12,6 @@ import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.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/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/style_widget/scrolling/styled_scrollview.dart'; import 'package:flowy_infra_ui/style_widget/scrolling/styled_scrollview.dart';
import 'package:flowy_infra_ui/widget/error_page.dart'; import 'package:flowy_infra_ui/widget/error_page.dart';
@ -237,7 +236,6 @@ class _GridPageContentState extends State<GridPageContent> {
viewId: widget.view.id, viewId: widget.view.id,
scrollController: _scrollController, scrollController: _scrollController,
), ),
const _GridFooter(),
], ],
); );
} }
@ -431,40 +429,3 @@ class _WrapScrollView extends StatelessWidget {
); );
} }
} }
class _GridFooter extends StatelessWidget {
const _GridFooter();
@override
Widget build(BuildContext context) {
return BlocSelector<GridBloc, GridState, int>(
selector: (state) => state.rowCount,
builder: (context, rowCount) {
return Padding(
padding: GridSize.contentInsets,
child: RichText(
text: TextSpan(
text: rowCountString(),
style: Theme.of(context)
.textTheme
.bodyMedium!
.copyWith(color: Theme.of(context).hintColor),
children: [
TextSpan(
text: ' $rowCount',
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
color: AFThemeExtension.of(context).gridRowCountColor,
),
),
],
),
),
);
},
);
}
}
String rowCountString() {
return '${LocaleKeys.grid_row_count.tr()} :';
}

View File

@ -5,6 +5,7 @@ import 'package:appflowy/plugins/database/application/calculations/calculation_t
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';
import 'package:appflowy/plugins/database/grid/application/calculations/calculations_bloc.dart'; import 'package:appflowy/plugins/database/grid/application/calculations/calculations_bloc.dart';
import 'package:appflowy/plugins/database/grid/application/calculations/field_type_calc_ext.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/calculations/calculation_selector.dart'; import 'package:appflowy/plugins/database/grid/presentation/widgets/calculations/calculation_selector.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/calculations/calculation_type_item.dart'; import 'package:appflowy/plugins/database/grid/presentation/widgets/calculations/calculation_type_item.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/calculations/remove_calculation_button.dart'; import 'package:appflowy/plugins/database/grid/presentation/widgets/calculations/remove_calculation_button.dart';
@ -67,7 +68,7 @@ class _CalculateCellState extends State<CalculateCell> {
), ),
), ),
), ),
...CalculationType.values.map( ...widget.fieldInfo.fieldType.calculationsForFieldType().map(
(type) => CalculationTypeItem( (type) => CalculationTypeItem(
type: type, type: type,
onTap: () { onTap: () {
@ -87,11 +88,9 @@ class _CalculateCellState extends State<CalculateCell> {
), ),
); );
}, },
child: widget.fieldInfo.fieldType == FieldType.Number child: widget.calculation != null
? widget.calculation != null
? _showCalculateValue(context, prefix) ? _showCalculateValue(context, prefix)
: CalculationSelector(isSelected: isSelected) : CalculationSelector(isSelected: isSelected),
: const SizedBox.shrink(),
), ),
); );
} }
@ -107,7 +106,7 @@ class _CalculateCellState extends State<CalculateCell> {
children: [ children: [
Flexible( Flexible(
child: FlowyText( child: FlowyText(
widget.calculation!.calculationType.label, widget.calculation!.calculationType.shortLabel,
color: Theme.of(context).hintColor, color: Theme.of(context).hintColor,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
@ -146,7 +145,7 @@ class _CalculateCellState extends State<CalculateCell> {
FieldType.Number => FieldType.Number =>
NumberTypeOptionPB.fromBuffer(widget.fieldInfo.field.typeOptionData) NumberTypeOptionPB.fromBuffer(widget.fieldInfo.field.typeOptionData)
.format .format
.iconSymbol(), .iconSymbol(false),
_ => null, _ => null,
}; };
} }

View File

@ -1,7 +1,8 @@
import 'package:appflowy/plugins/database/grid/application/grid_bloc.dart';
import 'package:appflowy/plugins/database/application/database_controller.dart'; import 'package:appflowy/plugins/database/application/database_controller.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:appflowy/plugins/database/grid/application/grid_bloc.dart';
import 'package:bloc_test/bloc_test.dart'; import 'package:bloc_test/bloc_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'util.dart'; import 'util.dart';
void main() { void main() {
@ -17,7 +18,8 @@ void main() {
context = await gridTest.createTestGrid(); context = await gridTest.createTestGrid();
}); });
// The initial number of rows is 3 for each grid. // The initial number of rows is 3 for each grid
// We create one row so we expect 4 rows
blocTest<GridBloc, GridState>( blocTest<GridBloc, GridState>(
"create a row", "create a row",
build: () => GridBloc( build: () => GridBloc(

View File

@ -739,7 +739,12 @@
"max": "Max", "max": "Max",
"median": "Median", "median": "Median",
"min": "Min", "min": "Min",
"sum": "Sum" "sum": "Sum",
"count": "Count",
"countEmpty": "Count empty",
"countEmptyShort": "Empty",
"countNonEmpty": "Count non empty",
"countNonEmptyShort": "Not empty"
} }
}, },
"document": { "document": {

View File

@ -6,7 +6,7 @@ use std::{
use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; use flowy_derive::{ProtoBuf, ProtoBuf_Enum};
use serde_repr::{Deserialize_repr, Serialize_repr}; use serde_repr::{Deserialize_repr, Serialize_repr};
use crate::{impl_into_calculation_type, services::calculations::Calculation}; use crate::{entities::FieldType, impl_into_calculation_type, services::calculations::Calculation};
#[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)] #[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)]
pub struct CalculationPB { pub struct CalculationPB {
@ -60,6 +60,9 @@ pub enum CalculationType {
Median = 2, // Number Median = 2, // Number
Min = 3, // Number Min = 3, // Number
Sum = 4, // Number Sum = 4, // Number
Count = 5, // All
CountEmpty = 6, // All
CountNonEmpty = 7, // All
} }
impl Display for CalculationType { impl Display for CalculationType {
@ -102,6 +105,23 @@ impl From<&CalculationType> for i64 {
} }
} }
impl CalculationType {
pub fn is_allowed(&self, field_type: FieldType) -> bool {
match self {
// Number fields only
CalculationType::Max
| CalculationType::Min
| CalculationType::Average
| CalculationType::Median
| CalculationType::Sum => {
matches!(field_type, FieldType::Number)
},
// All fields
CalculationType::Count | CalculationType::CountEmpty | CalculationType::CountNonEmpty => true,
}
}
}
#[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)] #[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)]
pub struct RepeatedCalculationsPB { pub struct RepeatedCalculationsPB {
#[pb(index = 1)] #[pb(index = 1)]

View File

@ -55,6 +55,9 @@ macro_rules! impl_into_calculation_type {
2 => CalculationType::Median, 2 => CalculationType::Median,
3 => CalculationType::Min, 3 => CalculationType::Min,
4 => CalculationType::Sum, 4 => CalculationType::Sum,
5 => CalculationType::Count,
6 => CalculationType::CountEmpty,
7 => CalculationType::CountNonEmpty,
_ => { _ => {
tracing::error!("🔴 Can't parse CalculationType from value: {}", ty); tracing::error!("🔴 Can't parse CalculationType from value: {}", ty);
CalculationType::Average CalculationType::Average

View File

@ -23,6 +23,7 @@ pub trait CalculationsDelegate: Send + Sync + 'static {
fn get_cells_for_field(&self, view_id: &str, field_id: &str) -> Fut<Vec<Arc<RowCell>>>; fn get_cells_for_field(&self, view_id: &str, field_id: &str) -> Fut<Vec<Arc<RowCell>>>;
fn get_field(&self, field_id: &str) -> Option<Field>; fn get_field(&self, field_id: &str) -> Option<Field>;
fn get_calculation(&self, view_id: &str, field_id: &str) -> Fut<Option<Arc<Calculation>>>; fn get_calculation(&self, view_id: &str, field_id: &str) -> Fut<Option<Arc<Calculation>>>;
fn get_all_calculations(&self, view_id: &str) -> Fut<Arc<Vec<Arc<Calculation>>>>;
fn update_calculation(&self, view_id: &str, calculation: Calculation); fn update_calculation(&self, view_id: &str, calculation: Calculation);
fn remove_calculation(&self, view_id: &str, calculation_id: &str); fn remove_calculation(&self, view_id: &str, calculation_id: &str);
} }
@ -76,7 +77,7 @@ impl CalculationsController {
} }
} }
#[tracing::instrument(name = "schedule_filter_task", level = "trace", skip(self))] #[tracing::instrument(name = "schedule_calculation_task", level = "trace", skip(self))]
async fn gen_task(&self, task_type: CalculationEvent, qos: QualityOfService) { async fn gen_task(&self, task_type: CalculationEvent, qos: QualityOfService) {
let task_id = self.task_scheduler.read().await.next_task_id(); let task_id = self.task_scheduler.read().await.next_task_id();
let task = Task::new( let task = Task::new(
@ -89,10 +90,10 @@ impl CalculationsController {
} }
#[tracing::instrument( #[tracing::instrument(
name = "process_filter_task", name = "process_calculation_task",
level = "trace", level = "trace",
skip_all, skip_all,
fields(filter_result), fields(calculation_result),
err err
)] )]
pub async fn process(&self, predicate: &str) -> FlowyResult<()> { pub async fn process(&self, predicate: &str) -> FlowyResult<()> {
@ -160,7 +161,8 @@ impl CalculationsController {
.await; .await;
if let Some(calculation) = calculation { if let Some(calculation) = calculation {
if new_field_type != FieldType::Number { let calc_type: CalculationType = calculation.calculation_type.into();
if !calc_type.is_allowed(new_field_type) {
self self
.delegate .delegate
.remove_calculation(&self.view_id, &calculation.id); .remove_calculation(&self.view_id, &calculation.id);
@ -228,6 +230,19 @@ impl CalculationsController {
let cells = row.cells.iter(); let cells = row.cells.iter();
let mut updates = vec![]; let mut updates = vec![];
// In case there are calculations where empty cells are counted
// as a contribution to the value.
if cells.len() == 0 {
let calculations = self.delegate.get_all_calculations(&self.view_id).await;
for calculation in calculations.iter() {
let update = self.get_updated_calculation(calculation.clone()).await;
if let Some(update) = update {
updates.push(CalculationPB::from(&update));
self.delegate.update_calculation(&self.view_id, update);
}
}
}
// Iterate each cell in the row // Iterate each cell in the row
for cell in cells { for cell in cells {
let field_id = cell.0; let field_id = cell.0;
@ -260,9 +275,6 @@ impl CalculationsController {
.await; .await;
let field = self.delegate.get_field(&calculation.field_id)?; let field = self.delegate.get_field(&calculation.field_id)?;
if field_cells.is_empty() {
return Some(calculation.with_value(String::new()));
} else {
let value = let value =
self self
.calculations_service .calculations_service
@ -271,7 +283,6 @@ impl CalculationsController {
if value != calculation.value { if value != calculation.value {
return Some(calculation.with_value(value)); return Some(calculation.with_value(value));
} }
}
None None
} }

View File

@ -26,6 +26,9 @@ impl CalculationsService {
CalculationType::Median => self.calculate_median(field, row_cells), CalculationType::Median => self.calculate_median(field, row_cells),
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::CountEmpty => self.calculate_count_empty(row_cells),
CalculationType::CountNonEmpty => self.calculate_count_non_empty(row_cells),
} }
} }
@ -62,7 +65,7 @@ impl CalculationsService {
if !values.is_empty() { if !values.is_empty() {
format!("{:.5}", Self::median(&values)) format!("{:.5}", Self::median(&values))
} else { } else {
"".to_owned() String::new()
} }
} }
@ -89,7 +92,7 @@ impl CalculationsService {
} }
} }
"".to_owned() String::new()
} }
fn calculate_max(&self, field: &Field, row_cells: Vec<Arc<RowCell>>) -> String { fn calculate_max(&self, field: &Field, row_cells: Vec<Arc<RowCell>>) -> String {
@ -105,7 +108,7 @@ impl CalculationsService {
} }
} }
"".to_owned() String::new()
} }
fn calculate_sum(&self, field: &Field, row_cells: Vec<Arc<RowCell>>) -> String { fn calculate_sum(&self, field: &Field, row_cells: Vec<Arc<RowCell>>) -> String {
@ -114,7 +117,45 @@ impl CalculationsService {
if !values.is_empty() { if !values.is_empty() {
format!("{:.5}", values.iter().sum::<f64>()) format!("{:.5}", values.iter().sum::<f64>())
} else { } else {
"".to_owned() String::new()
}
}
fn calculate_count(&self, row_cells: Vec<Arc<RowCell>>) -> String {
if !row_cells.is_empty() {
format!("{}", row_cells.len())
} else {
String::new()
}
}
fn calculate_count_empty(&self, row_cells: Vec<Arc<RowCell>>) -> String {
if !row_cells.is_empty() {
format!(
"{}",
row_cells
.iter()
.filter(|c| c.is_none())
.collect::<Vec<_>>()
.len()
)
} else {
String::new()
}
}
fn calculate_count_non_empty(&self, row_cells: Vec<Arc<RowCell>>) -> String {
if !row_cells.is_empty() {
format!(
"{}",
row_cells
.iter()
.filter(|c| c.is_some())
.collect::<Vec<_>>()
.len()
)
} else {
String::new()
} }
} }

View File

@ -66,4 +66,9 @@ impl CalculationsDelegate for DatabaseViewCalculationsDelegateImpl {
fn remove_calculation(&self, view_id: &str, calculation_id: &str) { fn remove_calculation(&self, view_id: &str, calculation_id: &str) {
self.0.remove_calculation(view_id, calculation_id) self.0.remove_calculation(view_id, calculation_id)
} }
fn get_all_calculations(&self, view_id: &str) -> Fut<Arc<Vec<Arc<Calculation>>>> {
let calculations = Arc::new(self.0.get_all_calculations(view_id));
to_fut(async move { calculations })
}
} }

View File

@ -161,14 +161,14 @@ impl DatabaseViewEditor {
.payload(changes) .payload(changes)
.send(); .send();
self self
.gen_did_create_row_view_tasks(row_detail.row.id.clone()) .gen_did_create_row_view_tasks(row_detail.row.clone())
.await; .await;
} }
pub async fn v_did_duplicate_row(&self, row_detail: &RowDetail) { pub async fn v_did_duplicate_row(&self, row_detail: &RowDetail) {
self self
.calculations_controller .calculations_controller
.did_receive_row_changed(row_detail.clone().row) .did_receive_row_changed(row_detail.row.clone())
.await; .await;
} }
@ -1090,7 +1090,7 @@ impl DatabaseViewEditor {
sort_controller sort_controller
.read() .read()
.await .await
.did_receive_row_changed(row_id) .did_receive_row_changed(row_id.clone())
.await; .await;
} }
if let Some(calculations_controller) = weak_calculations_controller.upgrade() { if let Some(calculations_controller) = weak_calculations_controller.upgrade() {
@ -1101,11 +1101,20 @@ impl DatabaseViewEditor {
}); });
} }
async fn gen_did_create_row_view_tasks(&self, row_id: RowId) { async fn gen_did_create_row_view_tasks(&self, row: Row) {
let weak_sort_controller = Arc::downgrade(&self.sort_controller); let weak_sort_controller = Arc::downgrade(&self.sort_controller);
let weak_calculations_controller = Arc::downgrade(&self.calculations_controller);
af_spawn(async move { af_spawn(async move {
if let Some(sort_controller) = weak_sort_controller.upgrade() { if let Some(sort_controller) = weak_sort_controller.upgrade() {
sort_controller.read().await.did_create_row(row_id).await; sort_controller
.read()
.await
.did_create_row(row.id.clone())
.await;
}
if let Some(calculations_controller) = weak_calculations_controller.upgrade() {
calculations_controller.did_receive_row_changed(row).await;
} }
}); });
} }