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;
CellListener? _cellListener;
ValueNotifier<T?>? _cellDataNotifier;
CellDataNotifier<T?>? _cellDataNotifier;
bool isListening = false;
VoidCallback? _onFieldChangedFn;
@ -170,8 +170,20 @@ class IGridCellController<T, D> extends Equatable {
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({
required void Function(T?) onCellChanged,
bool Function(T? oldValue, T? newValue)? listenWhenOnCellChanged,
VoidCallback? onCellFieldChanged,
}) {
if (isListening) {
@ -180,7 +192,10 @@ class IGridCellController<T, D> extends Equatable {
}
isListening = true;
_cellDataNotifier = ValueNotifier(_cellsCache.get(_cacheKey));
_cellDataNotifier = CellDataNotifier(
value: _cellsCache.get(_cacheKey),
listenWhen: listenWhenOnCellChanged,
);
_cellListener =
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.
/// It's useful when you call this method when user editing the [TextField].
/// The default debounce interval is 300 milliseconds.
void saveCellData(D data,
{bool deduplicate = false,
void Function(Option<FlowyError>)? resultCallback}) async {
void saveCellData(
D data, {
bool deduplicate = false,
void Function(Option<FlowyError>)? onFinish,
}) async {
_loadDataOperation?.cancel();
if (deduplicate) {
_loadDataOperation?.cancel();
_saveDataOperation?.cancel();
_saveDataOperation = Timer(const Duration(milliseconds: 300), () async {
final result = await _cellDataPersistence.save(data);
if (resultCallback != null) {
resultCallback(result);
}
onFinish?.call(result);
});
} else {
final result = await _cellDataPersistence.save(data);
if (resultCallback != null) {
resultCallback(result);
}
onFinish?.call(result);
}
}
@ -302,6 +314,7 @@ class IGridCellController<T, D> extends Equatable {
await _cellListener?.stop();
_loadDataOperation?.cancel();
_saveDataOperation?.cancel();
_cellDataNotifier?.dispose();
_cellDataNotifier = 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(
() => updateCalData(Some(newCalData), none()),
(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:freezed_annotation/freezed_annotation.dart';
import 'dart:async';
import 'package:dartz/dartz.dart';
import 'cell_service/cell_service.dart';
part 'number_cell_bloc.freezed.dart';
@ -20,20 +19,19 @@ class NumberCellBloc extends Bloc<NumberCellEvent, NumberCellState> {
initial: () {
_startListening();
},
didReceiveCellUpdate: (content) {
emit(state.copyWith(content: content));
didReceiveCellUpdate: (cellContent) {
emit(state.copyWith(cellContent: cellContent ?? ""));
},
updateCell: (text) {
cellController.saveCellData(text, resultCallback: (result) {
result.fold(
() => null,
(err) {
if (!isClosed) {
add(NumberCellEvent.didReceiveCellUpdate(right(err)));
}
},
);
});
if (state.cellContent != text) {
emit(state.copyWith(cellContent: text));
cellController.saveCellData(text, onFinish: (result) {
result.fold(
() {},
(err) => Log.error(err),
);
});
}
},
);
},
@ -51,13 +49,22 @@ class NumberCellBloc extends Bloc<NumberCellEvent, NumberCellState> {
}
void _startListening() {
_onCellChangedFn = cellController.startListening(
onCellChanged: ((cellContent) {
if (!isClosed) {
add(NumberCellEvent.didReceiveCellUpdate(left(cellContent ?? "")));
}
}),
);
_onCellChangedFn =
cellController.startListening(onCellChanged: ((cellContent) {
if (!isClosed) {
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 {
const factory NumberCellEvent.initial() = _Initial;
const factory NumberCellEvent.updateCell(String text) = _UpdateCell;
const factory NumberCellEvent.didReceiveCellUpdate(
Either<String, FlowyError> cellContent) = _DidReceiveCellUpdate;
const factory NumberCellEvent.didReceiveCellUpdate(String? cellContent) =
_DidReceiveCellUpdate;
}
@freezed
class NumberCellState with _$NumberCellState {
const factory NumberCellState({
required Either<String, FlowyError> content,
required String cellContent,
}) = _NumberCellState;
factory NumberCellState.initial(GridCellController context) {
final cellContent = context.getCellData() ?? "";
return NumberCellState(
content: left(cellContent),
cellContent: context.getCellData() ?? "",
);
}
}

View File

@ -29,8 +29,7 @@ class _NumberCellState extends GridFocusNodeCellState<GridNumberCell> {
final cellController = widget.cellControllerBuilder.build();
_cellBloc = getIt<NumberCellBloc>(param1: cellController)
..add(const NumberCellEvent.initial());
_controller =
TextEditingController(text: contentFromState(_cellBloc.state));
_controller = TextEditingController(text: _cellBloc.state.cellContent);
super.initState();
}
@ -41,9 +40,8 @@ class _NumberCellState extends GridFocusNodeCellState<GridNumberCell> {
child: MultiBlocListener(
listeners: [
BlocListener<NumberCellBloc, NumberCellState>(
listenWhen: (p, c) => p.content != c.content,
listener: (context, state) =>
_controller.text = contentFromState(state),
listenWhen: (p, c) => p.cellContent != c.cellContent,
listener: (context, state) => _controller.text = state.cellContent,
),
],
child: Padding(
@ -80,20 +78,16 @@ class _NumberCellState extends GridFocusNodeCellState<GridNumberCell> {
_delayOperation?.cancel();
_delayOperation = Timer(const Duration(milliseconds: 30), () {
if (_cellBloc.isClosed == false &&
_controller.text != contentFromState(_cellBloc.state)) {
_controller.text != _cellBloc.state.cellContent) {
_cellBloc.add(NumberCellEvent.updateCell(_controller.text));
}
});
}
}
String contentFromState(NumberCellState state) {
return state.content.fold((l) => l, (r) => "");
}
@override
String? onCopy() {
return _cellBloc.state.content.fold((content) => content, (r) => null);
return _cellBloc.state.cellContent;
}
@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::{strip_currency_symbol, NumberFormat, STRIP_SYMBOL};
use bytes::Bytes;
use flowy_error::{FlowyError, FlowyResult};
use flowy_error::FlowyResult;
use rust_decimal::Decimal;
use rusty_money::Money;
use std::str::FromStr;
@ -40,7 +40,8 @@ impl NumberCellData {
if num_str.chars().all(char::is_numeric) {
Self::from_format_str(&num_str, sign_positive, format)
} 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_u8(decoded_field_type as u8);
hasher.write(cell_str.as_bytes());
cell_str.hash(&mut hasher);
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() {
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());
}
Ok(cell_data)
@ -140,12 +146,13 @@ where
) {
if let Some(cell_data_cache) = self.cell_data_cache.as_ref() {
let field_type: FieldType = field_rev.ty.into();
let key = CellDataCacheKey::new(field_rev, field_type.clone(), cell_str);
tracing::trace!(
"Update cell cache field_type: {}, cell_data: {:?}",
"Cell cache update: field_type:{}, cell_str: {}, cell_data: {:?}",
field_type,
cell_str,
cell_data
);
let key = CellDataCacheKey::new(field_rev, field_type, cell_str);
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> {
if user_id.is_empty() {
return Err(ErrorCode::UserIdIsEmpty.into());