mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
fix: update the cell content if input is not valid data (#1652)
Co-authored-by: nathan <nathan@appflowy.io>
This commit is contained in:
parent
3ba3a8dc18
commit
e9f8796809
@ -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;
|
||||||
|
}
|
||||||
|
@ -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) {
|
||||||
|
@ -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() ?? "",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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());
|
||||||
|
Loading…
x
Reference in New Issue
Block a user