mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
feat: generic calculations (#4794)
* feat: add generic calculations * chore: remove row count at bottom of grid * fix: code review
This commit is contained in:
@ -6,7 +6,7 @@ use std::{
|
||||
use flowy_derive::{ProtoBuf, ProtoBuf_Enum};
|
||||
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)]
|
||||
pub struct CalculationPB {
|
||||
@ -56,10 +56,13 @@ impl std::convert::From<&Arc<Calculation>> for CalculationPB {
|
||||
pub enum CalculationType {
|
||||
#[default]
|
||||
Average = 0, // Number
|
||||
Max = 1, // Number
|
||||
Median = 2, // Number
|
||||
Min = 3, // Number
|
||||
Sum = 4, // Number
|
||||
Max = 1, // Number
|
||||
Median = 2, // Number
|
||||
Min = 3, // Number
|
||||
Sum = 4, // Number
|
||||
Count = 5, // All
|
||||
CountEmpty = 6, // All
|
||||
CountNonEmpty = 7, // All
|
||||
}
|
||||
|
||||
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)]
|
||||
pub struct RepeatedCalculationsPB {
|
||||
#[pb(index = 1)]
|
||||
|
@ -55,6 +55,9 @@ macro_rules! impl_into_calculation_type {
|
||||
2 => CalculationType::Median,
|
||||
3 => CalculationType::Min,
|
||||
4 => CalculationType::Sum,
|
||||
5 => CalculationType::Count,
|
||||
6 => CalculationType::CountEmpty,
|
||||
7 => CalculationType::CountNonEmpty,
|
||||
_ => {
|
||||
tracing::error!("🔴 Can't parse CalculationType from value: {}", ty);
|
||||
CalculationType::Average
|
||||
|
@ -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_field(&self, field_id: &str) -> Option<Field>;
|
||||
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 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) {
|
||||
let task_id = self.task_scheduler.read().await.next_task_id();
|
||||
let task = Task::new(
|
||||
@ -89,10 +90,10 @@ impl CalculationsController {
|
||||
}
|
||||
|
||||
#[tracing::instrument(
|
||||
name = "process_filter_task",
|
||||
name = "process_calculation_task",
|
||||
level = "trace",
|
||||
skip_all,
|
||||
fields(filter_result),
|
||||
fields(calculation_result),
|
||||
err
|
||||
)]
|
||||
pub async fn process(&self, predicate: &str) -> FlowyResult<()> {
|
||||
@ -160,7 +161,8 @@ impl CalculationsController {
|
||||
.await;
|
||||
|
||||
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
|
||||
.delegate
|
||||
.remove_calculation(&self.view_id, &calculation.id);
|
||||
@ -228,6 +230,19 @@ impl CalculationsController {
|
||||
let cells = row.cells.iter();
|
||||
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
|
||||
for cell in cells {
|
||||
let field_id = cell.0;
|
||||
@ -260,17 +275,13 @@ impl CalculationsController {
|
||||
.await;
|
||||
let field = self.delegate.get_field(&calculation.field_id)?;
|
||||
|
||||
if field_cells.is_empty() {
|
||||
return Some(calculation.with_value(String::new()));
|
||||
} else {
|
||||
let value =
|
||||
self
|
||||
.calculations_service
|
||||
.calculate(&field, calculation.calculation_type, field_cells);
|
||||
let value =
|
||||
self
|
||||
.calculations_service
|
||||
.calculate(&field, calculation.calculation_type, field_cells);
|
||||
|
||||
if value != calculation.value {
|
||||
return Some(calculation.with_value(value));
|
||||
}
|
||||
if value != calculation.value {
|
||||
return Some(calculation.with_value(value));
|
||||
}
|
||||
|
||||
None
|
||||
|
@ -26,6 +26,9 @@ impl CalculationsService {
|
||||
CalculationType::Median => self.calculate_median(field, row_cells),
|
||||
CalculationType::Min => self.calculate_min(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() {
|
||||
format!("{:.5}", Self::median(&values))
|
||||
} 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 {
|
||||
@ -105,7 +108,7 @@ impl CalculationsService {
|
||||
}
|
||||
}
|
||||
|
||||
"".to_owned()
|
||||
String::new()
|
||||
}
|
||||
|
||||
fn calculate_sum(&self, field: &Field, row_cells: Vec<Arc<RowCell>>) -> String {
|
||||
@ -114,7 +117,45 @@ impl CalculationsService {
|
||||
if !values.is_empty() {
|
||||
format!("{:.5}", values.iter().sum::<f64>())
|
||||
} 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()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -66,4 +66,9 @@ impl CalculationsDelegate for DatabaseViewCalculationsDelegateImpl {
|
||||
fn remove_calculation(&self, view_id: &str, calculation_id: &str) {
|
||||
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 })
|
||||
}
|
||||
}
|
||||
|
@ -161,14 +161,14 @@ impl DatabaseViewEditor {
|
||||
.payload(changes)
|
||||
.send();
|
||||
self
|
||||
.gen_did_create_row_view_tasks(row_detail.row.id.clone())
|
||||
.gen_did_create_row_view_tasks(row_detail.row.clone())
|
||||
.await;
|
||||
}
|
||||
|
||||
pub async fn v_did_duplicate_row(&self, row_detail: &RowDetail) {
|
||||
self
|
||||
.calculations_controller
|
||||
.did_receive_row_changed(row_detail.clone().row)
|
||||
.did_receive_row_changed(row_detail.row.clone())
|
||||
.await;
|
||||
}
|
||||
|
||||
@ -1090,7 +1090,7 @@ impl DatabaseViewEditor {
|
||||
sort_controller
|
||||
.read()
|
||||
.await
|
||||
.did_receive_row_changed(row_id)
|
||||
.did_receive_row_changed(row_id.clone())
|
||||
.await;
|
||||
}
|
||||
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_calculations_controller = Arc::downgrade(&self.calculations_controller);
|
||||
af_spawn(async move {
|
||||
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;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
Reference in New Issue
Block a user