From e2e38f72bb7528f61222ca2e13291f2378d2f9a4 Mon Sep 17 00:00:00 2001 From: Mathias Mogensen <42929161+Xazin@users.noreply.github.com> Date: Thu, 21 Mar 2024 17:40:23 +0100 Subject: [PATCH] feat: clear all cells (#4856) * feat: clear all cells * fix: smaller dialog width * fix: clippy warning --- .../database/domain/field_service.dart | 13 +++++ .../widgets/header/field_editor.dart | 23 ++++++++ .../presentation/widgets/dialogs.dart | 14 +++-- frontend/resources/translations/en.json | 2 + .../src/entities/field_entities.rs | 25 ++++++++ .../flowy-database2/src/event_handler.rs | 14 +++++ .../rust-lib/flowy-database2/src/event_map.rs | 6 ++ .../src/services/database/database_editor.rs | 57 ++++++++++++++++++- 8 files changed, 147 insertions(+), 7 deletions(-) diff --git a/frontend/appflowy_flutter/lib/plugins/database/domain/field_service.dart b/frontend/appflowy_flutter/lib/plugins/database/domain/field_service.dart index 4274c97c87..9bc873f7f1 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/domain/field_service.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/domain/field_service.dart @@ -62,6 +62,19 @@ class FieldBackendService { return DatabaseEventDeleteField(payload).send(); } + // Clear all data of all cells in a Field + static Future> clearField({ + required String viewId, + required String fieldId, + }) { + final payload = ClearFieldPayloadPB( + viewId: viewId, + fieldId: fieldId, + ); + + return DatabaseEventClearField(payload).send(); + } + /// Duplicate a field static Future> duplicateField({ required String viewId, diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/header/field_editor.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/header/field_editor.dart index 0cd22a7a29..6a49c854d4 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/header/field_editor.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/header/field_editor.dart @@ -104,6 +104,8 @@ class _FieldEditorState extends State { VSpace(GridSize.typeOptionSeparatorHeight), _actionCell(FieldAction.duplicate), VSpace(GridSize.typeOptionSeparatorHeight), + _actionCell(FieldAction.clearData), + VSpace(GridSize.typeOptionSeparatorHeight), _actionCell(FieldAction.delete), ], ).padding(all: 8.0), @@ -195,6 +197,7 @@ enum FieldAction { insertRight, toggleVisibility, duplicate, + clearData, delete; Widget icon(FieldInfo fieldInfo, Color? color) { @@ -213,6 +216,8 @@ enum FieldAction { } case FieldAction.duplicate: svgData = FlowySvgs.copy_s; + case FieldAction.clearData: + svgData = FlowySvgs.reload_s; case FieldAction.delete: svgData = FlowySvgs.delete_s; } @@ -241,6 +246,8 @@ enum FieldAction { } case FieldAction.duplicate: return LocaleKeys.grid_field_duplicate.tr(); + case FieldAction.clearData: + return LocaleKeys.grid_field_clear.tr(); case FieldAction.delete: return LocaleKeys.grid_field_delete.tr(); } @@ -273,6 +280,22 @@ enum FieldAction { fieldId: fieldInfo.id, ); break; + case FieldAction.clearData: + NavigatorAlertDialog( + constraints: const BoxConstraints( + maxWidth: 250, + maxHeight: 260, + ), + title: LocaleKeys.grid_field_clearFieldPromptMessage.tr(), + confirm: () { + FieldBackendService.clearField( + viewId: viewId, + fieldId: fieldInfo.id, + ); + }, + ).show(context); + PopoverContainer.of(context).close(); + break; case FieldAction.delete: NavigatorAlertDialog( title: LocaleKeys.grid_field_deleteFieldPromptMessage.tr(), diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/dialogs.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/dialogs.dart index 1458bcad5b..cd043e06fe 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/dialogs.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/dialogs.dart @@ -1,3 +1,5 @@ +import 'package:flutter/material.dart'; + import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/startup/tasks/app_widget.dart'; import 'package:easy_localization/easy_localization.dart'; @@ -8,7 +10,6 @@ import 'package:flowy_infra_ui/widget/buttons/primary_button.dart'; import 'package:flowy_infra_ui/widget/buttons/secondary_button.dart'; import 'package:flowy_infra_ui/widget/dialog/styled_dialogs.dart'; import 'package:flowy_infra_ui/widget/spacing.dart'; -import 'package:flutter/material.dart'; export 'package:flowy_infra_ui/widget/dialog/styled_dialogs.dart'; @@ -114,12 +115,14 @@ class NavigatorAlertDialog extends StatefulWidget { this.cancel, this.confirm, this.hideCancelButton = false, + this.constraints, }); final String title; final void Function()? cancel; final void Function()? confirm; final bool hideCancelButton; + final BoxConstraints? constraints; @override State createState() => _CreateFlowyAlertDialog(); @@ -140,10 +143,11 @@ class _CreateFlowyAlertDialog extends State { children: [ ...[ ConstrainedBox( - constraints: const BoxConstraints( - maxWidth: 400, - maxHeight: 260, - ), + constraints: widget.constraints ?? + const BoxConstraints( + maxWidth: 400, + maxHeight: 260, + ), child: FlowyText.medium( widget.title, fontSize: FontSizes.s16, diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index 8c55eb4722..65c71a549f 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -620,6 +620,7 @@ "insertRight": "Insert Right", "duplicate": "Duplicate", "delete": "Delete", + "clear": "Clear cells", "textFieldName": "Text", "checkboxFieldName": "Checkbox", "dateFieldName": "Date", @@ -660,6 +661,7 @@ "editProperty": "Edit property", "newProperty": "New property", "deleteFieldPromptMessage": "Are you sure? This property will be deleted", + "clearFieldPromptMessage": "Are you sure? All cells in this column will be emptied", "newColumn": "New Column", "format": "Format", "reminderOnDateTooltip": "This cell has a scheduled reminder", diff --git a/frontend/rust-lib/flowy-database2/src/entities/field_entities.rs b/frontend/rust-lib/flowy-database2/src/entities/field_entities.rs index 2db4ffb1b8..22b8c29858 100644 --- a/frontend/rust-lib/flowy-database2/src/entities/field_entities.rs +++ b/frontend/rust-lib/flowy-database2/src/entities/field_entities.rs @@ -10,6 +10,7 @@ use strum_macros::{EnumCount as EnumCountMacro, EnumIter}; use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; use flowy_error::ErrorCode; +use validator::Validate; use crate::entities::parser::NotEmptyStr; use crate::entities::position_entities::OrderObjectPositionPB; @@ -620,6 +621,30 @@ impl TryInto for DuplicateFieldPayloadPB { } } +#[derive(Debug, Clone, Default, ProtoBuf, Validate)] +pub struct ClearFieldPayloadPB { + #[pb(index = 1)] + #[validate(custom = "lib_infra::validator_fn::required_not_empty_str")] + pub field_id: String, + + #[pb(index = 2)] + #[validate(custom = "lib_infra::validator_fn::required_not_empty_str")] + pub view_id: String, +} + +impl TryInto for ClearFieldPayloadPB { + type Error = ErrorCode; + + fn try_into(self) -> Result { + let view_id = NotEmptyStr::parse(self.view_id).map_err(|_| ErrorCode::DatabaseIdIsEmpty)?; + let field_id = NotEmptyStr::parse(self.field_id).map_err(|_| ErrorCode::FieldIdIsEmpty)?; + Ok(FieldIdParams { + view_id: view_id.0, + field_id: field_id.0, + }) + } +} + #[derive(Debug, Clone, Default, ProtoBuf)] pub struct DeleteFieldPayloadPB { #[pb(index = 1)] diff --git a/frontend/rust-lib/flowy-database2/src/event_handler.rs b/frontend/rust-lib/flowy-database2/src/event_handler.rs index 1e40e61354..78528f255f 100644 --- a/frontend/rust-lib/flowy-database2/src/event_handler.rs +++ b/frontend/rust-lib/flowy-database2/src/event_handler.rs @@ -257,6 +257,20 @@ pub(crate) async fn delete_field_handler( Ok(()) } +#[tracing::instrument(level = "trace", skip(data, manager), err)] +pub(crate) async fn clear_field_handler( + data: AFPluginData, + manager: AFPluginState>, +) -> Result<(), FlowyError> { + let manager = upgrade_manager(manager)?; + let params: FieldIdParams = data.into_inner().try_into()?; + let database_editor = manager.get_database_with_view_id(¶ms.view_id).await?; + database_editor + .clear_field(¶ms.view_id, ¶ms.field_id) + .await?; + Ok(()) +} + #[tracing::instrument(level = "debug", skip(data, manager), err)] pub(crate) async fn switch_to_field_handler( data: AFPluginData, diff --git a/frontend/rust-lib/flowy-database2/src/event_map.rs b/frontend/rust-lib/flowy-database2/src/event_map.rs index 17e9c68ff5..f753c34d5f 100644 --- a/frontend/rust-lib/flowy-database2/src/event_map.rs +++ b/frontend/rust-lib/flowy-database2/src/event_map.rs @@ -27,6 +27,7 @@ pub fn init(database_manager: Weak) -> AFPlugin { .event(DatabaseEvent::UpdateField, update_field_handler) .event(DatabaseEvent::UpdateFieldTypeOption, update_field_type_option_handler) .event(DatabaseEvent::DeleteField, delete_field_handler) + .event(DatabaseEvent::ClearField, clear_field_handler) .event(DatabaseEvent::UpdateFieldType, switch_to_field_handler) .event(DatabaseEvent::DuplicateField, duplicate_field_handler) .event(DatabaseEvent::MoveField, move_field_handler) @@ -161,6 +162,11 @@ pub enum DatabaseEvent { #[event(input = "DeleteFieldPayloadPB")] DeleteField = 14, + /// [ClearField] event is used to clear all Cells in a Field. [ClearFieldPayloadPB] is the context that + /// is used to clear the field from the Database. + #[event(input = "ClearFieldPayloadPB")] + ClearField = 15, + /// [UpdateFieldType] event is used to update the current Field's type. /// It will insert a new FieldTypeOptionData if the new FieldType doesn't exist before, otherwise /// reuse the existing FieldTypeOptionData. You could check the [DatabaseRevisionPad] for more details. diff --git a/frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs b/frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs index 94a39bf411..d9f7ec2541 100644 --- a/frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs +++ b/frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs @@ -361,6 +361,30 @@ impl DatabaseEditor { Ok(()) } + pub async fn clear_field(&self, view_id: &str, field_id: &str) -> FlowyResult<()> { + let field_type: FieldType = self + .get_field(field_id) + .map(|field| field.field_type.into()) + .unwrap_or_default(); + + if matches!( + field_type, + FieldType::LastEditedTime | FieldType::CreatedTime + ) { + return Err(FlowyError::new( + ErrorCode::Internal, + "Can not clear the field type of Last Edited Time or Created Time.", + )); + } + + let cells: Vec = self.get_cells_for_field(view_id, field_id).await; + for row_cell in cells { + self.clear_cell(view_id, row_cell.row_id, field_id).await?; + } + + Ok(()) + } + /// Update the field type option data. /// Do nothing if the [TypeOptionData] is empty. pub async fn update_field_type_option( @@ -804,6 +828,37 @@ impl DatabaseEditor { }); }); + self + .did_update_row(view_id, row_id, field_id, old_row) + .await; + + Ok(()) + } + + pub async fn clear_cell(&self, view_id: &str, row_id: RowId, field_id: &str) -> FlowyResult<()> { + // Get the old row before updating the cell. It would be better to get the old cell + let old_row = { self.get_row_detail(view_id, &row_id) }; + + self.database.lock().update_row(&row_id, |row_update| { + row_update.update_cells(|cell_update| { + cell_update.clear(field_id); + }); + }); + + self + .did_update_row(view_id, row_id, field_id, old_row) + .await; + + Ok(()) + } + + async fn did_update_row( + &self, + view_id: &str, + row_id: RowId, + field_id: &str, + old_row: Option, + ) { let option_row = self.get_row_detail(view_id, &row_id); if let Some(new_row_detail) = option_row { for view in self.database_views.editors().await { @@ -821,8 +876,6 @@ impl DatabaseEditor { self .notify_update_row(view_id, row_id, vec![changeset]) .await; - - Ok(()) } pub fn get_auto_updated_fields_changesets(