fix: update the cell content if input is not valid data (#1652)

Co-authored-by: nathan <nathan@appflowy.io>
This commit is contained in:
Nathan.fooo 2023-01-05 20:29:19 +08:00 committed by GitHub
parent 3ba3a8dc18
commit e9f8796809
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 98 additions and 58 deletions

View File

@ -133,7 +133,7 @@ class IGridCellController<T, D> extends Equatable {
final IGridCellDataPersistence<D> _cellDataPersistence; final IGridCellDataPersistence<D> _cellDataPersistence;
CellListener? _cellListener; CellListener? _cellListener;
ValueNotifier<T?>? _cellDataNotifier; CellDataNotifier<T?>? _cellDataNotifier;
bool isListening = false; bool isListening = false;
VoidCallback? _onFieldChangedFn; VoidCallback? _onFieldChangedFn;
@ -170,8 +170,20 @@ class IGridCellController<T, D> extends Equatable {
FieldType get fieldType => cellId.fieldInfo.fieldType; FieldType get fieldType => cellId.fieldInfo.fieldType;
/// Listen on the cell content or field changes
///
/// An optional [listenWhenOnCellChanged] can be implemented for more
/// granular control over when [listener] is called.
/// [listenWhenOnCellChanged] will be invoked on each [onCellChanged]
/// get called.
/// [listenWhenOnCellChanged] takes the previous `value` and current
/// `value` and must return a [bool] which determines whether or not
/// the [onCellChanged] function will be invoked.
/// [onCellChanged] is optional and if omitted, it will default to `true`.
///
VoidCallback? startListening({ VoidCallback? startListening({
required void Function(T?) onCellChanged, required void Function(T?) onCellChanged,
bool Function(T? oldValue, T? newValue)? listenWhenOnCellChanged,
VoidCallback? onCellFieldChanged, VoidCallback? onCellFieldChanged,
}) { }) {
if (isListening) { if (isListening) {
@ -180,7 +192,10 @@ class IGridCellController<T, D> extends Equatable {
} }
isListening = true; isListening = true;
_cellDataNotifier = ValueNotifier(_cellsCache.get(_cacheKey)); _cellDataNotifier = CellDataNotifier(
value: _cellsCache.get(_cacheKey),
listenWhen: listenWhenOnCellChanged,
);
_cellListener = _cellListener =
CellListener(rowId: cellId.rowId, fieldId: cellId.fieldInfo.id); CellListener(rowId: cellId.rowId, fieldId: cellId.fieldInfo.id);
@ -255,24 +270,21 @@ class IGridCellController<T, D> extends Equatable {
/// You can set [deduplicate] to true (default is false) to reduce the save operation. /// You can set [deduplicate] to true (default is false) to reduce the save operation.
/// It's useful when you call this method when user editing the [TextField]. /// It's useful when you call this method when user editing the [TextField].
/// The default debounce interval is 300 milliseconds. /// The default debounce interval is 300 milliseconds.
void saveCellData(D data, void saveCellData(
{bool deduplicate = false, D data, {
void Function(Option<FlowyError>)? resultCallback}) async { bool deduplicate = false,
void Function(Option<FlowyError>)? onFinish,
}) async {
_loadDataOperation?.cancel();
if (deduplicate) { if (deduplicate) {
_loadDataOperation?.cancel();
_saveDataOperation?.cancel(); _saveDataOperation?.cancel();
_saveDataOperation = Timer(const Duration(milliseconds: 300), () async { _saveDataOperation = Timer(const Duration(milliseconds: 300), () async {
final result = await _cellDataPersistence.save(data); final result = await _cellDataPersistence.save(data);
if (resultCallback != null) { onFinish?.call(result);
resultCallback(result);
}
}); });
} else { } else {
final result = await _cellDataPersistence.save(data); final result = await _cellDataPersistence.save(data);
if (resultCallback != null) { onFinish?.call(result);
resultCallback(result);
}
} }
} }
@ -302,6 +314,7 @@ class IGridCellController<T, D> extends Equatable {
await _cellListener?.stop(); await _cellListener?.stop();
_loadDataOperation?.cancel(); _loadDataOperation?.cancel();
_saveDataOperation?.cancel(); _saveDataOperation?.cancel();
_cellDataNotifier?.dispose();
_cellDataNotifier = null; _cellDataNotifier = null;
if (_onFieldChangedFn != null) { if (_onFieldChangedFn != null) {
@ -343,3 +356,23 @@ class GridCellFieldNotifierImpl extends IGridCellFieldNotifier {
); );
} }
} }
class CellDataNotifier<T> extends ChangeNotifier {
T _value;
bool Function(T? oldValue, T? newValue)? listenWhen;
CellDataNotifier({required T value, this.listenWhen}) : _value = value;
set value(T newValue) {
if (listenWhen?.call(_value, newValue) ?? false) {
_value = newValue;
notifyListeners();
} else {
if (_value != newValue) {
_value = newValue;
notifyListeners();
}
}
}
T get value => _value;
}

View File

@ -102,7 +102,7 @@ class DateCalBloc extends Bloc<DateCalEvent, DateCalState> {
} }
} }
cellController.saveCellData(newCalData, resultCallback: (result) { cellController.saveCellData(newCalData, onFinish: (result) {
result.fold( result.fold(
() => updateCalData(Some(newCalData), none()), () => updateCalData(Some(newCalData), none()),
(err) { (err) {

View File

@ -1,8 +1,7 @@
import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart'; import 'package:flowy_sdk/log.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:freezed_annotation/freezed_annotation.dart';
import 'dart:async'; import 'dart:async';
import 'package:dartz/dartz.dart';
import 'cell_service/cell_service.dart'; import 'cell_service/cell_service.dart';
part 'number_cell_bloc.freezed.dart'; part 'number_cell_bloc.freezed.dart';
@ -20,20 +19,19 @@ class NumberCellBloc extends Bloc<NumberCellEvent, NumberCellState> {
initial: () { initial: () {
_startListening(); _startListening();
}, },
didReceiveCellUpdate: (content) { didReceiveCellUpdate: (cellContent) {
emit(state.copyWith(content: content)); emit(state.copyWith(cellContent: cellContent ?? ""));
}, },
updateCell: (text) { updateCell: (text) {
cellController.saveCellData(text, resultCallback: (result) { if (state.cellContent != text) {
result.fold( emit(state.copyWith(cellContent: text));
() => null, cellController.saveCellData(text, onFinish: (result) {
(err) { result.fold(
if (!isClosed) { () {},
add(NumberCellEvent.didReceiveCellUpdate(right(err))); (err) => Log.error(err),
} );
}, });
); }
});
}, },
); );
}, },
@ -51,13 +49,22 @@ class NumberCellBloc extends Bloc<NumberCellEvent, NumberCellState> {
} }
void _startListening() { void _startListening() {
_onCellChangedFn = cellController.startListening( _onCellChangedFn =
onCellChanged: ((cellContent) { cellController.startListening(onCellChanged: ((cellContent) {
if (!isClosed) { if (!isClosed) {
add(NumberCellEvent.didReceiveCellUpdate(left(cellContent ?? ""))); add(NumberCellEvent.didReceiveCellUpdate(cellContent));
} }
}), }), listenWhenOnCellChanged: (oldValue, newValue) {
); // If the new value is not the same as the content, which means the
// backend formatted the content that user enter. For example:
//
// state.cellContent: "abc"
// oldValue: ""
// newValue: ""
// The oldValue is the same as newValue. So the [onCellChanged] won't
// get called. So just return true to refresh the cell content
return true;
});
} }
} }
@ -65,20 +72,19 @@ class NumberCellBloc extends Bloc<NumberCellEvent, NumberCellState> {
class NumberCellEvent with _$NumberCellEvent { class NumberCellEvent with _$NumberCellEvent {
const factory NumberCellEvent.initial() = _Initial; const factory NumberCellEvent.initial() = _Initial;
const factory NumberCellEvent.updateCell(String text) = _UpdateCell; const factory NumberCellEvent.updateCell(String text) = _UpdateCell;
const factory NumberCellEvent.didReceiveCellUpdate( const factory NumberCellEvent.didReceiveCellUpdate(String? cellContent) =
Either<String, FlowyError> cellContent) = _DidReceiveCellUpdate; _DidReceiveCellUpdate;
} }
@freezed @freezed
class NumberCellState with _$NumberCellState { class NumberCellState with _$NumberCellState {
const factory NumberCellState({ const factory NumberCellState({
required Either<String, FlowyError> content, required String cellContent,
}) = _NumberCellState; }) = _NumberCellState;
factory NumberCellState.initial(GridCellController context) { factory NumberCellState.initial(GridCellController context) {
final cellContent = context.getCellData() ?? "";
return NumberCellState( return NumberCellState(
content: left(cellContent), cellContent: context.getCellData() ?? "",
); );
} }
} }

View File

@ -29,8 +29,7 @@ class _NumberCellState extends GridFocusNodeCellState<GridNumberCell> {
final cellController = widget.cellControllerBuilder.build(); final cellController = widget.cellControllerBuilder.build();
_cellBloc = getIt<NumberCellBloc>(param1: cellController) _cellBloc = getIt<NumberCellBloc>(param1: cellController)
..add(const NumberCellEvent.initial()); ..add(const NumberCellEvent.initial());
_controller = _controller = TextEditingController(text: _cellBloc.state.cellContent);
TextEditingController(text: contentFromState(_cellBloc.state));
super.initState(); super.initState();
} }
@ -41,9 +40,8 @@ class _NumberCellState extends GridFocusNodeCellState<GridNumberCell> {
child: MultiBlocListener( child: MultiBlocListener(
listeners: [ listeners: [
BlocListener<NumberCellBloc, NumberCellState>( BlocListener<NumberCellBloc, NumberCellState>(
listenWhen: (p, c) => p.content != c.content, listenWhen: (p, c) => p.cellContent != c.cellContent,
listener: (context, state) => listener: (context, state) => _controller.text = state.cellContent,
_controller.text = contentFromState(state),
), ),
], ],
child: Padding( child: Padding(
@ -80,20 +78,16 @@ class _NumberCellState extends GridFocusNodeCellState<GridNumberCell> {
_delayOperation?.cancel(); _delayOperation?.cancel();
_delayOperation = Timer(const Duration(milliseconds: 30), () { _delayOperation = Timer(const Duration(milliseconds: 30), () {
if (_cellBloc.isClosed == false && if (_cellBloc.isClosed == false &&
_controller.text != contentFromState(_cellBloc.state)) { _controller.text != _cellBloc.state.cellContent) {
_cellBloc.add(NumberCellEvent.updateCell(_controller.text)); _cellBloc.add(NumberCellEvent.updateCell(_controller.text));
} }
}); });
} }
} }
String contentFromState(NumberCellState state) {
return state.content.fold((l) => l, (r) => "");
}
@override @override
String? onCopy() { String? onCopy() {
return _cellBloc.state.content.fold((content) => content, (r) => null); return _cellBloc.state.cellContent;
} }
@override @override

View File

@ -2,7 +2,7 @@ use crate::services::cell::{CellBytesCustomParser, CellProtobufBlobParser, Decod
use crate::services::field::number_currency::Currency; use crate::services::field::number_currency::Currency;
use crate::services::field::{strip_currency_symbol, NumberFormat, STRIP_SYMBOL}; use crate::services::field::{strip_currency_symbol, NumberFormat, STRIP_SYMBOL};
use bytes::Bytes; use bytes::Bytes;
use flowy_error::{FlowyError, FlowyResult}; use flowy_error::FlowyResult;
use rust_decimal::Decimal; use rust_decimal::Decimal;
use rusty_money::Money; use rusty_money::Money;
use std::str::FromStr; use std::str::FromStr;
@ -40,7 +40,8 @@ impl NumberCellData {
if num_str.chars().all(char::is_numeric) { if num_str.chars().all(char::is_numeric) {
Self::from_format_str(&num_str, sign_positive, format) Self::from_format_str(&num_str, sign_positive, format)
} else { } else {
Err(FlowyError::invalid_data().context("Should only contain numbers")) // returns empty string if it can be formatted
Ok(Self::default())
} }
} }
}, },

View File

@ -60,7 +60,7 @@ impl CellDataCacheKey {
} }
hasher.write(field_rev.id.as_bytes()); hasher.write(field_rev.id.as_bytes());
hasher.write_u8(decoded_field_type as u8); hasher.write_u8(decoded_field_type as u8);
hasher.write(cell_str.as_bytes()); cell_str.hash(&mut hasher);
Self(hasher.finish()) Self(hasher.finish())
} }
} }
@ -125,8 +125,14 @@ where
} }
} }
let cell_data = self.decode_cell_str(cell_str, decoded_field_type, field_rev)?; let cell_data = self.decode_cell_str(cell_str.clone(), decoded_field_type, field_rev)?;
if let Some(cell_data_cache) = self.cell_data_cache.as_ref() { if let Some(cell_data_cache) = self.cell_data_cache.as_ref() {
tracing::trace!(
"Cell cache update: field_type:{}, cell_str: {}, cell_data: {:?}",
decoded_field_type,
cell_str,
cell_data
);
cell_data_cache.write().insert(key.as_ref(), cell_data.clone()); cell_data_cache.write().insert(key.as_ref(), cell_data.clone());
} }
Ok(cell_data) Ok(cell_data)
@ -140,12 +146,13 @@ where
) { ) {
if let Some(cell_data_cache) = self.cell_data_cache.as_ref() { if let Some(cell_data_cache) = self.cell_data_cache.as_ref() {
let field_type: FieldType = field_rev.ty.into(); let field_type: FieldType = field_rev.ty.into();
let key = CellDataCacheKey::new(field_rev, field_type.clone(), cell_str);
tracing::trace!( tracing::trace!(
"Update cell cache field_type: {}, cell_data: {:?}", "Cell cache update: field_type:{}, cell_str: {}, cell_data: {:?}",
field_type, field_type,
cell_str,
cell_data cell_data
); );
let key = CellDataCacheKey::new(field_rev, field_type, cell_str);
cell_data_cache.write().insert(key.as_ref(), cell_data); cell_data_cache.write().insert(key.as_ref(), cell_data);
} }
} }

View File

@ -18,7 +18,6 @@ impl UserDB {
} }
} }
#[tracing::instrument(level = "trace", skip(self))]
fn open_user_db_if_need(&self, user_id: &str) -> Result<Arc<ConnectionPool>, FlowyError> { fn open_user_db_if_need(&self, user_id: &str) -> Result<Arc<ConnectionPool>, FlowyError> {
if user_id.is_empty() { if user_id.is_empty() {
return Err(ErrorCode::UserIdIsEmpty.into()); return Err(ErrorCode::UserIdIsEmpty.into());