mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
feat: calculations (#4473)
* feat: initial calculation controller * fix: entities * feat: calculations * fix: review comments and support floats * fix: abstract business logic into calculations service * fix: clean calculation entities after merge * feat: react to changes to row/cell/field_type * chore: changes after merging main * feat: handle delete field * test: add grid calculations tests * fix: add validation + format numbers * refactor: get cell number * chore: bump collab * chore: fix clippy * chore: update docs * chore: update docs * chore: fmt * chore: fix flutter * chore: collab rev * fix: cleanup and hover to show * fix: localization * test: add basic rust test * fix: clippy * fix: support updating calculation on duplicate row --------- Co-authored-by: nathan <nathan@appflowy.io>
This commit is contained in:
parent
274742e334
commit
5cbc8b1e18
@ -39,12 +39,7 @@ void main() {
|
|||||||
await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);
|
await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);
|
||||||
|
|
||||||
// Invoke the field editor
|
// Invoke the field editor
|
||||||
await tester.tapGridFieldWithName('Type');
|
await tester.changeFieldTypeOfFieldWithName('Type', FieldType.Checkbox);
|
||||||
await tester.tapEditFieldButton();
|
|
||||||
|
|
||||||
await tester.tapSwitchFieldTypeButton();
|
|
||||||
await tester.selectFieldType(FieldType.Checkbox);
|
|
||||||
await tester.dismissFieldEditor();
|
|
||||||
|
|
||||||
await tester.assertFieldTypeWithFieldName(
|
await tester.assertFieldTypeWithFieldName(
|
||||||
'Type',
|
'Type',
|
||||||
|
@ -0,0 +1,107 @@
|
|||||||
|
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
||||||
|
import 'package:appflowy_backend/protobuf/flowy-folder/view.pbenum.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:integration_test/integration_test.dart';
|
||||||
|
|
||||||
|
import '../util/database_test_op.dart';
|
||||||
|
import '../util/util.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
|
group('Grid Calculations', () {
|
||||||
|
testWidgets('add calculation and update cell', (tester) async {
|
||||||
|
await tester.initializeAppFlowy();
|
||||||
|
await tester.tapGoButton();
|
||||||
|
|
||||||
|
await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);
|
||||||
|
|
||||||
|
// Change one Field to Number
|
||||||
|
await tester.changeFieldTypeOfFieldWithName('Type', FieldType.Number);
|
||||||
|
|
||||||
|
expect(find.text('Calculate'), findsOneWidget);
|
||||||
|
|
||||||
|
await tester.changeCalculateAtIndex(1, CalculationType.Sum);
|
||||||
|
|
||||||
|
// Enter values in cells
|
||||||
|
await tester.editCell(
|
||||||
|
rowIndex: 0,
|
||||||
|
fieldType: FieldType.Number,
|
||||||
|
input: '100',
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.editCell(
|
||||||
|
rowIndex: 1,
|
||||||
|
fieldType: FieldType.Number,
|
||||||
|
input: '100',
|
||||||
|
);
|
||||||
|
|
||||||
|
// Dismiss edit cell
|
||||||
|
await tester.sendKeyDownEvent(LogicalKeyboardKey.enter);
|
||||||
|
|
||||||
|
await tester.pumpAndSettle(const Duration(seconds: 1));
|
||||||
|
|
||||||
|
expect(find.text('200'), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('add calculations and remove row', (tester) async {
|
||||||
|
await tester.initializeAppFlowy();
|
||||||
|
await tester.tapGoButton();
|
||||||
|
|
||||||
|
await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);
|
||||||
|
|
||||||
|
// Change two Fields to Number
|
||||||
|
await tester.changeFieldTypeOfFieldWithName('Type', FieldType.Number);
|
||||||
|
await tester.changeFieldTypeOfFieldWithName('Done', FieldType.Number);
|
||||||
|
|
||||||
|
expect(find.text('Calculate'), findsNWidgets(2));
|
||||||
|
|
||||||
|
await tester.changeCalculateAtIndex(1, CalculationType.Sum);
|
||||||
|
await tester.changeCalculateAtIndex(2, CalculationType.Min);
|
||||||
|
|
||||||
|
// Enter values in cells
|
||||||
|
await tester.editCell(
|
||||||
|
rowIndex: 0,
|
||||||
|
fieldType: FieldType.Number,
|
||||||
|
input: '100',
|
||||||
|
);
|
||||||
|
await tester.editCell(
|
||||||
|
rowIndex: 1,
|
||||||
|
fieldType: FieldType.Number,
|
||||||
|
input: '150',
|
||||||
|
);
|
||||||
|
await tester.editCell(
|
||||||
|
rowIndex: 0,
|
||||||
|
fieldType: FieldType.Number,
|
||||||
|
input: '50',
|
||||||
|
cellIndex: 1,
|
||||||
|
);
|
||||||
|
await tester.editCell(
|
||||||
|
rowIndex: 1,
|
||||||
|
fieldType: FieldType.Number,
|
||||||
|
input: '100',
|
||||||
|
cellIndex: 1,
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
// Dismiss edit cell
|
||||||
|
await tester.sendKeyDownEvent(LogicalKeyboardKey.enter);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.text('250'), findsOneWidget);
|
||||||
|
expect(find.text('50'), findsNWidgets(2));
|
||||||
|
|
||||||
|
// Delete 1st row
|
||||||
|
await tester.hoverOnFirstRowOfGrid();
|
||||||
|
await tester.tapRowMenuButtonInGrid();
|
||||||
|
await tester.tapDeleteOnRowMenu();
|
||||||
|
|
||||||
|
await tester.pumpAndSettle(const Duration(seconds: 1));
|
||||||
|
|
||||||
|
expect(find.text('150'), findsNWidgets(2));
|
||||||
|
expect(find.text('100'), findsNWidgets(2));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
@ -1,5 +1,8 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:appflowy/plugins/database/application/calculations/calculation_type_ext.dart';
|
||||||
|
import 'package:appflowy/plugins/database/grid/presentation/widgets/calculations/calculate_cell.dart';
|
||||||
|
import 'package:appflowy/plugins/database/grid/presentation/widgets/calculations/calculation_type_item.dart';
|
||||||
import 'package:appflowy/plugins/database/grid/presentation/widgets/common/type_option_separator.dart';
|
import 'package:appflowy/plugins/database/grid/presentation/widgets/common/type_option_separator.dart';
|
||||||
import 'package:appflowy/plugins/database/grid/presentation/widgets/header/type_option/number.dart';
|
import 'package:appflowy/plugins/database/grid/presentation/widgets/header/type_option/number.dart';
|
||||||
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/checkbox.dart';
|
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/checkbox.dart';
|
||||||
@ -720,6 +723,31 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
|||||||
await pumpAndSettle();
|
await pumpAndSettle();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> changeFieldTypeOfFieldWithName(
|
||||||
|
String name,
|
||||||
|
FieldType type,
|
||||||
|
) async {
|
||||||
|
await tapGridFieldWithName(name);
|
||||||
|
await tapEditFieldButton();
|
||||||
|
|
||||||
|
await tapSwitchFieldTypeButton();
|
||||||
|
await selectFieldType(type);
|
||||||
|
await dismissFieldEditor();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> changeCalculateAtIndex(int index, CalculationType type) async {
|
||||||
|
await tap(find.byType(CalculateCell).at(index));
|
||||||
|
await pumpAndSettle();
|
||||||
|
|
||||||
|
await tap(
|
||||||
|
find.descendant(
|
||||||
|
of: find.byType(CalculationTypeItem),
|
||||||
|
matching: find.text(type.label),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await pumpAndSettle();
|
||||||
|
}
|
||||||
|
|
||||||
/// Should call [tapGridFieldWithName] first.
|
/// Should call [tapGridFieldWithName] first.
|
||||||
Future<void> tapEditFieldButton() async {
|
Future<void> tapEditFieldButton() async {
|
||||||
await tapButtonWithName(LocaleKeys.grid_field_editProperty.tr());
|
await tapButtonWithName(LocaleKeys.grid_field_editProperty.tr());
|
||||||
|
@ -0,0 +1,18 @@
|
|||||||
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
|
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
|
||||||
|
extension CalcTypeLabel on CalculationType {
|
||||||
|
String get label => switch (this) {
|
||||||
|
CalculationType.Average =>
|
||||||
|
LocaleKeys.grid_calculationTypeLabel_average.tr(),
|
||||||
|
CalculationType.Max => LocaleKeys.grid_calculationTypeLabel_max.tr(),
|
||||||
|
CalculationType.Median =>
|
||||||
|
LocaleKeys.grid_calculationTypeLabel_median.tr(),
|
||||||
|
CalculationType.Min => LocaleKeys.grid_calculationTypeLabel_min.tr(),
|
||||||
|
CalculationType.Sum => LocaleKeys.grid_calculationTypeLabel_sum.tr(),
|
||||||
|
_ => throw UnimplementedError(
|
||||||
|
'Label for $this has not been implemented',
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
@ -0,0 +1,54 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
|
import 'package:appflowy/core/notification/grid_notification.dart';
|
||||||
|
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
||||||
|
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||||
|
import 'package:dartz/dartz.dart';
|
||||||
|
import 'package:flowy_infra/notifier.dart';
|
||||||
|
|
||||||
|
typedef UpdateCalculationValue
|
||||||
|
= Either<CalculationChangesetNotificationPB, FlowyError>;
|
||||||
|
|
||||||
|
class CalculationsListener {
|
||||||
|
CalculationsListener({required this.viewId});
|
||||||
|
|
||||||
|
final String viewId;
|
||||||
|
|
||||||
|
PublishNotifier<UpdateCalculationValue>? _calculationNotifier =
|
||||||
|
PublishNotifier();
|
||||||
|
DatabaseNotificationListener? _listener;
|
||||||
|
|
||||||
|
void start({
|
||||||
|
required void Function(UpdateCalculationValue) onCalculationChanged,
|
||||||
|
}) {
|
||||||
|
_calculationNotifier?.addPublishListener(onCalculationChanged);
|
||||||
|
_listener = DatabaseNotificationListener(
|
||||||
|
objectId: viewId,
|
||||||
|
handler: _handler,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handler(
|
||||||
|
DatabaseNotification ty,
|
||||||
|
Either<Uint8List, FlowyError> result,
|
||||||
|
) {
|
||||||
|
switch (ty) {
|
||||||
|
case DatabaseNotification.DidUpdateCalculation:
|
||||||
|
_calculationNotifier?.value = result.fold(
|
||||||
|
(payload) => left(
|
||||||
|
CalculationChangesetNotificationPB.fromBuffer(payload),
|
||||||
|
),
|
||||||
|
(err) => right(err),
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> stop() async {
|
||||||
|
await _listener?.stop();
|
||||||
|
_calculationNotifier?.dispose();
|
||||||
|
_calculationNotifier = null;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,46 @@
|
|||||||
|
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
||||||
|
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
||||||
|
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||||
|
import 'package:dartz/dartz.dart';
|
||||||
|
|
||||||
|
class CalculationsBackendService {
|
||||||
|
const CalculationsBackendService({required this.viewId});
|
||||||
|
|
||||||
|
final String viewId;
|
||||||
|
|
||||||
|
// Get Calculations (initial fetch)
|
||||||
|
Future<Either<RepeatedCalculationsPB, FlowyError>> getCalculations() async {
|
||||||
|
final payload = DatabaseViewIdPB()..value = viewId;
|
||||||
|
|
||||||
|
return DatabaseEventGetAllCalculations(payload).send();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> updateCalculation(
|
||||||
|
String fieldId,
|
||||||
|
CalculationType type, {
|
||||||
|
String? calculationId,
|
||||||
|
}) async {
|
||||||
|
final payload = UpdateCalculationChangesetPB()
|
||||||
|
..viewId = viewId
|
||||||
|
..fieldId = fieldId
|
||||||
|
..calculationType = type;
|
||||||
|
|
||||||
|
if (calculationId != null) {
|
||||||
|
payload.calculationId = calculationId;
|
||||||
|
}
|
||||||
|
|
||||||
|
await DatabaseEventUpdateCalculation(payload).send();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> removeCalculation(
|
||||||
|
String fieldId,
|
||||||
|
String calculationId,
|
||||||
|
) async {
|
||||||
|
final payload = RemoveCalculationChangesetPB()
|
||||||
|
..viewId = viewId
|
||||||
|
..fieldId = fieldId
|
||||||
|
..calculationId = calculationId;
|
||||||
|
|
||||||
|
await DatabaseEventRemoveCalculation(payload).send();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,182 @@
|
|||||||
|
import 'package:appflowy/plugins/database/application/calculations/calculations_listener.dart';
|
||||||
|
import 'package:appflowy/plugins/database/application/calculations/calculations_service.dart';
|
||||||
|
import 'package:appflowy/plugins/database/application/field/field_controller.dart';
|
||||||
|
import 'package:appflowy/plugins/database/application/field/field_info.dart';
|
||||||
|
import 'package:appflowy/workspace/application/view/view_service.dart';
|
||||||
|
import 'package:appflowy_backend/protobuf/flowy-database2/calculation_entities.pb.dart';
|
||||||
|
import 'package:appflowy_backend/protobuf/flowy-database2/field_settings_entities.pbenum.dart';
|
||||||
|
import 'package:bloc/bloc.dart';
|
||||||
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
|
|
||||||
|
part 'calculations_bloc.freezed.dart';
|
||||||
|
|
||||||
|
class CalculationsBloc extends Bloc<CalculationsEvent, CalculationsState> {
|
||||||
|
CalculationsBloc({
|
||||||
|
required this.viewId,
|
||||||
|
required FieldController fieldController,
|
||||||
|
}) : _fieldController = fieldController,
|
||||||
|
_calculationsListener = CalculationsListener(viewId: viewId),
|
||||||
|
_calculationsService = CalculationsBackendService(viewId: viewId),
|
||||||
|
super(CalculationsState.initial()) {
|
||||||
|
_dispatch();
|
||||||
|
}
|
||||||
|
|
||||||
|
final String viewId;
|
||||||
|
final FieldController _fieldController;
|
||||||
|
final CalculationsListener _calculationsListener;
|
||||||
|
late final CalculationsBackendService _calculationsService;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> close() async {
|
||||||
|
_fieldController.removeListener(onFieldsListener: _onReceiveFields);
|
||||||
|
await super.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _dispatch() {
|
||||||
|
on<CalculationsEvent>((event, emit) async {
|
||||||
|
await event.when(
|
||||||
|
started: () async {
|
||||||
|
_startListening();
|
||||||
|
await _getAllCalculations();
|
||||||
|
|
||||||
|
add(
|
||||||
|
CalculationsEvent.didReceiveFieldUpdate(
|
||||||
|
_fieldController.fieldInfos,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
didReceiveFieldUpdate: (fields) async {
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
fields: fields
|
||||||
|
.where(
|
||||||
|
(e) =>
|
||||||
|
e.visibility != null &&
|
||||||
|
e.visibility != FieldVisibility.AlwaysHidden,
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
didReceiveCalculationsUpdate: (calculationsMap) async {
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
calculationsByFieldId: calculationsMap,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
updateCalculationType: (fieldId, type, calculationId) async {
|
||||||
|
await _calculationsService.updateCalculation(
|
||||||
|
fieldId,
|
||||||
|
type,
|
||||||
|
calculationId: calculationId,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
removeCalculation: (fieldId, calculationId) async {
|
||||||
|
await _calculationsService.removeCalculation(fieldId, calculationId);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _startListening() {
|
||||||
|
_fieldController.addListener(
|
||||||
|
listenWhen: () => !isClosed,
|
||||||
|
onReceiveFields: _onReceiveFields,
|
||||||
|
);
|
||||||
|
|
||||||
|
_calculationsListener.start(
|
||||||
|
onCalculationChanged: (changesetOrFailure) {
|
||||||
|
if (isClosed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
changesetOrFailure.fold(
|
||||||
|
(changeset) {
|
||||||
|
final calculationsMap = {...state.calculationsByFieldId};
|
||||||
|
if (changeset.insertCalculations.isNotEmpty) {
|
||||||
|
for (final insert in changeset.insertCalculations) {
|
||||||
|
calculationsMap[insert.fieldId] = insert;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changeset.updateCalculations.isNotEmpty) {
|
||||||
|
for (final update in changeset.updateCalculations) {
|
||||||
|
calculationsMap.removeWhere((key, _) => key == update.fieldId);
|
||||||
|
calculationsMap.addAll({update.fieldId: update});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changeset.deleteCalculations.isNotEmpty) {
|
||||||
|
for (final delete in changeset.deleteCalculations) {
|
||||||
|
calculationsMap.removeWhere((key, _) => key == delete.fieldId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
add(
|
||||||
|
CalculationsEvent.didReceiveCalculationsUpdate(
|
||||||
|
calculationsMap,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
(_) => null,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onReceiveFields(List<FieldInfo> fields) =>
|
||||||
|
add(CalculationsEvent.didReceiveFieldUpdate(fields));
|
||||||
|
|
||||||
|
Future<void> _getAllCalculations() async {
|
||||||
|
final calculationsOrFailure = await _calculationsService.getCalculations();
|
||||||
|
|
||||||
|
final RepeatedCalculationsPB? calculations =
|
||||||
|
calculationsOrFailure.getLeftOrNull();
|
||||||
|
if (calculations != null) {
|
||||||
|
final calculationMap = <String, CalculationPB>{};
|
||||||
|
for (final calculation in calculations.items) {
|
||||||
|
calculationMap[calculation.fieldId] = calculation;
|
||||||
|
}
|
||||||
|
|
||||||
|
add(CalculationsEvent.didReceiveCalculationsUpdate(calculationMap));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
class CalculationsEvent with _$CalculationsEvent {
|
||||||
|
const factory CalculationsEvent.started() = _Started;
|
||||||
|
|
||||||
|
const factory CalculationsEvent.didReceiveFieldUpdate(
|
||||||
|
List<FieldInfo> fields,
|
||||||
|
) = _DidReceiveFieldUpdate;
|
||||||
|
|
||||||
|
const factory CalculationsEvent.didReceiveCalculationsUpdate(
|
||||||
|
Map<String, CalculationPB> calculationsByFieldId,
|
||||||
|
) = _DidReceiveCalculationsUpdate;
|
||||||
|
|
||||||
|
const factory CalculationsEvent.updateCalculationType(
|
||||||
|
String fieldId,
|
||||||
|
CalculationType type, {
|
||||||
|
@Default(null) String? calculationId,
|
||||||
|
}) = _UpdateCalculationType;
|
||||||
|
|
||||||
|
const factory CalculationsEvent.removeCalculation(
|
||||||
|
String fieldId,
|
||||||
|
String calculationId,
|
||||||
|
) = _RemoveCalculation;
|
||||||
|
}
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
class CalculationsState with _$CalculationsState {
|
||||||
|
const factory CalculationsState({
|
||||||
|
required List<FieldInfo> fields,
|
||||||
|
required Map<String, CalculationPB> calculationsByFieldId,
|
||||||
|
}) = _CalculationsState;
|
||||||
|
|
||||||
|
factory CalculationsState.initial() => const CalculationsState(
|
||||||
|
fields: [],
|
||||||
|
calculationsByFieldId: {},
|
||||||
|
);
|
||||||
|
}
|
@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
|||||||
|
|
||||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
import 'package:appflowy/plugins/database/application/row/row_service.dart';
|
import 'package:appflowy/plugins/database/application/row/row_service.dart';
|
||||||
|
import 'package:appflowy/plugins/database/grid/presentation/widgets/calculations/calculations_row.dart';
|
||||||
import 'package:appflowy/plugins/database/grid/presentation/widgets/toolbar/grid_setting_bar.dart';
|
import 'package:appflowy/plugins/database/grid/presentation/widgets/toolbar/grid_setting_bar.dart';
|
||||||
import 'package:appflowy/plugins/database/tab_bar/desktop/setting_menu.dart';
|
import 'package:appflowy/plugins/database/tab_bar/desktop/setting_menu.dart';
|
||||||
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_builder.dart';
|
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_builder.dart';
|
||||||
@ -12,8 +13,7 @@ import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
|||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flowy_infra/theme_extension.dart';
|
import 'package:flowy_infra/theme_extension.dart';
|
||||||
import 'package:flowy_infra_ui/flowy_infra_ui_web.dart';
|
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
import 'package:flowy_infra_ui/style_widget/scrolling/styled_scroll_bar.dart';
|
|
||||||
import 'package:flowy_infra_ui/style_widget/scrolling/styled_scrollview.dart';
|
import 'package:flowy_infra_ui/style_widget/scrolling/styled_scrollview.dart';
|
||||||
import 'package:flowy_infra_ui/widget/error_page.dart';
|
import 'package:flowy_infra_ui/widget/error_page.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
@ -303,17 +303,22 @@ class _GridRows extends StatelessWidget {
|
|||||||
GridState state,
|
GridState state,
|
||||||
List<RowInfo> rowInfos,
|
List<RowInfo> rowInfos,
|
||||||
) {
|
) {
|
||||||
final children = [
|
final children = rowInfos.mapIndexed((index, rowInfo) {
|
||||||
...rowInfos.mapIndexed((index, rowInfo) {
|
return _renderRow(
|
||||||
return _renderRow(
|
context,
|
||||||
context,
|
rowInfo.rowId,
|
||||||
rowInfo.rowId,
|
isDraggable: state.reorderable,
|
||||||
isDraggable: state.reorderable,
|
index: index,
|
||||||
index: index,
|
);
|
||||||
);
|
}).toList()
|
||||||
}),
|
..add(const GridRowBottomBar(key: Key('grid_footer')))
|
||||||
const GridRowBottomBar(key: Key('gridFooter')),
|
..add(
|
||||||
];
|
GridCalculationsRow(
|
||||||
|
key: const Key('grid_calculations'),
|
||||||
|
viewId: viewId,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
return ReorderableListView.builder(
|
return ReorderableListView.builder(
|
||||||
/// This is a workaround related to
|
/// This is a workaround related to
|
||||||
/// https://github.com/flutter/flutter/issues/25652
|
/// https://github.com/flutter/flutter/issues/25652
|
||||||
@ -434,9 +439,10 @@ class _GridFooter extends StatelessWidget {
|
|||||||
child: RichText(
|
child: RichText(
|
||||||
text: TextSpan(
|
text: TextSpan(
|
||||||
text: rowCountString(),
|
text: rowCountString(),
|
||||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
style: Theme.of(context)
|
||||||
color: Theme.of(context).hintColor,
|
.textTheme
|
||||||
),
|
.bodyMedium!
|
||||||
|
.copyWith(color: Theme.of(context).hintColor),
|
||||||
children: [
|
children: [
|
||||||
TextSpan(
|
TextSpan(
|
||||||
text: ' $rowCount',
|
text: ' $rowCount',
|
||||||
|
@ -0,0 +1,137 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||||
|
import 'package:appflowy/plugins/database/application/calculations/calculation_type_ext.dart';
|
||||||
|
import 'package:appflowy/plugins/database/application/field/field_info.dart';
|
||||||
|
import 'package:appflowy/plugins/database/grid/application/calculations/calculations_bloc.dart';
|
||||||
|
import 'package:appflowy/plugins/database/grid/presentation/widgets/calculations/calculation_selector.dart';
|
||||||
|
import 'package:appflowy/plugins/database/grid/presentation/widgets/calculations/calculation_type_item.dart';
|
||||||
|
import 'package:appflowy/plugins/database/grid/presentation/widgets/calculations/remove_calculation_button.dart';
|
||||||
|
import 'package:appflowy_backend/protobuf/flowy-database2/calculation_entities.pb.dart';
|
||||||
|
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';
|
||||||
|
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||||
|
import 'package:flowy_infra/theme_extension.dart';
|
||||||
|
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
|
class CalculateCell extends StatefulWidget {
|
||||||
|
const CalculateCell({
|
||||||
|
super.key,
|
||||||
|
required this.fieldInfo,
|
||||||
|
required this.width,
|
||||||
|
this.calculation,
|
||||||
|
});
|
||||||
|
|
||||||
|
final FieldInfo fieldInfo;
|
||||||
|
final double width;
|
||||||
|
final CalculationPB? calculation;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<CalculateCell> createState() => _CalculateCellState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CalculateCellState extends State<CalculateCell> {
|
||||||
|
bool isSelected = false;
|
||||||
|
|
||||||
|
void setIsSelected(bool selected) => setState(() => isSelected = selected);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return SizedBox(
|
||||||
|
height: 35,
|
||||||
|
width: widget.width,
|
||||||
|
child: AppFlowyPopover(
|
||||||
|
constraints: BoxConstraints.loose(const Size(150, 200)),
|
||||||
|
direction: PopoverDirection.bottomWithCenterAligned,
|
||||||
|
onClose: () => setIsSelected(false),
|
||||||
|
popupBuilder: (_) {
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
if (mounted) {
|
||||||
|
setIsSelected(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
if (widget.calculation != null)
|
||||||
|
RemoveCalculationButton(
|
||||||
|
onTap: () => context.read<CalculationsBloc>().add(
|
||||||
|
CalculationsEvent.removeCalculation(
|
||||||
|
widget.fieldInfo.id,
|
||||||
|
widget.calculation!.id,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
...CalculationType.values.map(
|
||||||
|
(type) => CalculationTypeItem(
|
||||||
|
type: type,
|
||||||
|
onTap: () {
|
||||||
|
if (type != widget.calculation?.calculationType) {
|
||||||
|
context.read<CalculationsBloc>().add(
|
||||||
|
CalculationsEvent.updateCalculationType(
|
||||||
|
widget.fieldInfo.id,
|
||||||
|
type,
|
||||||
|
calculationId: widget.calculation?.id,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: widget.fieldInfo.fieldType == FieldType.Number
|
||||||
|
? widget.calculation != null
|
||||||
|
? _showCalculateValue(context)
|
||||||
|
: CalculationSelector(isSelected: isSelected)
|
||||||
|
: const SizedBox.shrink(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _showCalculateValue(BuildContext context) {
|
||||||
|
return FlowyButton(
|
||||||
|
radius: BorderRadius.zero,
|
||||||
|
hoverColor: AFThemeExtension.of(context).lightGreyHover,
|
||||||
|
text: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
Flexible(
|
||||||
|
child: FlowyText(
|
||||||
|
widget.calculation!.calculationType.label,
|
||||||
|
color: Theme.of(context).hintColor,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (widget.calculation!.value.isNotEmpty) ...[
|
||||||
|
const HSpace(8),
|
||||||
|
Flexible(
|
||||||
|
child: FlowyText(
|
||||||
|
_withoutTrailingZeros(widget.calculation!.value),
|
||||||
|
color: AFThemeExtension.of(context).textColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
const HSpace(8),
|
||||||
|
FlowySvg(
|
||||||
|
FlowySvgs.arrow_down_s,
|
||||||
|
color: Theme.of(context).hintColor,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String _withoutTrailingZeros(String value) {
|
||||||
|
final regex = RegExp(r'^(\d+(?:\.\d*?[1-9](?=0|\b))?)\.?0*$');
|
||||||
|
if (regex.hasMatch(value)) {
|
||||||
|
final match = regex.firstMatch(value)!;
|
||||||
|
return match.group(1)!;
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,61 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||||
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flowy_infra/theme_extension.dart';
|
||||||
|
import 'package:flowy_infra_ui/style_widget/button.dart';
|
||||||
|
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||||
|
import 'package:flowy_infra_ui/widget/spacing.dart';
|
||||||
|
|
||||||
|
class CalculationSelector extends StatefulWidget {
|
||||||
|
const CalculationSelector({
|
||||||
|
super.key,
|
||||||
|
required this.isSelected,
|
||||||
|
});
|
||||||
|
|
||||||
|
final bool isSelected;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<CalculationSelector> createState() => _CalculationSelectorState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CalculationSelectorState extends State<CalculationSelector> {
|
||||||
|
bool _isHovering = false;
|
||||||
|
|
||||||
|
void _setHovering(bool isHovering) =>
|
||||||
|
setState(() => _isHovering = isHovering);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return MouseRegion(
|
||||||
|
onEnter: (_) => _setHovering(true),
|
||||||
|
onExit: (_) => _setHovering(false),
|
||||||
|
child: AnimatedOpacity(
|
||||||
|
duration: const Duration(milliseconds: 200),
|
||||||
|
opacity: widget.isSelected || _isHovering ? 1 : 0,
|
||||||
|
child: FlowyButton(
|
||||||
|
radius: BorderRadius.zero,
|
||||||
|
hoverColor: AFThemeExtension.of(context).lightGreyHover,
|
||||||
|
text: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
Flexible(
|
||||||
|
child: FlowyText(
|
||||||
|
LocaleKeys.grid_calculate.tr(),
|
||||||
|
color: Theme.of(context).hintColor,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const HSpace(8),
|
||||||
|
FlowySvg(
|
||||||
|
FlowySvgs.arrow_down_s,
|
||||||
|
color: Theme.of(context).hintColor,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,33 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'package:appflowy/plugins/database/application/calculations/calculation_type_ext.dart';
|
||||||
|
import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';
|
||||||
|
import 'package:appflowy_backend/protobuf/flowy-database2/calculation_entities.pbenum.dart';
|
||||||
|
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||||
|
import 'package:flowy_infra_ui/style_widget/button.dart';
|
||||||
|
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||||
|
|
||||||
|
class CalculationTypeItem extends StatelessWidget {
|
||||||
|
const CalculationTypeItem({
|
||||||
|
super.key,
|
||||||
|
required this.type,
|
||||||
|
required this.onTap,
|
||||||
|
});
|
||||||
|
|
||||||
|
final CalculationType type;
|
||||||
|
final VoidCallback onTap;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return SizedBox(
|
||||||
|
height: GridSize.popoverItemHeight,
|
||||||
|
child: FlowyButton(
|
||||||
|
text: FlowyText.medium(type.label, overflow: TextOverflow.ellipsis),
|
||||||
|
onTap: () {
|
||||||
|
onTap();
|
||||||
|
PopoverContainer.of(context).close();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,46 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'package:appflowy/plugins/database/grid/application/calculations/calculations_bloc.dart';
|
||||||
|
import 'package:appflowy/plugins/database/grid/application/grid_bloc.dart';
|
||||||
|
import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';
|
||||||
|
import 'package:appflowy/plugins/database/grid/presentation/widgets/calculations/calculate_cell.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
|
class GridCalculationsRow extends StatelessWidget {
|
||||||
|
const GridCalculationsRow({super.key, required this.viewId});
|
||||||
|
|
||||||
|
final String viewId;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final gridBloc = context.read<GridBloc>();
|
||||||
|
|
||||||
|
return BlocProvider(
|
||||||
|
create: (context) => CalculationsBloc(
|
||||||
|
viewId: gridBloc.databaseController.viewId,
|
||||||
|
fieldController: gridBloc.databaseController.fieldController,
|
||||||
|
)..add(const CalculationsEvent.started()),
|
||||||
|
child: BlocBuilder<CalculationsBloc, CalculationsState>(
|
||||||
|
builder: (context, state) {
|
||||||
|
return Padding(
|
||||||
|
padding: GridSize.contentInsets,
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
...state.fields.map(
|
||||||
|
(field) => CalculateCell(
|
||||||
|
key: Key(
|
||||||
|
'${field.id}-${state.calculationsByFieldId[field.id]?.id}',
|
||||||
|
),
|
||||||
|
width: field.fieldSettings!.width.toDouble(),
|
||||||
|
fieldInfo: field,
|
||||||
|
calculation: state.calculationsByFieldId[field.id],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,34 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
|
import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';
|
||||||
|
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flowy_infra_ui/style_widget/button.dart';
|
||||||
|
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||||
|
|
||||||
|
class RemoveCalculationButton extends StatelessWidget {
|
||||||
|
const RemoveCalculationButton({
|
||||||
|
super.key,
|
||||||
|
required this.onTap,
|
||||||
|
});
|
||||||
|
|
||||||
|
final VoidCallback onTap;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return SizedBox(
|
||||||
|
height: GridSize.popoverItemHeight,
|
||||||
|
child: FlowyButton(
|
||||||
|
text: FlowyText.medium(
|
||||||
|
LocaleKeys.grid_calculationTypeLabel_none.tr(),
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
onTap();
|
||||||
|
PopoverContainer.of(context).close();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,13 +1,13 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
import 'package:appflowy/plugins/database/grid/application/grid_bloc.dart';
|
import 'package:appflowy/plugins/database/grid/application/grid_bloc.dart';
|
||||||
import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';
|
import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flowy_infra/theme_extension.dart';
|
import 'package:flowy_infra/theme_extension.dart';
|
||||||
|
|
||||||
import 'package:flowy_infra_ui/style_widget/button.dart';
|
import 'package:flowy_infra_ui/style_widget/button.dart';
|
||||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
class GridAddRowButton extends StatelessWidget {
|
class GridAddRowButton extends StatelessWidget {
|
||||||
@ -16,6 +16,12 @@ class GridAddRowButton extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return FlowyButton(
|
return FlowyButton(
|
||||||
|
radius: BorderRadius.zero,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border(
|
||||||
|
bottom: BorderSide(color: Theme.of(context).dividerColor),
|
||||||
|
),
|
||||||
|
),
|
||||||
text: FlowyText(
|
text: FlowyText(
|
||||||
LocaleKeys.grid_row_newRow.tr(),
|
LocaleKeys.grid_row_newRow.tr(),
|
||||||
color: Theme.of(context).hintColor,
|
color: Theme.of(context).hintColor,
|
||||||
@ -38,7 +44,7 @@ class GridRowBottomBar extends StatelessWidget {
|
|||||||
return Container(
|
return Container(
|
||||||
padding: GridSize.footerContentInsets,
|
padding: GridSize.footerContentInsets,
|
||||||
height: GridSize.footerHeight,
|
height: GridSize.footerHeight,
|
||||||
margin: const EdgeInsets.only(bottom: 8, top: 8),
|
// margin: const EdgeInsets.only(bottom: 8, top: 8),
|
||||||
child: const GridAddRowButton(),
|
child: const GridAddRowButton(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import 'package:appflowy/plugins/database/application/cell/cell_controller.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||||
import "package:appflowy/generated/locale_keys.g.dart";
|
import "package:appflowy/generated/locale_keys.g.dart";
|
||||||
|
import 'package:appflowy/plugins/database/application/cell/cell_controller.dart';
|
||||||
import 'package:appflowy/plugins/database/application/field/field_controller.dart';
|
import 'package:appflowy/plugins/database/application/field/field_controller.dart';
|
||||||
import 'package:appflowy/plugins/database/application/row/row_controller.dart';
|
import 'package:appflowy/plugins/database/application/row/row_controller.dart';
|
||||||
import 'package:appflowy/plugins/database/application/row/row_service.dart';
|
import 'package:appflowy/plugins/database/application/row/row_service.dart';
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:appflowy/plugins/database/application/database_controller.dart';
|
import 'package:appflowy/plugins/database/application/database_controller.dart';
|
||||||
import 'package:appflowy/plugins/database/application/row/row_controller.dart';
|
import 'package:appflowy/plugins/database/application/row/row_controller.dart';
|
||||||
import 'package:appflowy/plugins/database/grid/application/row/row_detail_bloc.dart';
|
import 'package:appflowy/plugins/database/grid/application/row/row_detail_bloc.dart';
|
||||||
@ -5,10 +7,10 @@ import 'package:appflowy/plugins/database/widgets/row/row_document.dart';
|
|||||||
import 'package:appflowy/startup/startup.dart';
|
import 'package:appflowy/startup/startup.dart';
|
||||||
import 'package:appflowy/user/application/reminder/reminder_bloc.dart';
|
import 'package:appflowy/user/application/reminder/reminder_bloc.dart';
|
||||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
import '../cell/editable_cell_builder.dart';
|
import '../cell/editable_cell_builder.dart';
|
||||||
|
|
||||||
import 'row_banner.dart';
|
import 'row_banner.dart';
|
||||||
import 'row_property.dart';
|
import 'row_property.dart';
|
||||||
|
|
||||||
@ -49,9 +51,7 @@ class _RowDetailPageState extends State<RowDetailPage> {
|
|||||||
rowController: widget.rowController,
|
rowController: widget.rowController,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
BlocProvider.value(
|
BlocProvider.value(value: getIt<ReminderBloc>()),
|
||||||
value: getIt<ReminderBloc>(),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
child: ListView(
|
child: ListView(
|
||||||
controller: scrollController,
|
controller: scrollController,
|
||||||
|
15
frontend/appflowy_tauri/src-tauri/Cargo.lock
generated
15
frontend/appflowy_tauri/src-tauri/Cargo.lock
generated
@ -816,7 +816,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "collab"
|
name = "collab"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d00b477a9b844d86b5caeff573ca395dc5bf7198#d00b477a9b844d86b5caeff573ca395dc5bf7198"
|
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a6b0932581b4544a0800a0451b9522e6caab5570#a6b0932581b4544a0800a0451b9522e6caab5570"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
@ -838,7 +838,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "collab-database"
|
name = "collab-database"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d00b477a9b844d86b5caeff573ca395dc5bf7198#d00b477a9b844d86b5caeff573ca395dc5bf7198"
|
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a6b0932581b4544a0800a0451b9522e6caab5570#a6b0932581b4544a0800a0451b9522e6caab5570"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
@ -867,7 +867,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "collab-document"
|
name = "collab-document"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d00b477a9b844d86b5caeff573ca395dc5bf7198#d00b477a9b844d86b5caeff573ca395dc5bf7198"
|
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a6b0932581b4544a0800a0451b9522e6caab5570#a6b0932581b4544a0800a0451b9522e6caab5570"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"collab",
|
"collab",
|
||||||
@ -886,7 +886,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "collab-entity"
|
name = "collab-entity"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d00b477a9b844d86b5caeff573ca395dc5bf7198#d00b477a9b844d86b5caeff573ca395dc5bf7198"
|
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a6b0932581b4544a0800a0451b9522e6caab5570#a6b0932581b4544a0800a0451b9522e6caab5570"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bytes",
|
"bytes",
|
||||||
@ -901,7 +901,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "collab-folder"
|
name = "collab-folder"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d00b477a9b844d86b5caeff573ca395dc5bf7198#d00b477a9b844d86b5caeff573ca395dc5bf7198"
|
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a6b0932581b4544a0800a0451b9522e6caab5570#a6b0932581b4544a0800a0451b9522e6caab5570"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"chrono",
|
"chrono",
|
||||||
@ -938,7 +938,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "collab-plugins"
|
name = "collab-plugins"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d00b477a9b844d86b5caeff573ca395dc5bf7198#d00b477a9b844d86b5caeff573ca395dc5bf7198"
|
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a6b0932581b4544a0800a0451b9522e6caab5570#a6b0932581b4544a0800a0451b9522e6caab5570"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-stream",
|
"async-stream",
|
||||||
@ -977,7 +977,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "collab-user"
|
name = "collab-user"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d00b477a9b844d86b5caeff573ca395dc5bf7198#d00b477a9b844d86b5caeff573ca395dc5bf7198"
|
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a6b0932581b4544a0800a0451b9522e6caab5570#a6b0932581b4544a0800a0451b9522e6caab5570"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"collab",
|
"collab",
|
||||||
@ -1802,6 +1802,7 @@ dependencies = [
|
|||||||
"tokio",
|
"tokio",
|
||||||
"tracing",
|
"tracing",
|
||||||
"url",
|
"url",
|
||||||
|
"validator",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -72,10 +72,10 @@ client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "d23
|
|||||||
# To switch to the local path, run:
|
# To switch to the local path, run:
|
||||||
# scripts/tool/update_collab_source.sh
|
# scripts/tool/update_collab_source.sh
|
||||||
# ⚠️⚠️⚠️️
|
# ⚠️⚠️⚠️️
|
||||||
collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "d00b477a9b844d86b5caeff573ca395dc5bf7198" }
|
collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a6b0932581b4544a0800a0451b9522e6caab5570" }
|
||||||
collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "d00b477a9b844d86b5caeff573ca395dc5bf7198" }
|
collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a6b0932581b4544a0800a0451b9522e6caab5570" }
|
||||||
collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "d00b477a9b844d86b5caeff573ca395dc5bf7198" }
|
collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a6b0932581b4544a0800a0451b9522e6caab5570" }
|
||||||
collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "d00b477a9b844d86b5caeff573ca395dc5bf7198" }
|
collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a6b0932581b4544a0800a0451b9522e6caab5570" }
|
||||||
collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "d00b477a9b844d86b5caeff573ca395dc5bf7198" }
|
collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a6b0932581b4544a0800a0451b9522e6caab5570" }
|
||||||
collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "d00b477a9b844d86b5caeff573ca395dc5bf7198" }
|
collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a6b0932581b4544a0800a0451b9522e6caab5570" }
|
||||||
collab-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "d00b477a9b844d86b5caeff573ca395dc5bf7198" }
|
collab-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a6b0932581b4544a0800a0451b9522e6caab5570" }
|
||||||
|
@ -72,10 +72,10 @@ client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "d23
|
|||||||
# To switch to the local path, run:
|
# To switch to the local path, run:
|
||||||
# scripts/tool/update_collab_source.sh
|
# scripts/tool/update_collab_source.sh
|
||||||
# ⚠️⚠️⚠️️
|
# ⚠️⚠️⚠️️
|
||||||
collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "d00b477a9b844d86b5caeff573ca395dc5bf7198" }
|
collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a6b0932581b4544a0800a0451b9522e6caab5570" }
|
||||||
collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "d00b477a9b844d86b5caeff573ca395dc5bf7198" }
|
collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a6b0932581b4544a0800a0451b9522e6caab5570" }
|
||||||
collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "d00b477a9b844d86b5caeff573ca395dc5bf7198" }
|
collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a6b0932581b4544a0800a0451b9522e6caab5570" }
|
||||||
collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "d00b477a9b844d86b5caeff573ca395dc5bf7198" }
|
collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a6b0932581b4544a0800a0451b9522e6caab5570" }
|
||||||
collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "d00b477a9b844d86b5caeff573ca395dc5bf7198" }
|
collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a6b0932581b4544a0800a0451b9522e6caab5570" }
|
||||||
collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "d00b477a9b844d86b5caeff573ca395dc5bf7198" }
|
collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a6b0932581b4544a0800a0451b9522e6caab5570" }
|
||||||
collab-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "d00b477a9b844d86b5caeff573ca395dc5bf7198" }
|
collab-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a6b0932581b4544a0800a0451b9522e6caab5570" }
|
||||||
|
@ -657,7 +657,16 @@
|
|||||||
"showComplete": "Show all tasks"
|
"showComplete": "Show all tasks"
|
||||||
},
|
},
|
||||||
"menuName": "Grid",
|
"menuName": "Grid",
|
||||||
"referencedGridPrefix": "View of"
|
"referencedGridPrefix": "View of",
|
||||||
|
"calculate": "Calculate",
|
||||||
|
"calculationTypeLabel": {
|
||||||
|
"none": "None",
|
||||||
|
"average": "Average",
|
||||||
|
"max": "Max",
|
||||||
|
"median": "Median",
|
||||||
|
"min": "Min",
|
||||||
|
"sum": "Sum"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"document": {
|
"document": {
|
||||||
"menuName": "Document",
|
"menuName": "Document",
|
||||||
@ -1254,4 +1263,4 @@
|
|||||||
"userIcon": "User icon"
|
"userIcon": "User icon"
|
||||||
},
|
},
|
||||||
"noLogFiles": "There're no log files"
|
"noLogFiles": "There're no log files"
|
||||||
}
|
}
|
||||||
|
15
frontend/rust-lib/Cargo.lock
generated
15
frontend/rust-lib/Cargo.lock
generated
@ -744,7 +744,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "collab"
|
name = "collab"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d00b477a9b844d86b5caeff573ca395dc5bf7198#d00b477a9b844d86b5caeff573ca395dc5bf7198"
|
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a6b0932581b4544a0800a0451b9522e6caab5570#a6b0932581b4544a0800a0451b9522e6caab5570"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
@ -766,7 +766,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "collab-database"
|
name = "collab-database"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d00b477a9b844d86b5caeff573ca395dc5bf7198#d00b477a9b844d86b5caeff573ca395dc5bf7198"
|
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a6b0932581b4544a0800a0451b9522e6caab5570#a6b0932581b4544a0800a0451b9522e6caab5570"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
@ -795,7 +795,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "collab-document"
|
name = "collab-document"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d00b477a9b844d86b5caeff573ca395dc5bf7198#d00b477a9b844d86b5caeff573ca395dc5bf7198"
|
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a6b0932581b4544a0800a0451b9522e6caab5570#a6b0932581b4544a0800a0451b9522e6caab5570"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"collab",
|
"collab",
|
||||||
@ -814,7 +814,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "collab-entity"
|
name = "collab-entity"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d00b477a9b844d86b5caeff573ca395dc5bf7198#d00b477a9b844d86b5caeff573ca395dc5bf7198"
|
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a6b0932581b4544a0800a0451b9522e6caab5570#a6b0932581b4544a0800a0451b9522e6caab5570"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bytes",
|
"bytes",
|
||||||
@ -829,7 +829,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "collab-folder"
|
name = "collab-folder"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d00b477a9b844d86b5caeff573ca395dc5bf7198#d00b477a9b844d86b5caeff573ca395dc5bf7198"
|
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a6b0932581b4544a0800a0451b9522e6caab5570#a6b0932581b4544a0800a0451b9522e6caab5570"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"chrono",
|
"chrono",
|
||||||
@ -866,7 +866,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "collab-plugins"
|
name = "collab-plugins"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d00b477a9b844d86b5caeff573ca395dc5bf7198#d00b477a9b844d86b5caeff573ca395dc5bf7198"
|
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a6b0932581b4544a0800a0451b9522e6caab5570#a6b0932581b4544a0800a0451b9522e6caab5570"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-stream",
|
"async-stream",
|
||||||
@ -905,7 +905,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "collab-user"
|
name = "collab-user"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d00b477a9b844d86b5caeff573ca395dc5bf7198#d00b477a9b844d86b5caeff573ca395dc5bf7198"
|
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a6b0932581b4544a0800a0451b9522e6caab5570#a6b0932581b4544a0800a0451b9522e6caab5570"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"collab",
|
"collab",
|
||||||
@ -1790,6 +1790,7 @@ dependencies = [
|
|||||||
"tokio",
|
"tokio",
|
||||||
"tracing",
|
"tracing",
|
||||||
"url",
|
"url",
|
||||||
|
"validator",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -115,10 +115,10 @@ client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "d23
|
|||||||
# To switch to the local path, run:
|
# To switch to the local path, run:
|
||||||
# scripts/tool/update_collab_source.sh
|
# scripts/tool/update_collab_source.sh
|
||||||
# ⚠️⚠️⚠️️
|
# ⚠️⚠️⚠️️
|
||||||
collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "d00b477a9b844d86b5caeff573ca395dc5bf7198" }
|
collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a6b0932581b4544a0800a0451b9522e6caab5570" }
|
||||||
collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "d00b477a9b844d86b5caeff573ca395dc5bf7198" }
|
collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a6b0932581b4544a0800a0451b9522e6caab5570" }
|
||||||
collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "d00b477a9b844d86b5caeff573ca395dc5bf7198" }
|
collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a6b0932581b4544a0800a0451b9522e6caab5570" }
|
||||||
collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "d00b477a9b844d86b5caeff573ca395dc5bf7198" }
|
collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a6b0932581b4544a0800a0451b9522e6caab5570" }
|
||||||
collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "d00b477a9b844d86b5caeff573ca395dc5bf7198" }
|
collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a6b0932581b4544a0800a0451b9522e6caab5570" }
|
||||||
collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "d00b477a9b844d86b5caeff573ca395dc5bf7198" }
|
collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a6b0932581b4544a0800a0451b9522e6caab5570" }
|
||||||
collab-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "d00b477a9b844d86b5caeff573ca395dc5bf7198" }
|
collab-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a6b0932581b4544a0800a0451b9522e6caab5570" }
|
||||||
|
@ -14,10 +14,13 @@ collab-integrate = { workspace = true }
|
|||||||
flowy-database-pub = { workspace = true }
|
flowy-database-pub = { workspace = true }
|
||||||
|
|
||||||
flowy-derive.workspace = true
|
flowy-derive.workspace = true
|
||||||
flowy-notification = { workspace = true }
|
flowy-notification = { workspace = true }
|
||||||
parking_lot.workspace = true
|
parking_lot.workspace = true
|
||||||
protobuf.workspace = true
|
protobuf.workspace = true
|
||||||
flowy-error = { workspace = true, features = ["impl_from_dispatch_error", "impl_from_collab_database"]}
|
flowy-error = { workspace = true, features = [
|
||||||
|
"impl_from_dispatch_error",
|
||||||
|
"impl_from_collab_database",
|
||||||
|
] }
|
||||||
lib-dispatch = { workspace = true }
|
lib-dispatch = { workspace = true }
|
||||||
tokio = { workspace = true, features = ["sync"] }
|
tokio = { workspace = true, features = ["sync"] }
|
||||||
bytes.workspace = true
|
bytes.workspace = true
|
||||||
@ -26,12 +29,12 @@ serde.workspace = true
|
|||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
serde_repr.workspace = true
|
serde_repr.workspace = true
|
||||||
lib-infra = { workspace = true }
|
lib-infra = { workspace = true }
|
||||||
chrono = { workspace = true, default-features = false, features = ["clock"] }
|
chrono = { workspace = true, default-features = false, features = ["clock"] }
|
||||||
rust_decimal = "1.28.1"
|
rust_decimal = "1.28.1"
|
||||||
rusty-money = {version = "0.4.1", features = ["iso"]}
|
rusty-money = { version = "0.4.1", features = ["iso"] }
|
||||||
lazy_static = "1.4.0"
|
lazy_static = "1.4.0"
|
||||||
indexmap = {version = "2.1.0", features = ["serde"]}
|
indexmap = { version = "2.1.0", features = ["serde"] }
|
||||||
url = { version = "2"}
|
url = { version = "2" }
|
||||||
fancy-regex = "0.11.0"
|
fancy-regex = "0.11.0"
|
||||||
futures.workspace = true
|
futures.workspace = true
|
||||||
dashmap = "5"
|
dashmap = "5"
|
||||||
@ -45,6 +48,7 @@ csv = "1.1.6"
|
|||||||
strum = "0.25"
|
strum = "0.25"
|
||||||
strum_macros = "0.25"
|
strum_macros = "0.25"
|
||||||
lru.workspace = true
|
lru.workspace = true
|
||||||
|
validator = { version = "0.16.0", features = ["derive"] }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
event-integration = { path = "../event-integration", default-features = false }
|
event-integration = { path = "../event-integration", default-features = false }
|
||||||
@ -55,4 +59,4 @@ flowy-codegen.workspace = true
|
|||||||
|
|
||||||
[features]
|
[features]
|
||||||
dart = ["flowy-codegen/dart", "flowy-notification/dart"]
|
dart = ["flowy-codegen/dart", "flowy-notification/dart"]
|
||||||
ts = ["flowy-codegen/ts", "flowy-notification/tauri_ts"]
|
ts = ["flowy-codegen/ts", "flowy-notification/tauri_ts"]
|
||||||
|
@ -0,0 +1,81 @@
|
|||||||
|
use flowy_derive::ProtoBuf;
|
||||||
|
|
||||||
|
use super::{CalculationPB, CalculationType};
|
||||||
|
|
||||||
|
use lib_infra::validator_fn::required_not_empty_str;
|
||||||
|
use validator::Validate;
|
||||||
|
|
||||||
|
#[derive(Default, ProtoBuf, Validate)]
|
||||||
|
pub struct UpdateCalculationChangesetPB {
|
||||||
|
#[pb(index = 1)]
|
||||||
|
#[validate(custom = "required_not_empty_str")]
|
||||||
|
pub view_id: String,
|
||||||
|
|
||||||
|
#[pb(index = 2, one_of)]
|
||||||
|
pub calculation_id: Option<String>,
|
||||||
|
|
||||||
|
#[pb(index = 3)]
|
||||||
|
#[validate(custom = "required_not_empty_str")]
|
||||||
|
pub field_id: String,
|
||||||
|
|
||||||
|
#[pb(index = 4)]
|
||||||
|
pub calculation_type: CalculationType,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, ProtoBuf, Validate)]
|
||||||
|
pub struct RemoveCalculationChangesetPB {
|
||||||
|
#[pb(index = 1)]
|
||||||
|
#[validate(custom = "required_not_empty_str")]
|
||||||
|
pub view_id: String,
|
||||||
|
|
||||||
|
#[pb(index = 2)]
|
||||||
|
#[validate(custom = "required_not_empty_str")]
|
||||||
|
pub field_id: String,
|
||||||
|
|
||||||
|
#[pb(index = 3)]
|
||||||
|
#[validate(custom = "required_not_empty_str")]
|
||||||
|
pub calculation_id: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default, ProtoBuf, Clone)]
|
||||||
|
pub struct CalculationChangesetNotificationPB {
|
||||||
|
#[pb(index = 1)]
|
||||||
|
pub view_id: String,
|
||||||
|
|
||||||
|
#[pb(index = 2)]
|
||||||
|
pub insert_calculations: Vec<CalculationPB>,
|
||||||
|
|
||||||
|
#[pb(index = 3)]
|
||||||
|
pub update_calculations: Vec<CalculationPB>,
|
||||||
|
|
||||||
|
#[pb(index = 4)]
|
||||||
|
pub delete_calculations: Vec<CalculationPB>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CalculationChangesetNotificationPB {
|
||||||
|
pub fn from_insert(view_id: &str, calculations: Vec<CalculationPB>) -> Self {
|
||||||
|
Self {
|
||||||
|
view_id: view_id.to_string(),
|
||||||
|
insert_calculations: calculations,
|
||||||
|
delete_calculations: Default::default(),
|
||||||
|
update_calculations: Default::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn from_delete(view_id: &str, calculations: Vec<CalculationPB>) -> Self {
|
||||||
|
Self {
|
||||||
|
view_id: view_id.to_string(),
|
||||||
|
insert_calculations: Default::default(),
|
||||||
|
delete_calculations: calculations,
|
||||||
|
update_calculations: Default::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_update(view_id: &str, calculations: Vec<CalculationPB>) -> Self {
|
||||||
|
Self {
|
||||||
|
view_id: view_id.to_string(),
|
||||||
|
insert_calculations: Default::default(),
|
||||||
|
delete_calculations: Default::default(),
|
||||||
|
update_calculations: calculations,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,126 @@
|
|||||||
|
use std::{
|
||||||
|
fmt::{Display, Formatter},
|
||||||
|
sync::Arc,
|
||||||
|
};
|
||||||
|
|
||||||
|
use flowy_derive::{ProtoBuf, ProtoBuf_Enum};
|
||||||
|
use serde_repr::{Deserialize_repr, Serialize_repr};
|
||||||
|
|
||||||
|
use crate::{impl_into_calculation_type, services::calculations::Calculation};
|
||||||
|
|
||||||
|
#[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)]
|
||||||
|
pub struct CalculationPB {
|
||||||
|
#[pb(index = 1)]
|
||||||
|
pub id: String,
|
||||||
|
|
||||||
|
#[pb(index = 2)]
|
||||||
|
pub field_id: String,
|
||||||
|
|
||||||
|
#[pb(index = 3)]
|
||||||
|
pub calculation_type: CalculationType,
|
||||||
|
|
||||||
|
#[pb(index = 4)]
|
||||||
|
pub value: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::convert::From<&Calculation> for CalculationPB {
|
||||||
|
fn from(calculation: &Calculation) -> Self {
|
||||||
|
let calculation_type = calculation.calculation_type.into();
|
||||||
|
|
||||||
|
Self {
|
||||||
|
id: calculation.id.clone(),
|
||||||
|
field_id: calculation.field_id.clone(),
|
||||||
|
calculation_type,
|
||||||
|
value: calculation.value.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::convert::From<&Arc<Calculation>> for CalculationPB {
|
||||||
|
fn from(calculation: &Arc<Calculation>) -> Self {
|
||||||
|
let calculation_type = calculation.calculation_type.into();
|
||||||
|
|
||||||
|
Self {
|
||||||
|
id: calculation.id.clone(),
|
||||||
|
field_id: calculation.field_id.clone(),
|
||||||
|
calculation_type,
|
||||||
|
value: calculation.value.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(
|
||||||
|
Default, Debug, Copy, Clone, PartialEq, Hash, Eq, ProtoBuf_Enum, Serialize_repr, Deserialize_repr,
|
||||||
|
)]
|
||||||
|
#[repr(u8)]
|
||||||
|
pub enum CalculationType {
|
||||||
|
#[default]
|
||||||
|
Average = 0, // Number
|
||||||
|
Max = 1, // Number
|
||||||
|
Median = 2, // Number
|
||||||
|
Min = 3, // Number
|
||||||
|
Sum = 4, // Number
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for CalculationType {
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||||
|
let value: i64 = (*self).into();
|
||||||
|
f.write_fmt(format_args!("{}", value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsRef<CalculationType> for CalculationType {
|
||||||
|
fn as_ref(&self) -> &CalculationType {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&CalculationType> for CalculationType {
|
||||||
|
fn from(calculation_type: &CalculationType) -> Self {
|
||||||
|
*calculation_type
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CalculationType {
|
||||||
|
pub fn value(&self) -> i64 {
|
||||||
|
(*self).into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl_into_calculation_type!(i64);
|
||||||
|
impl_into_calculation_type!(u8);
|
||||||
|
|
||||||
|
impl From<CalculationType> for i64 {
|
||||||
|
fn from(ty: CalculationType) -> Self {
|
||||||
|
(ty as u8) as i64
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&CalculationType> for i64 {
|
||||||
|
fn from(ty: &CalculationType) -> Self {
|
||||||
|
i64::from(*ty)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)]
|
||||||
|
pub struct RepeatedCalculationsPB {
|
||||||
|
#[pb(index = 1)]
|
||||||
|
pub items: Vec<CalculationPB>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::convert::From<Vec<Arc<Calculation>>> for RepeatedCalculationsPB {
|
||||||
|
fn from(calculations: Vec<Arc<Calculation>>) -> Self {
|
||||||
|
RepeatedCalculationsPB {
|
||||||
|
items: calculations
|
||||||
|
.into_iter()
|
||||||
|
.map(|rev: Arc<Calculation>| rev.as_ref().into())
|
||||||
|
.collect(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::convert::From<Vec<CalculationPB>> for RepeatedCalculationsPB {
|
||||||
|
fn from(items: Vec<CalculationPB>) -> Self {
|
||||||
|
Self { items }
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,5 @@
|
|||||||
|
mod calculation_changeset;
|
||||||
|
mod calculation_entities;
|
||||||
|
|
||||||
|
pub use calculation_changeset::*;
|
||||||
|
pub use calculation_entities::*;
|
@ -6,6 +6,9 @@ use collab_database::views::DatabaseLayout;
|
|||||||
use flowy_derive::ProtoBuf;
|
use flowy_derive::ProtoBuf;
|
||||||
use flowy_error::{ErrorCode, FlowyError};
|
use flowy_error::{ErrorCode, FlowyError};
|
||||||
|
|
||||||
|
use lib_infra::validator_fn::required_not_empty_str;
|
||||||
|
use validator::Validate;
|
||||||
|
|
||||||
use crate::entities::parser::NotEmptyStr;
|
use crate::entities::parser::NotEmptyStr;
|
||||||
use crate::entities::{DatabaseLayoutPB, FieldIdPB, RowMetaPB};
|
use crate::entities::{DatabaseLayoutPB, FieldIdPB, RowMetaPB};
|
||||||
use crate::services::database::CreateDatabaseViewParams;
|
use crate::services::database::CreateDatabaseViewParams;
|
||||||
@ -66,9 +69,10 @@ impl AsRef<str> for DatabaseIdPB {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, ProtoBuf, Default, Debug)]
|
#[derive(Clone, ProtoBuf, Default, Debug, Validate)]
|
||||||
pub struct DatabaseViewIdPB {
|
pub struct DatabaseViewIdPB {
|
||||||
#[pb(index = 1)]
|
#[pb(index = 1)]
|
||||||
|
#[validate(custom = "required_not_empty_str")]
|
||||||
pub value: String,
|
pub value: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -42,3 +42,24 @@ macro_rules! impl_into_field_visibility {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! impl_into_calculation_type {
|
||||||
|
($target: ident) => {
|
||||||
|
impl std::convert::From<$target> for CalculationType {
|
||||||
|
fn from(ty: $target) -> Self {
|
||||||
|
match ty {
|
||||||
|
0 => CalculationType::Average,
|
||||||
|
1 => CalculationType::Max,
|
||||||
|
2 => CalculationType::Median,
|
||||||
|
3 => CalculationType::Min,
|
||||||
|
4 => CalculationType::Sum,
|
||||||
|
_ => {
|
||||||
|
tracing::error!("🔴 Can't parse CalculationType from value: {}", ty);
|
||||||
|
CalculationType::Average
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
mod board_entities;
|
mod board_entities;
|
||||||
|
pub mod calculation;
|
||||||
mod calendar_entities;
|
mod calendar_entities;
|
||||||
mod cell_entities;
|
mod cell_entities;
|
||||||
mod database_entities;
|
mod database_entities;
|
||||||
@ -19,6 +20,7 @@ mod view_entities;
|
|||||||
mod macros;
|
mod macros;
|
||||||
|
|
||||||
pub use board_entities::*;
|
pub use board_entities::*;
|
||||||
|
pub use calculation::*;
|
||||||
pub use calendar_entities::*;
|
pub use calendar_entities::*;
|
||||||
pub use cell_entities::*;
|
pub use cell_entities::*;
|
||||||
pub use database_entities::*;
|
pub use database_entities::*;
|
||||||
|
@ -920,3 +920,45 @@ pub(crate) async fn update_field_settings_handler(
|
|||||||
.await?;
|
.await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(level = "debug", skip_all, err)]
|
||||||
|
pub(crate) async fn get_all_calculations_handler(
|
||||||
|
data: AFPluginData<DatabaseViewIdPB>,
|
||||||
|
manager: AFPluginState<Weak<DatabaseManager>>,
|
||||||
|
) -> DataResult<RepeatedCalculationsPB, FlowyError> {
|
||||||
|
let manager = upgrade_manager(manager)?;
|
||||||
|
let view_id = data.into_inner();
|
||||||
|
let database_editor = manager.get_database_with_view_id(view_id.as_ref()).await?;
|
||||||
|
|
||||||
|
let calculations = database_editor.get_all_calculations(view_id.as_ref()).await;
|
||||||
|
|
||||||
|
data_result_ok(calculations)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(level = "trace", skip(data, manager), err)]
|
||||||
|
pub(crate) async fn update_calculation_handler(
|
||||||
|
data: AFPluginData<UpdateCalculationChangesetPB>,
|
||||||
|
manager: AFPluginState<Weak<DatabaseManager>>,
|
||||||
|
) -> Result<(), FlowyError> {
|
||||||
|
let manager = upgrade_manager(manager)?;
|
||||||
|
let params: UpdateCalculationChangesetPB = data.into_inner();
|
||||||
|
let editor = manager.get_database_with_view_id(¶ms.view_id).await?;
|
||||||
|
|
||||||
|
editor.update_calculation(params).await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(level = "trace", skip(data, manager), err)]
|
||||||
|
pub(crate) async fn remove_calculation_handler(
|
||||||
|
data: AFPluginData<RemoveCalculationChangesetPB>,
|
||||||
|
manager: AFPluginState<Weak<DatabaseManager>>,
|
||||||
|
) -> Result<(), FlowyError> {
|
||||||
|
let manager = upgrade_manager(manager)?;
|
||||||
|
let params: RemoveCalculationChangesetPB = data.into_inner();
|
||||||
|
let editor = manager.get_database_with_view_id(¶ms.view_id).await?;
|
||||||
|
|
||||||
|
editor.remove_calculation(params).await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
@ -79,6 +79,10 @@ pub fn init(database_manager: Weak<DatabaseManager>) -> AFPlugin {
|
|||||||
.event(DatabaseEvent::GetFieldSettings, get_field_settings_handler)
|
.event(DatabaseEvent::GetFieldSettings, get_field_settings_handler)
|
||||||
.event(DatabaseEvent::GetAllFieldSettings, get_all_field_settings_handler)
|
.event(DatabaseEvent::GetAllFieldSettings, get_all_field_settings_handler)
|
||||||
.event(DatabaseEvent::UpdateFieldSettings, update_field_settings_handler)
|
.event(DatabaseEvent::UpdateFieldSettings, update_field_settings_handler)
|
||||||
|
// Calculations
|
||||||
|
.event(DatabaseEvent::GetAllCalculations, get_all_calculations_handler)
|
||||||
|
.event(DatabaseEvent::UpdateCalculation, update_calculation_handler)
|
||||||
|
.event(DatabaseEvent::RemoveCalculation, remove_calculation_handler)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// [DatabaseEvent] defines events that are used to interact with the Grid. You could check [this](https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/architecture/backend/protobuf)
|
/// [DatabaseEvent] defines events that are used to interact with the Grid. You could check [this](https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/architecture/backend/protobuf)
|
||||||
@ -329,4 +333,13 @@ pub enum DatabaseEvent {
|
|||||||
/// Updates the field settings for a field in the given view
|
/// Updates the field settings for a field in the given view
|
||||||
#[event(input = "FieldSettingsChangesetPB")]
|
#[event(input = "FieldSettingsChangesetPB")]
|
||||||
UpdateFieldSettings = 162,
|
UpdateFieldSettings = 162,
|
||||||
|
|
||||||
|
#[event(input = "DatabaseViewIdPB", output = "RepeatedCalculationsPB")]
|
||||||
|
GetAllCalculations = 163,
|
||||||
|
|
||||||
|
#[event(input = "UpdateCalculationChangesetPB")]
|
||||||
|
UpdateCalculation = 164,
|
||||||
|
|
||||||
|
#[event(input = "RemoveCalculationChangesetPB")]
|
||||||
|
RemoveCalculation = 165,
|
||||||
}
|
}
|
||||||
|
@ -8,3 +8,4 @@ pub mod notification;
|
|||||||
mod protobuf;
|
mod protobuf;
|
||||||
pub mod services;
|
pub mod services;
|
||||||
pub mod template;
|
pub mod template;
|
||||||
|
pub mod utils;
|
||||||
|
@ -50,6 +50,8 @@ pub enum DatabaseNotification {
|
|||||||
DidUpdateDatabaseSnapshotState = 85,
|
DidUpdateDatabaseSnapshotState = 85,
|
||||||
// Trigger when the field setting is changed
|
// Trigger when the field setting is changed
|
||||||
DidUpdateFieldSettings = 86,
|
DidUpdateFieldSettings = 86,
|
||||||
|
// Trigger when Calculation changed
|
||||||
|
DidUpdateCalculation = 87,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::convert::From<DatabaseNotification> for i32 {
|
impl std::convert::From<DatabaseNotification> for i32 {
|
||||||
@ -80,7 +82,8 @@ impl std::convert::From<i32> for DatabaseNotification {
|
|||||||
82 => DatabaseNotification::DidUpdateDatabaseLayout,
|
82 => DatabaseNotification::DidUpdateDatabaseLayout,
|
||||||
83 => DatabaseNotification::DidDeleteDatabaseView,
|
83 => DatabaseNotification::DidDeleteDatabaseView,
|
||||||
84 => DatabaseNotification::DidMoveDatabaseViewToTrash,
|
84 => DatabaseNotification::DidMoveDatabaseViewToTrash,
|
||||||
87 => DatabaseNotification::DidUpdateFieldSettings,
|
86 => DatabaseNotification::DidUpdateFieldSettings,
|
||||||
|
87 => DatabaseNotification::DidUpdateCalculation,
|
||||||
_ => DatabaseNotification::Unknown,
|
_ => DatabaseNotification::Unknown,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,6 @@
|
|||||||
|
use parking_lot::RwLock;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use crate::utils::cache::AnyTypeCache;
|
||||||
|
|
||||||
|
pub type CalculationsByFieldIdCache = Arc<RwLock<AnyTypeCache<String>>>;
|
@ -0,0 +1,359 @@
|
|||||||
|
use std::str::FromStr;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use collab::core::any_map::AnyMapExtension;
|
||||||
|
use collab_database::fields::Field;
|
||||||
|
use collab_database::rows::{Row, RowCell};
|
||||||
|
use flowy_error::FlowyResult;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use tokio::sync::RwLock;
|
||||||
|
|
||||||
|
use lib_infra::future::Fut;
|
||||||
|
use lib_infra::priority_task::{QualityOfService, Task, TaskContent, TaskDispatcher};
|
||||||
|
|
||||||
|
use crate::entities::{
|
||||||
|
CalculationChangesetNotificationPB, CalculationPB, CalculationType, FieldType,
|
||||||
|
};
|
||||||
|
use crate::services::calculations::CalculationsByFieldIdCache;
|
||||||
|
use crate::services::database_view::{DatabaseViewChanged, DatabaseViewChangedNotifier};
|
||||||
|
use crate::utils::cache::AnyTypeCache;
|
||||||
|
|
||||||
|
use super::{Calculation, CalculationChangeset, CalculationsService};
|
||||||
|
|
||||||
|
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 update_calculation(&self, view_id: &str, calculation: Calculation);
|
||||||
|
fn remove_calculation(&self, view_id: &str, calculation_id: &str);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct CalculationsController {
|
||||||
|
view_id: String,
|
||||||
|
handler_id: String,
|
||||||
|
delegate: Box<dyn CalculationsDelegate>,
|
||||||
|
calculations_by_field_cache: CalculationsByFieldIdCache,
|
||||||
|
task_scheduler: Arc<RwLock<TaskDispatcher>>,
|
||||||
|
calculations_service: CalculationsService,
|
||||||
|
notifier: DatabaseViewChangedNotifier,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for CalculationsController {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
tracing::trace!("Drop {}", std::any::type_name::<Self>());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CalculationsController {
|
||||||
|
pub async fn new<T>(
|
||||||
|
view_id: &str,
|
||||||
|
handler_id: &str,
|
||||||
|
delegate: T,
|
||||||
|
calculations: Vec<Arc<Calculation>>,
|
||||||
|
task_scheduler: Arc<RwLock<TaskDispatcher>>,
|
||||||
|
notifier: DatabaseViewChangedNotifier,
|
||||||
|
) -> Self
|
||||||
|
where
|
||||||
|
T: CalculationsDelegate + 'static,
|
||||||
|
{
|
||||||
|
let this = Self {
|
||||||
|
view_id: view_id.to_string(),
|
||||||
|
handler_id: handler_id.to_string(),
|
||||||
|
delegate: Box::new(delegate),
|
||||||
|
calculations_by_field_cache: AnyTypeCache::<String>::new(),
|
||||||
|
task_scheduler,
|
||||||
|
calculations_service: CalculationsService::new(),
|
||||||
|
notifier,
|
||||||
|
};
|
||||||
|
this.update_cache(calculations).await;
|
||||||
|
this
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn close(&self) {
|
||||||
|
if let Ok(mut task_scheduler) = self.task_scheduler.try_write() {
|
||||||
|
task_scheduler.unregister_handler(&self.handler_id).await;
|
||||||
|
} else {
|
||||||
|
tracing::error!("Attempt to get the lock of task_scheduler failed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(name = "schedule_filter_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(
|
||||||
|
&self.handler_id,
|
||||||
|
task_id,
|
||||||
|
TaskContent::Text(task_type.to_string()),
|
||||||
|
qos,
|
||||||
|
);
|
||||||
|
self.task_scheduler.write().await.add_task(task);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(
|
||||||
|
name = "process_filter_task",
|
||||||
|
level = "trace",
|
||||||
|
skip_all,
|
||||||
|
fields(filter_result),
|
||||||
|
err
|
||||||
|
)]
|
||||||
|
pub async fn process(&self, predicate: &str) -> FlowyResult<()> {
|
||||||
|
let event_type = CalculationEvent::from_str(predicate).unwrap();
|
||||||
|
match event_type {
|
||||||
|
CalculationEvent::RowChanged(row) => self.handle_row_changed(row).await,
|
||||||
|
CalculationEvent::CellUpdated(field_id) => self.handle_cell_changed(field_id).await,
|
||||||
|
CalculationEvent::FieldDeleted(field_id) => self.handle_field_deleted(field_id).await,
|
||||||
|
CalculationEvent::FieldTypeChanged(field_id, new_field_type) => {
|
||||||
|
self
|
||||||
|
.handle_field_type_changed(field_id, new_field_type)
|
||||||
|
.await
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn did_receive_field_deleted(&self, field_id: String) {
|
||||||
|
self
|
||||||
|
.gen_task(
|
||||||
|
CalculationEvent::FieldDeleted(field_id),
|
||||||
|
QualityOfService::UserInteractive,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_field_deleted(&self, field_id: String) {
|
||||||
|
let calculation = self
|
||||||
|
.delegate
|
||||||
|
.get_calculation(&self.view_id, &field_id)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
if let Some(calculation) = calculation {
|
||||||
|
self
|
||||||
|
.delegate
|
||||||
|
.remove_calculation(&self.view_id, &calculation.id);
|
||||||
|
|
||||||
|
let notification = CalculationChangesetNotificationPB::from_delete(
|
||||||
|
&self.view_id,
|
||||||
|
vec![CalculationPB::from(&calculation)],
|
||||||
|
);
|
||||||
|
|
||||||
|
let _ = self
|
||||||
|
.notifier
|
||||||
|
.send(DatabaseViewChanged::CalculationValueNotification(
|
||||||
|
notification,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn did_receive_field_type_changed(&self, field_id: String, new_field_type: FieldType) {
|
||||||
|
self
|
||||||
|
.gen_task(
|
||||||
|
CalculationEvent::FieldTypeChanged(field_id, new_field_type),
|
||||||
|
QualityOfService::UserInteractive,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_field_type_changed(&self, field_id: String, new_field_type: FieldType) {
|
||||||
|
let calculation = self
|
||||||
|
.delegate
|
||||||
|
.get_calculation(&self.view_id, &field_id)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
if let Some(calculation) = calculation {
|
||||||
|
if new_field_type != FieldType::Number {
|
||||||
|
self
|
||||||
|
.delegate
|
||||||
|
.remove_calculation(&self.view_id, &calculation.id);
|
||||||
|
|
||||||
|
let notification = CalculationChangesetNotificationPB::from_delete(
|
||||||
|
&self.view_id,
|
||||||
|
vec![CalculationPB::from(&calculation)],
|
||||||
|
);
|
||||||
|
|
||||||
|
let _ = self
|
||||||
|
.notifier
|
||||||
|
.send(DatabaseViewChanged::CalculationValueNotification(
|
||||||
|
notification,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn did_receive_cell_changed(&self, field_id: String) {
|
||||||
|
self
|
||||||
|
.gen_task(
|
||||||
|
CalculationEvent::CellUpdated(field_id),
|
||||||
|
QualityOfService::UserInteractive,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_cell_changed(&self, field_id: String) {
|
||||||
|
let calculation = self
|
||||||
|
.delegate
|
||||||
|
.get_calculation(&self.view_id, &field_id)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
if let Some(calculation) = calculation {
|
||||||
|
let update = self.get_updated_calculation(calculation).await;
|
||||||
|
if let Some(update) = update {
|
||||||
|
self
|
||||||
|
.delegate
|
||||||
|
.update_calculation(&self.view_id, update.clone());
|
||||||
|
|
||||||
|
let notification = CalculationChangesetNotificationPB::from_update(
|
||||||
|
&self.view_id,
|
||||||
|
vec![CalculationPB::from(&update)],
|
||||||
|
);
|
||||||
|
|
||||||
|
let _ = self
|
||||||
|
.notifier
|
||||||
|
.send(DatabaseViewChanged::CalculationValueNotification(
|
||||||
|
notification,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn did_receive_row_changed(&self, row: Row) {
|
||||||
|
self
|
||||||
|
.gen_task(
|
||||||
|
CalculationEvent::RowChanged(row),
|
||||||
|
QualityOfService::UserInteractive,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_row_changed(&self, row: Row) {
|
||||||
|
let cells = row.cells.iter();
|
||||||
|
|
||||||
|
let mut updates = vec![];
|
||||||
|
|
||||||
|
// Iterate each cell in the row
|
||||||
|
for cell in cells {
|
||||||
|
let field_id = cell.0;
|
||||||
|
let value = cell.1.value().get("data");
|
||||||
|
|
||||||
|
// Only continue if there is a value in the cell
|
||||||
|
if let Some(_cell_value) = value {
|
||||||
|
let calculation = self.delegate.get_calculation(&self.view_id, field_id).await;
|
||||||
|
|
||||||
|
if let Some(calculation) = calculation {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !updates.is_empty() {
|
||||||
|
let notification = CalculationChangesetNotificationPB::from_update(&self.view_id, updates);
|
||||||
|
|
||||||
|
let _ = self
|
||||||
|
.notifier
|
||||||
|
.send(DatabaseViewChanged::CalculationValueNotification(
|
||||||
|
notification,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_updated_calculation(&self, calculation: Arc<Calculation>) -> Option<Calculation> {
|
||||||
|
let row_cells = self
|
||||||
|
.delegate
|
||||||
|
.get_cells_for_field(&self.view_id, &calculation.field_id)
|
||||||
|
.await;
|
||||||
|
let field = self.delegate.get_field(&calculation.field_id)?;
|
||||||
|
|
||||||
|
if !row_cells.is_empty() {
|
||||||
|
let value =
|
||||||
|
self
|
||||||
|
.calculations_service
|
||||||
|
.calculate(&field, calculation.calculation_type, row_cells);
|
||||||
|
|
||||||
|
if value != calculation.value {
|
||||||
|
return Some(calculation.with_value(value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn did_receive_changes(
|
||||||
|
&self,
|
||||||
|
changeset: CalculationChangeset,
|
||||||
|
) -> Option<CalculationChangesetNotificationPB> {
|
||||||
|
let mut notification: Option<CalculationChangesetNotificationPB> = None;
|
||||||
|
|
||||||
|
if let Some(insert) = &changeset.insert_calculation {
|
||||||
|
let row_cells: Vec<Arc<RowCell>> = self
|
||||||
|
.delegate
|
||||||
|
.get_cells_for_field(&self.view_id, &insert.field_id)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let field = self.delegate.get_field(&insert.field_id)?;
|
||||||
|
|
||||||
|
let value = self
|
||||||
|
.calculations_service
|
||||||
|
.calculate(&field, insert.calculation_type, row_cells);
|
||||||
|
|
||||||
|
notification = Some(CalculationChangesetNotificationPB::from_insert(
|
||||||
|
&self.view_id,
|
||||||
|
vec![CalculationPB {
|
||||||
|
id: insert.id.clone(),
|
||||||
|
field_id: insert.field_id.clone(),
|
||||||
|
calculation_type: CalculationType::from(insert.calculation_type),
|
||||||
|
value,
|
||||||
|
}],
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(delete) = &changeset.delete_calculation {
|
||||||
|
notification = Some(CalculationChangesetNotificationPB::from_delete(
|
||||||
|
&self.view_id,
|
||||||
|
vec![CalculationPB {
|
||||||
|
id: delete.id.clone(),
|
||||||
|
field_id: delete.field_id.clone(),
|
||||||
|
calculation_type: CalculationType::from(delete.calculation_type),
|
||||||
|
value: delete.value.clone(),
|
||||||
|
}],
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
notification
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn update_cache(&self, calculations: Vec<Arc<Calculation>>) {
|
||||||
|
for calculation in calculations {
|
||||||
|
let field_id = &calculation.field_id;
|
||||||
|
self
|
||||||
|
.calculations_by_field_cache
|
||||||
|
.write()
|
||||||
|
.insert(field_id, calculation.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||||
|
enum CalculationEvent {
|
||||||
|
RowChanged(Row),
|
||||||
|
CellUpdated(String),
|
||||||
|
FieldTypeChanged(String, FieldType),
|
||||||
|
FieldDeleted(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToString for CalculationEvent {
|
||||||
|
fn to_string(&self) -> String {
|
||||||
|
serde_json::to_string(self).unwrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for CalculationEvent {
|
||||||
|
type Err = serde_json::Error;
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
serde_json::from_str(s)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,130 @@
|
|||||||
|
use anyhow::bail;
|
||||||
|
use collab::core::any_map::AnyMapExtension;
|
||||||
|
use collab_database::views::{CalculationMap, CalculationMapBuilder};
|
||||||
|
|
||||||
|
use crate::entities::CalculationPB;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Calculation {
|
||||||
|
pub id: String,
|
||||||
|
pub field_id: String,
|
||||||
|
pub calculation_type: i64,
|
||||||
|
pub value: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
const CALCULATION_ID: &str = "id";
|
||||||
|
const FIELD_ID: &str = "field_id";
|
||||||
|
const CALCULATION_TYPE: &str = "ty";
|
||||||
|
const CALCULATION_VALUE: &str = "calculation_value";
|
||||||
|
|
||||||
|
impl From<Calculation> for CalculationMap {
|
||||||
|
fn from(data: Calculation) -> Self {
|
||||||
|
CalculationMapBuilder::new()
|
||||||
|
.insert_str_value(CALCULATION_ID, data.id)
|
||||||
|
.insert_str_value(FIELD_ID, data.field_id)
|
||||||
|
.insert_i64_value(CALCULATION_TYPE, data.calculation_type)
|
||||||
|
.insert_str_value(CALCULATION_VALUE, data.value)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::convert::From<&CalculationPB> for Calculation {
|
||||||
|
fn from(calculation: &CalculationPB) -> Self {
|
||||||
|
let calculation_type = calculation.calculation_type.into();
|
||||||
|
|
||||||
|
Self {
|
||||||
|
id: calculation.id.clone(),
|
||||||
|
field_id: calculation.field_id.clone(),
|
||||||
|
calculation_type,
|
||||||
|
value: calculation.value.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<CalculationMap> for Calculation {
|
||||||
|
type Error = anyhow::Error;
|
||||||
|
|
||||||
|
fn try_from(calculation: CalculationMap) -> Result<Self, Self::Error> {
|
||||||
|
match (
|
||||||
|
calculation.get_str_value(CALCULATION_ID),
|
||||||
|
calculation.get_str_value(FIELD_ID),
|
||||||
|
) {
|
||||||
|
(Some(id), Some(field_id)) => {
|
||||||
|
let value = calculation
|
||||||
|
.get_str_value(CALCULATION_VALUE)
|
||||||
|
.unwrap_or_default();
|
||||||
|
let calculation_type = calculation
|
||||||
|
.get_i64_value(CALCULATION_TYPE)
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
Ok(Calculation {
|
||||||
|
id,
|
||||||
|
field_id,
|
||||||
|
calculation_type,
|
||||||
|
value,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
_ => {
|
||||||
|
bail!("Invalid calculation data")
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct CalculationUpdatedNotification {
|
||||||
|
pub view_id: String,
|
||||||
|
|
||||||
|
pub calculation: Calculation,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CalculationUpdatedNotification {
|
||||||
|
pub fn new(view_id: String, calculation: Calculation) -> Self {
|
||||||
|
Self {
|
||||||
|
view_id,
|
||||||
|
calculation,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Calculation {
|
||||||
|
pub fn none(id: String, field_id: String, calculation_type: Option<i64>) -> Self {
|
||||||
|
Self {
|
||||||
|
id,
|
||||||
|
field_id,
|
||||||
|
calculation_type: calculation_type.unwrap_or(0),
|
||||||
|
value: "".to_owned(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_value(&self, value: String) -> Self {
|
||||||
|
Self {
|
||||||
|
id: self.id.clone(),
|
||||||
|
field_id: self.field_id.clone(),
|
||||||
|
calculation_type: self.calculation_type,
|
||||||
|
value,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct CalculationChangeset {
|
||||||
|
pub(crate) insert_calculation: Option<Calculation>,
|
||||||
|
pub(crate) delete_calculation: Option<Calculation>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CalculationChangeset {
|
||||||
|
pub fn from_insert(calculation: Calculation) -> Self {
|
||||||
|
Self {
|
||||||
|
insert_calculation: Some(calculation),
|
||||||
|
delete_calculation: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_delete(calculation: Calculation) -> Self {
|
||||||
|
Self {
|
||||||
|
insert_calculation: None,
|
||||||
|
delete_calculation: Some(calculation),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,11 @@
|
|||||||
|
mod cache;
|
||||||
|
mod controller;
|
||||||
|
mod entities;
|
||||||
|
mod service;
|
||||||
|
mod task;
|
||||||
|
|
||||||
|
pub(crate) use cache::*;
|
||||||
|
pub use controller::*;
|
||||||
|
pub use entities::*;
|
||||||
|
pub(crate) use service::*;
|
||||||
|
pub(crate) use task::*;
|
@ -0,0 +1,142 @@
|
|||||||
|
use crate::entities::{CalculationType, FieldType};
|
||||||
|
|
||||||
|
use crate::services::field::TypeOptionCellExt;
|
||||||
|
use collab_database::fields::Field;
|
||||||
|
use collab_database::rows::RowCell;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
pub struct CalculationsService {}
|
||||||
|
|
||||||
|
impl CalculationsService {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn calculate(
|
||||||
|
&self,
|
||||||
|
field: &Field,
|
||||||
|
calculation_type: i64,
|
||||||
|
row_cells: Vec<Arc<RowCell>>,
|
||||||
|
) -> String {
|
||||||
|
let ty: CalculationType = calculation_type.into();
|
||||||
|
|
||||||
|
match ty {
|
||||||
|
CalculationType::Average => self.calculate_average(field, row_cells),
|
||||||
|
CalculationType::Max => self.calculate_max(field, row_cells),
|
||||||
|
CalculationType::Median => self.calculate_median(field, row_cells),
|
||||||
|
CalculationType::Min => self.calculate_min(field, row_cells),
|
||||||
|
CalculationType::Sum => self.calculate_sum(field, row_cells),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn calculate_average(&self, field: &Field, row_cells: Vec<Arc<RowCell>>) -> String {
|
||||||
|
let mut sum = 0.0;
|
||||||
|
let mut len = 0.0;
|
||||||
|
let field_type = FieldType::from(field.field_type);
|
||||||
|
if let Some(handler) = TypeOptionCellExt::new_with_cell_data_cache(field, None)
|
||||||
|
.get_type_option_cell_data_handler(&field_type)
|
||||||
|
{
|
||||||
|
for row_cell in row_cells {
|
||||||
|
if let Some(cell) = &row_cell.cell {
|
||||||
|
if let Some(value) = handler.handle_numeric_cell(cell) {
|
||||||
|
sum += value;
|
||||||
|
len += 1.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len > 0.0 {
|
||||||
|
format!("{:.5}", sum / len)
|
||||||
|
} else {
|
||||||
|
"0".to_owned()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn calculate_median(&self, field: &Field, row_cells: Vec<Arc<RowCell>>) -> String {
|
||||||
|
let values = self.reduce_values_f64(field, row_cells, |values| {
|
||||||
|
values.sort_by(|a, b| a.partial_cmp(b).unwrap());
|
||||||
|
values.clone()
|
||||||
|
});
|
||||||
|
|
||||||
|
if !values.is_empty() {
|
||||||
|
format!("{:.5}", Self::median(&values))
|
||||||
|
} else {
|
||||||
|
"".to_owned()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn median(array: &Vec<f64>) -> f64 {
|
||||||
|
if (array.len() % 2) == 0 {
|
||||||
|
let left = array.len() / 2 - 1;
|
||||||
|
let right = array.len() / 2;
|
||||||
|
(array[left] + array[right]) / 2.0
|
||||||
|
} else {
|
||||||
|
array[array.len() / 2]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn calculate_min(&self, field: &Field, row_cells: Vec<Arc<RowCell>>) -> String {
|
||||||
|
let values = self.reduce_values_f64(field, row_cells, |values| {
|
||||||
|
values.sort_by(|a, b| a.partial_cmp(b).unwrap());
|
||||||
|
values.clone()
|
||||||
|
});
|
||||||
|
|
||||||
|
if !values.is_empty() {
|
||||||
|
let min = values.iter().min_by(|a, b| a.total_cmp(b));
|
||||||
|
if let Some(min) = min {
|
||||||
|
return format!("{:.5}", min);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
"".to_owned()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn calculate_max(&self, field: &Field, row_cells: Vec<Arc<RowCell>>) -> String {
|
||||||
|
let values = self.reduce_values_f64(field, row_cells, |values| {
|
||||||
|
values.sort_by(|a, b| a.partial_cmp(b).unwrap());
|
||||||
|
values.clone()
|
||||||
|
});
|
||||||
|
|
||||||
|
if !values.is_empty() {
|
||||||
|
let max = values.iter().max_by(|a, b| a.total_cmp(b));
|
||||||
|
if let Some(max) = max {
|
||||||
|
return format!("{:.5}", max);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
"".to_owned()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn calculate_sum(&self, field: &Field, row_cells: Vec<Arc<RowCell>>) -> String {
|
||||||
|
let values = self.reduce_values_f64(field, row_cells, |values| values.clone());
|
||||||
|
|
||||||
|
if !values.is_empty() {
|
||||||
|
format!("{:.5}", values.iter().sum::<f64>())
|
||||||
|
} else {
|
||||||
|
"".to_owned()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reduce_values_f64<F, T>(&self, field: &Field, row_cells: Vec<Arc<RowCell>>, f: F) -> T
|
||||||
|
where
|
||||||
|
F: FnOnce(&mut Vec<f64>) -> T,
|
||||||
|
{
|
||||||
|
let mut values = vec![];
|
||||||
|
|
||||||
|
let field_type = FieldType::from(field.field_type);
|
||||||
|
if let Some(handler) = TypeOptionCellExt::new_with_cell_data_cache(field, None)
|
||||||
|
.get_type_option_cell_data_handler(&field_type)
|
||||||
|
{
|
||||||
|
for row_cell in row_cells {
|
||||||
|
if let Some(cell) = &row_cell.cell {
|
||||||
|
if let Some(value) = handler.handle_numeric_cell(cell) {
|
||||||
|
values.push(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
f(&mut values)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,42 @@
|
|||||||
|
use lib_infra::future::BoxResultFuture;
|
||||||
|
use lib_infra::priority_task::{TaskContent, TaskHandler};
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use crate::services::calculations::CalculationsController;
|
||||||
|
|
||||||
|
pub struct CalculationsTaskHandler {
|
||||||
|
handler_id: String,
|
||||||
|
calculations_controller: Arc<CalculationsController>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CalculationsTaskHandler {
|
||||||
|
pub fn new(handler_id: String, calculations_controller: Arc<CalculationsController>) -> Self {
|
||||||
|
Self {
|
||||||
|
handler_id,
|
||||||
|
calculations_controller,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TaskHandler for CalculationsTaskHandler {
|
||||||
|
fn handler_id(&self) -> &str {
|
||||||
|
&self.handler_id
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handler_name(&self) -> &str {
|
||||||
|
"CalculationsTaskHandler"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(&self, content: TaskContent) -> BoxResultFuture<(), anyhow::Error> {
|
||||||
|
let calculations_controller = self.calculations_controller.clone();
|
||||||
|
Box::pin(async move {
|
||||||
|
if let TaskContent::Text(predicate) = content {
|
||||||
|
calculations_controller
|
||||||
|
.process(&predicate)
|
||||||
|
.await
|
||||||
|
.map_err(anyhow::Error::from)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -1,126 +1,7 @@
|
|||||||
use parking_lot::RwLock;
|
use parking_lot::RwLock;
|
||||||
use std::any::{type_name, Any};
|
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::fmt::Debug;
|
|
||||||
use std::hash::Hash;
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use crate::utils::cache::AnyTypeCache;
|
||||||
|
|
||||||
pub type CellCache = Arc<RwLock<AnyTypeCache<u64>>>;
|
pub type CellCache = Arc<RwLock<AnyTypeCache<u64>>>;
|
||||||
pub type CellFilterCache = Arc<RwLock<AnyTypeCache<String>>>;
|
pub type CellFilterCache = Arc<RwLock<AnyTypeCache<String>>>;
|
||||||
|
|
||||||
#[derive(Default, Debug)]
|
|
||||||
/// The better option is use LRU cache
|
|
||||||
pub struct AnyTypeCache<TypeValueKey>(HashMap<TypeValueKey, TypeValue>);
|
|
||||||
|
|
||||||
impl<TypeValueKey> AnyTypeCache<TypeValueKey>
|
|
||||||
where
|
|
||||||
TypeValueKey: Clone + Hash + Eq,
|
|
||||||
{
|
|
||||||
pub fn new() -> Arc<RwLock<AnyTypeCache<TypeValueKey>>> {
|
|
||||||
Arc::new(RwLock::new(AnyTypeCache(HashMap::default())))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn insert<T>(&mut self, key: &TypeValueKey, val: T) -> Option<T>
|
|
||||||
where
|
|
||||||
T: 'static + Send + Sync,
|
|
||||||
{
|
|
||||||
self
|
|
||||||
.0
|
|
||||||
.insert(key.clone(), TypeValue::new(val))
|
|
||||||
.and_then(downcast_owned)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn remove(&mut self, key: &TypeValueKey) {
|
|
||||||
self.0.remove(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
// pub fn remove<T, K: AsRef<TypeValueKey>>(&mut self, key: K) -> Option<T>
|
|
||||||
// where
|
|
||||||
// T: 'static + Send + Sync,
|
|
||||||
// {
|
|
||||||
// self.0.remove(key.as_ref()).and_then(downcast_owned)
|
|
||||||
// }
|
|
||||||
|
|
||||||
pub fn get<T>(&self, key: &TypeValueKey) -> Option<&T>
|
|
||||||
where
|
|
||||||
T: 'static + Send + Sync,
|
|
||||||
{
|
|
||||||
self
|
|
||||||
.0
|
|
||||||
.get(key)
|
|
||||||
.and_then(|type_value| type_value.boxed.downcast_ref())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_mut<T>(&mut self, key: &TypeValueKey) -> Option<&mut T>
|
|
||||||
where
|
|
||||||
T: 'static + Send + Sync,
|
|
||||||
{
|
|
||||||
self
|
|
||||||
.0
|
|
||||||
.get_mut(key)
|
|
||||||
.and_then(|type_value| type_value.boxed.downcast_mut())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn contains(&self, key: &TypeValueKey) -> bool {
|
|
||||||
self.0.contains_key(key)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_empty(&self) -> bool {
|
|
||||||
self.0.is_empty()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn downcast_owned<T: 'static + Send + Sync>(type_value: TypeValue) -> Option<T> {
|
|
||||||
type_value.boxed.downcast().ok().map(|boxed| *boxed)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
struct TypeValue {
|
|
||||||
boxed: Box<dyn Any + Send + Sync + 'static>,
|
|
||||||
#[allow(dead_code)]
|
|
||||||
ty: &'static str,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TypeValue {
|
|
||||||
pub fn new<T>(value: T) -> Self
|
|
||||||
where
|
|
||||||
T: Send + Sync + 'static,
|
|
||||||
{
|
|
||||||
Self {
|
|
||||||
boxed: Box::new(value),
|
|
||||||
ty: type_name::<T>(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::ops::Deref for TypeValue {
|
|
||||||
type Target = Box<dyn Any + Send + Sync + 'static>;
|
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
|
||||||
&self.boxed
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::ops::DerefMut for TypeValue {
|
|
||||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
|
||||||
&mut self.boxed
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// #[cfg(test)]
|
|
||||||
// mod tests {
|
|
||||||
// use crate::services::cell::CellDataCache;
|
|
||||||
//
|
|
||||||
// #[test]
|
|
||||||
// fn test() {
|
|
||||||
// let mut ext = CellDataCache::new();
|
|
||||||
// ext.insert("1", "a".to_string());
|
|
||||||
// ext.insert("2", 2);
|
|
||||||
//
|
|
||||||
// let a: &String = ext.get("1").unwrap();
|
|
||||||
// assert_eq!(a, "a");
|
|
||||||
//
|
|
||||||
// let a: Option<&usize> = ext.get("1");
|
|
||||||
// assert!(a.is_none());
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
@ -39,6 +39,11 @@ pub trait CellDataDecoder: TypeOption {
|
|||||||
|
|
||||||
/// Same as [CellDataDecoder::stringify_cell_data] but the input parameter is the [Cell]
|
/// Same as [CellDataDecoder::stringify_cell_data] but the input parameter is the [Cell]
|
||||||
fn stringify_cell(&self, cell: &Cell) -> String;
|
fn stringify_cell(&self, cell: &Cell) -> String;
|
||||||
|
|
||||||
|
/// Decode the cell into f64
|
||||||
|
/// Different field type has different way to decode the cell data into f64
|
||||||
|
/// If the field type doesn't support to decode the cell data into f64, it will return None
|
||||||
|
fn numeric_cell(&self, cell: &Cell) -> Option<f64>;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait CellDataChangeset: TypeOption {
|
pub trait CellDataChangeset: TypeOption {
|
||||||
@ -172,7 +177,7 @@ pub fn stringify_cell_data(
|
|||||||
.get_type_option_cell_data_handler(from_field_type)
|
.get_type_option_cell_data_handler(from_field_type)
|
||||||
{
|
{
|
||||||
None => "".to_string(),
|
None => "".to_string(),
|
||||||
Some(handler) => handler.stringify_cell_str(cell, to_field_type, field),
|
Some(handler) => handler.handle_stringify_cell(cell, to_field_type, field),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -16,9 +16,8 @@ use lib_infra::priority_task::TaskDispatcher;
|
|||||||
|
|
||||||
use crate::entities::*;
|
use crate::entities::*;
|
||||||
use crate::notification::{send_notification, DatabaseNotification};
|
use crate::notification::{send_notification, DatabaseNotification};
|
||||||
use crate::services::cell::{
|
use crate::services::calculations::Calculation;
|
||||||
apply_cell_changeset, get_cell_protobuf, AnyTypeCache, CellCache, ToCellChangeset,
|
use crate::services::cell::{apply_cell_changeset, get_cell_protobuf, CellCache, ToCellChangeset};
|
||||||
};
|
|
||||||
use crate::services::database::util::database_view_setting_pb_from_view;
|
use crate::services::database::util::database_view_setting_pb_from_view;
|
||||||
use crate::services::database::UpdatedRow;
|
use crate::services::database::UpdatedRow;
|
||||||
use crate::services::database_view::{
|
use crate::services::database_view::{
|
||||||
@ -37,6 +36,7 @@ use crate::services::filter::Filter;
|
|||||||
use crate::services::group::{default_group_setting, GroupChangesets, GroupSetting, RowChangeset};
|
use crate::services::group::{default_group_setting, GroupChangesets, GroupSetting, RowChangeset};
|
||||||
use crate::services::share::csv::{CSVExport, CSVFormat};
|
use crate::services::share::csv::{CSVExport, CSVFormat};
|
||||||
use crate::services::sort::Sort;
|
use crate::services::sort::Sort;
|
||||||
|
use crate::utils::cache::AnyTypeCache;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct DatabaseEditor {
|
pub struct DatabaseEditor {
|
||||||
@ -226,6 +226,26 @@ impl DatabaseEditor {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn get_all_calculations(&self, view_id: &str) -> RepeatedCalculationsPB {
|
||||||
|
if let Ok(view_editor) = self.database_views.get_view_editor(view_id).await {
|
||||||
|
view_editor.v_get_all_calculations().await.into()
|
||||||
|
} else {
|
||||||
|
RepeatedCalculationsPB { items: vec![] }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn update_calculation(&self, update: UpdateCalculationChangesetPB) -> FlowyResult<()> {
|
||||||
|
let view_editor = self.database_views.get_view_editor(&update.view_id).await?;
|
||||||
|
view_editor.v_update_calculations(update).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn remove_calculation(&self, remove: RemoveCalculationChangesetPB) -> FlowyResult<()> {
|
||||||
|
let view_editor = self.database_views.get_view_editor(&remove.view_id).await?;
|
||||||
|
view_editor.v_remove_calculation(remove).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn get_all_filters(&self, view_id: &str) -> RepeatedFilterPB {
|
pub async fn get_all_filters(&self, view_id: &str) -> RepeatedFilterPB {
|
||||||
if let Ok(view_editor) = self.database_views.get_view_editor(view_id).await {
|
if let Ok(view_editor) = self.database_views.get_view_editor(view_id).await {
|
||||||
view_editor.v_get_all_filters().await.into()
|
view_editor.v_get_all_filters().await.into()
|
||||||
@ -310,6 +330,11 @@ impl DatabaseEditor {
|
|||||||
let notified_changeset =
|
let notified_changeset =
|
||||||
DatabaseFieldChangesetPB::delete(&database_id, vec![FieldIdPB::from(field_id)]);
|
DatabaseFieldChangesetPB::delete(&database_id, vec![FieldIdPB::from(field_id)]);
|
||||||
self.notify_did_update_database(notified_changeset).await?;
|
self.notify_did_update_database(notified_changeset).await?;
|
||||||
|
|
||||||
|
for view in self.database_views.editors().await {
|
||||||
|
view.v_did_delete_field(field_id).await;
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -364,6 +389,10 @@ impl DatabaseEditor {
|
|||||||
.set_field_type(new_field_type.into())
|
.set_field_type(new_field_type.into())
|
||||||
.set_type_option(new_field_type.into(), Some(transformed_type_option));
|
.set_type_option(new_field_type.into(), Some(transformed_type_option));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
for view in self.database_views.editors().await {
|
||||||
|
view.v_did_update_field_type(field_id, new_field_type).await;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -405,7 +434,12 @@ impl DatabaseEditor {
|
|||||||
match params {
|
match params {
|
||||||
None => warn!("Failed to duplicate row: {}", row_id),
|
None => warn!("Failed to duplicate row: {}", row_id),
|
||||||
Some(params) => {
|
Some(params) => {
|
||||||
let _ = self.create_row(view_id, None, params).await;
|
let result = self.create_row(view_id, None, params).await;
|
||||||
|
if let Some(row_detail) = result.unwrap_or(None) {
|
||||||
|
for view in self.database_views.editors().await {
|
||||||
|
view.v_did_duplicate_row(&row_detail).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -725,7 +759,9 @@ impl DatabaseEditor {
|
|||||||
let option_row = self.get_row_detail(view_id, &row_id);
|
let option_row = self.get_row_detail(view_id, &row_id);
|
||||||
if let Some(new_row_detail) = option_row {
|
if let Some(new_row_detail) = option_row {
|
||||||
for view in self.database_views.editors().await {
|
for view in self.database_views.editors().await {
|
||||||
view.v_did_update_row(&old_row, &new_row_detail).await;
|
view
|
||||||
|
.v_did_update_row(&old_row, &new_row_detail, field_id.to_owned())
|
||||||
|
.await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1423,6 +1459,23 @@ impl DatabaseViewOperation for DatabaseViewOperationImpl {
|
|||||||
self.database.lock().remove_all_sorts(view_id);
|
self.database.lock().remove_all_sorts(view_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get_all_calculations(&self, view_id: &str) -> Vec<Arc<Calculation>> {
|
||||||
|
self
|
||||||
|
.database
|
||||||
|
.lock()
|
||||||
|
.get_all_calculations(view_id)
|
||||||
|
.into_iter()
|
||||||
|
.map(Arc::new)
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_calculation(&self, view_id: &str, field_id: &str) -> Option<Calculation> {
|
||||||
|
self
|
||||||
|
.database
|
||||||
|
.lock()
|
||||||
|
.get_calculation::<Calculation>(view_id, field_id)
|
||||||
|
}
|
||||||
|
|
||||||
fn get_all_filters(&self, view_id: &str) -> Vec<Arc<Filter>> {
|
fn get_all_filters(&self, view_id: &str) -> Vec<Arc<Filter>> {
|
||||||
self
|
self
|
||||||
.database
|
.database
|
||||||
@ -1569,6 +1622,17 @@ impl DatabaseViewOperation for DatabaseViewOperationImpl {
|
|||||||
.payload(FieldSettingsPB::from(new_field_settings))
|
.payload(FieldSettingsPB::from(new_field_settings))
|
||||||
.send()
|
.send()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn update_calculation(&self, view_id: &str, calculation: Calculation) {
|
||||||
|
self
|
||||||
|
.database
|
||||||
|
.lock()
|
||||||
|
.update_calculation(view_id, calculation)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remove_calculation(&self, view_id: &str, field_id: &str) {
|
||||||
|
self.database.lock().remove_calculation(view_id, field_id)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument(level = "trace", skip_all, err)]
|
#[tracing::instrument(level = "trace", skip_all, err)]
|
||||||
|
@ -6,6 +6,7 @@ pub use views::*;
|
|||||||
|
|
||||||
mod layout_deps;
|
mod layout_deps;
|
||||||
mod notifier;
|
mod notifier;
|
||||||
|
mod view_calculations;
|
||||||
mod view_editor;
|
mod view_editor;
|
||||||
mod view_filter;
|
mod view_filter;
|
||||||
mod view_group;
|
mod view_group;
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
#![allow(clippy::while_let_loop)]
|
#![allow(clippy::while_let_loop)]
|
||||||
use crate::entities::{
|
use crate::entities::{
|
||||||
DatabaseViewSettingPB, FilterChangesetNotificationPB, GroupChangesPB, GroupRowsNotificationPB,
|
CalculationChangesetNotificationPB, DatabaseViewSettingPB, FilterChangesetNotificationPB,
|
||||||
ReorderAllRowsPB, ReorderSingleRowPB, RowsVisibilityChangePB, SortChangesetNotificationPB,
|
GroupChangesPB, GroupRowsNotificationPB, ReorderAllRowsPB, ReorderSingleRowPB,
|
||||||
|
RowsVisibilityChangePB, SortChangesetNotificationPB,
|
||||||
};
|
};
|
||||||
use crate::notification::{send_notification, DatabaseNotification};
|
use crate::notification::{send_notification, DatabaseNotification};
|
||||||
use crate::services::filter::FilterResultNotification;
|
use crate::services::filter::FilterResultNotification;
|
||||||
@ -15,6 +16,7 @@ pub enum DatabaseViewChanged {
|
|||||||
FilterNotification(FilterResultNotification),
|
FilterNotification(FilterResultNotification),
|
||||||
ReorderAllRowsNotification(ReorderAllRowsResult),
|
ReorderAllRowsNotification(ReorderAllRowsResult),
|
||||||
ReorderSingleRowNotification(ReorderSingleRowResult),
|
ReorderSingleRowNotification(ReorderSingleRowResult),
|
||||||
|
CalculationValueNotification(CalculationChangesetNotificationPB),
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type DatabaseViewChangedNotifier = broadcast::Sender<DatabaseViewChanged>;
|
pub type DatabaseViewChangedNotifier = broadcast::Sender<DatabaseViewChanged>;
|
||||||
@ -76,6 +78,12 @@ impl DatabaseViewChangedReceiverRunner {
|
|||||||
.payload(reorder_row)
|
.payload(reorder_row)
|
||||||
.send()
|
.send()
|
||||||
},
|
},
|
||||||
|
DatabaseViewChanged::CalculationValueNotification(notification) => send_notification(
|
||||||
|
¬ification.view_id,
|
||||||
|
DatabaseNotification::DidUpdateCalculation,
|
||||||
|
)
|
||||||
|
.payload(notification)
|
||||||
|
.send(),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
@ -94,6 +102,15 @@ pub async fn notify_did_update_filter(notification: FilterChangesetNotificationP
|
|||||||
.send();
|
.send();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn notify_did_update_calculation(notification: CalculationChangesetNotificationPB) {
|
||||||
|
send_notification(
|
||||||
|
¬ification.view_id,
|
||||||
|
DatabaseNotification::DidUpdateCalculation,
|
||||||
|
)
|
||||||
|
.payload(notification)
|
||||||
|
.send();
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn notify_did_update_sort(notification: SortChangesetNotificationPB) {
|
pub async fn notify_did_update_sort(notification: SortChangesetNotificationPB) {
|
||||||
if !notification.is_empty() {
|
if !notification.is_empty() {
|
||||||
send_notification(¬ification.view_id, DatabaseNotification::DidUpdateSort)
|
send_notification(¬ification.view_id, DatabaseNotification::DidUpdateSort)
|
||||||
|
@ -0,0 +1,69 @@
|
|||||||
|
use collab_database::fields::Field;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use collab_database::rows::RowCell;
|
||||||
|
use lib_infra::future::{to_fut, Fut};
|
||||||
|
|
||||||
|
use crate::services::calculations::{
|
||||||
|
Calculation, CalculationsController, CalculationsDelegate, CalculationsTaskHandler,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::services::database_view::{
|
||||||
|
gen_handler_id, DatabaseViewChangedNotifier, DatabaseViewOperation,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub async fn make_calculations_controller(
|
||||||
|
view_id: &str,
|
||||||
|
delegate: Arc<dyn DatabaseViewOperation>,
|
||||||
|
notifier: DatabaseViewChangedNotifier,
|
||||||
|
) -> Arc<CalculationsController> {
|
||||||
|
let calculations = delegate.get_all_calculations(view_id);
|
||||||
|
let task_scheduler = delegate.get_task_scheduler();
|
||||||
|
let calculations_delegate = DatabaseViewCalculationsDelegateImpl(delegate.clone());
|
||||||
|
let handler_id = gen_handler_id();
|
||||||
|
|
||||||
|
let calculations_controller = CalculationsController::new(
|
||||||
|
view_id,
|
||||||
|
&handler_id,
|
||||||
|
calculations_delegate,
|
||||||
|
calculations,
|
||||||
|
task_scheduler.clone(),
|
||||||
|
notifier,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let calculations_controller = Arc::new(calculations_controller);
|
||||||
|
task_scheduler
|
||||||
|
.write()
|
||||||
|
.await
|
||||||
|
.register_handler(CalculationsTaskHandler::new(
|
||||||
|
handler_id,
|
||||||
|
calculations_controller.clone(),
|
||||||
|
));
|
||||||
|
calculations_controller
|
||||||
|
}
|
||||||
|
|
||||||
|
struct DatabaseViewCalculationsDelegateImpl(Arc<dyn DatabaseViewOperation>);
|
||||||
|
|
||||||
|
impl CalculationsDelegate for DatabaseViewCalculationsDelegateImpl {
|
||||||
|
fn get_cells_for_field(&self, view_id: &str, field_id: &str) -> Fut<Vec<Arc<RowCell>>> {
|
||||||
|
self.0.get_cells_for_field(view_id, field_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_field(&self, field_id: &str) -> Option<Field> {
|
||||||
|
self.0.get_field(field_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_calculation(&self, view_id: &str, field_id: &str) -> Fut<Option<Arc<Calculation>>> {
|
||||||
|
let calculation = self.0.get_calculation(view_id, field_id).map(Arc::new);
|
||||||
|
to_fut(async move { calculation })
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_calculation(&self, view_id: &str, calculation: Calculation) {
|
||||||
|
self.0.update_calculation(view_id, calculation)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remove_calculation(&self, view_id: &str, calculation_id: &str) {
|
||||||
|
self.0.remove_calculation(view_id, calculation_id)
|
||||||
|
}
|
||||||
|
}
|
@ -2,7 +2,9 @@ use std::borrow::Cow;
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use collab_database::database::{gen_database_filter_id, gen_database_sort_id};
|
use collab_database::database::{
|
||||||
|
gen_database_calculation_id, gen_database_filter_id, gen_database_sort_id,
|
||||||
|
};
|
||||||
use collab_database::fields::{Field, TypeOptionData};
|
use collab_database::fields::{Field, TypeOptionData};
|
||||||
use collab_database::rows::{Cells, Row, RowDetail, RowId};
|
use collab_database::rows::{Cells, Row, RowDetail, RowId};
|
||||||
use collab_database::views::{DatabaseLayout, DatabaseView};
|
use collab_database::views::{DatabaseLayout, DatabaseView};
|
||||||
@ -15,10 +17,12 @@ use lib_dispatch::prelude::af_spawn;
|
|||||||
use crate::entities::{
|
use crate::entities::{
|
||||||
CalendarEventPB, DatabaseLayoutMetaPB, DatabaseLayoutSettingPB, DeleteFilterParams,
|
CalendarEventPB, DatabaseLayoutMetaPB, DatabaseLayoutSettingPB, DeleteFilterParams,
|
||||||
DeleteSortParams, FieldType, FieldVisibility, GroupChangesPB, GroupPB, InsertedRowPB,
|
DeleteSortParams, FieldType, FieldVisibility, GroupChangesPB, GroupPB, InsertedRowPB,
|
||||||
LayoutSettingChangeset, LayoutSettingParams, RowMetaPB, RowsChangePB,
|
LayoutSettingChangeset, LayoutSettingParams, RemoveCalculationChangesetPB, RowMetaPB,
|
||||||
SortChangesetNotificationPB, SortPB, UpdateFilterParams, UpdateSortParams,
|
RowsChangePB, SortChangesetNotificationPB, SortPB, UpdateCalculationChangesetPB,
|
||||||
|
UpdateFilterParams, UpdateSortParams,
|
||||||
};
|
};
|
||||||
use crate::notification::{send_notification, DatabaseNotification};
|
use crate::notification::{send_notification, DatabaseNotification};
|
||||||
|
use crate::services::calculations::{Calculation, CalculationChangeset, CalculationsController};
|
||||||
use crate::services::cell::CellCache;
|
use crate::services::cell::CellCache;
|
||||||
use crate::services::database::{database_view_setting_pb_from_view, DatabaseRowEvent, UpdatedRow};
|
use crate::services::database::{database_view_setting_pb_from_view, DatabaseRowEvent, UpdatedRow};
|
||||||
use crate::services::database_view::view_filter::make_filter_controller;
|
use crate::services::database_view::view_filter::make_filter_controller;
|
||||||
@ -40,12 +44,16 @@ use crate::services::group::{GroupChangesets, GroupController, MoveGroupRowConte
|
|||||||
use crate::services::setting::CalendarLayoutSetting;
|
use crate::services::setting::CalendarLayoutSetting;
|
||||||
use crate::services::sort::{DeletedSortType, Sort, SortChangeset, SortController, SortType};
|
use crate::services::sort::{DeletedSortType, Sort, SortChangeset, SortController, SortType};
|
||||||
|
|
||||||
|
use super::notify_did_update_calculation;
|
||||||
|
use super::view_calculations::make_calculations_controller;
|
||||||
|
|
||||||
pub struct DatabaseViewEditor {
|
pub struct DatabaseViewEditor {
|
||||||
pub view_id: String,
|
pub view_id: String,
|
||||||
delegate: Arc<dyn DatabaseViewOperation>,
|
delegate: Arc<dyn DatabaseViewOperation>,
|
||||||
group_controller: Arc<RwLock<Option<Box<dyn GroupController>>>>,
|
group_controller: Arc<RwLock<Option<Box<dyn GroupController>>>>,
|
||||||
filter_controller: Arc<FilterController>,
|
filter_controller: Arc<FilterController>,
|
||||||
sort_controller: Arc<RwLock<SortController>>,
|
sort_controller: Arc<RwLock<SortController>>,
|
||||||
|
calculations_controller: Arc<CalculationsController>,
|
||||||
pub notifier: DatabaseViewChangedNotifier,
|
pub notifier: DatabaseViewChangedNotifier,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -87,12 +95,17 @@ impl DatabaseViewEditor {
|
|||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
|
// Calculations
|
||||||
|
let calculations_controller =
|
||||||
|
make_calculations_controller(&view_id, delegate.clone(), notifier.clone()).await;
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
view_id,
|
view_id,
|
||||||
delegate,
|
delegate,
|
||||||
group_controller,
|
group_controller,
|
||||||
filter_controller,
|
filter_controller,
|
||||||
sort_controller,
|
sort_controller,
|
||||||
|
calculations_controller,
|
||||||
notifier,
|
notifier,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -100,6 +113,7 @@ impl DatabaseViewEditor {
|
|||||||
pub async fn close(&self) {
|
pub async fn close(&self) {
|
||||||
self.sort_controller.write().await.close().await;
|
self.sort_controller.write().await.close().await;
|
||||||
self.filter_controller.close().await;
|
self.filter_controller.close().await;
|
||||||
|
self.calculations_controller.close().await;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn v_get_view(&self) -> Option<DatabaseView> {
|
pub async fn v_get_view(&self) -> Option<DatabaseView> {
|
||||||
@ -148,8 +162,17 @@ impl DatabaseViewEditor {
|
|||||||
.send();
|
.send();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn v_did_duplicate_row(&self, row_detail: &RowDetail) {
|
||||||
|
self
|
||||||
|
.calculations_controller
|
||||||
|
.did_receive_row_changed(row_detail.clone().row)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
#[tracing::instrument(level = "trace", skip_all)]
|
#[tracing::instrument(level = "trace", skip_all)]
|
||||||
pub async fn v_did_delete_row(&self, row: &Row) {
|
pub async fn v_did_delete_row(&self, row: &Row) {
|
||||||
|
let deleted_row = row.clone();
|
||||||
|
|
||||||
// Send the group notification if the current view has groups;
|
// Send the group notification if the current view has groups;
|
||||||
let result = self
|
let result = self
|
||||||
.mut_group_controller(|group_controller, _| group_controller.did_delete_row(row))
|
.mut_group_controller(|group_controller, _| group_controller.did_delete_row(row))
|
||||||
@ -170,15 +193,32 @@ impl DatabaseViewEditor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
let changes = RowsChangePB::from_delete(row.id.clone().into_inner());
|
let changes = RowsChangePB::from_delete(row.id.clone().into_inner());
|
||||||
|
|
||||||
send_notification(&self.view_id, DatabaseNotification::DidUpdateViewRows)
|
send_notification(&self.view_id, DatabaseNotification::DidUpdateViewRows)
|
||||||
.payload(changes)
|
.payload(changes)
|
||||||
.send();
|
.send();
|
||||||
|
|
||||||
|
// Updating calculations for each of the Rows cells is a tedious task
|
||||||
|
// Therefore we spawn a separate task for this
|
||||||
|
let weak_calculations_controller = Arc::downgrade(&self.calculations_controller);
|
||||||
|
af_spawn(async move {
|
||||||
|
if let Some(calculations_controller) = weak_calculations_controller.upgrade() {
|
||||||
|
calculations_controller
|
||||||
|
.did_receive_row_changed(deleted_row)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Notify the view that the row has been updated. If the view has groups,
|
/// Notify the view that the row has been updated. If the view has groups,
|
||||||
/// send the group notification with [GroupRowsNotificationPB]. Otherwise,
|
/// send the group notification with [GroupRowsNotificationPB]. Otherwise,
|
||||||
/// send the view notification with [RowsChangePB]
|
/// send the view notification with [RowsChangePB]
|
||||||
pub async fn v_did_update_row(&self, old_row: &Option<RowDetail>, row_detail: &RowDetail) {
|
pub async fn v_did_update_row(
|
||||||
|
&self,
|
||||||
|
old_row: &Option<RowDetail>,
|
||||||
|
row_detail: &RowDetail,
|
||||||
|
field_id: String,
|
||||||
|
) {
|
||||||
let result = self
|
let result = self
|
||||||
.mut_group_controller(|group_controller, field| {
|
.mut_group_controller(|group_controller, field| {
|
||||||
Ok(group_controller.did_update_group_row(old_row, row_detail, &field))
|
Ok(group_controller.did_update_group_row(old_row, row_detail, &field))
|
||||||
@ -211,11 +251,12 @@ impl DatabaseViewEditor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Each row update will trigger a filter and sort operation. We don't want
|
// Each row update will trigger a calculations, filter and sort operation. We don't want
|
||||||
// to block the main thread, so we spawn a new task to do the work.
|
// to block the main thread, so we spawn a new task to do the work.
|
||||||
let row_id = row_detail.row.id.clone();
|
let row_id = row_detail.row.id.clone();
|
||||||
let weak_filter_controller = Arc::downgrade(&self.filter_controller);
|
let weak_filter_controller = Arc::downgrade(&self.filter_controller);
|
||||||
let weak_sort_controller = Arc::downgrade(&self.sort_controller);
|
let weak_sort_controller = Arc::downgrade(&self.sort_controller);
|
||||||
|
let weak_calculations_controller = Arc::downgrade(&self.calculations_controller);
|
||||||
af_spawn(async move {
|
af_spawn(async move {
|
||||||
if let Some(filter_controller) = weak_filter_controller.upgrade() {
|
if let Some(filter_controller) = weak_filter_controller.upgrade() {
|
||||||
filter_controller
|
filter_controller
|
||||||
@ -226,7 +267,13 @@ impl DatabaseViewEditor {
|
|||||||
sort_controller
|
sort_controller
|
||||||
.read()
|
.read()
|
||||||
.await
|
.await
|
||||||
.did_receive_row_changed(row_id)
|
.did_receive_row_changed(row_id.clone())
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(calculations_controller) = weak_calculations_controller.upgrade() {
|
||||||
|
calculations_controller
|
||||||
|
.did_receive_cell_changed(field_id)
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -508,6 +555,68 @@ impl DatabaseViewEditor {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn v_get_all_calculations(&self) -> Vec<Arc<Calculation>> {
|
||||||
|
self.delegate.get_all_calculations(&self.view_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn v_update_calculations(
|
||||||
|
&self,
|
||||||
|
params: UpdateCalculationChangesetPB,
|
||||||
|
) -> FlowyResult<()> {
|
||||||
|
let calculation_id = match params.calculation_id {
|
||||||
|
None => gen_database_calculation_id(),
|
||||||
|
Some(calculation_id) => calculation_id,
|
||||||
|
};
|
||||||
|
|
||||||
|
let calculation = Calculation::none(
|
||||||
|
calculation_id,
|
||||||
|
params.field_id,
|
||||||
|
Some(params.calculation_type.value()),
|
||||||
|
);
|
||||||
|
|
||||||
|
let changeset = self
|
||||||
|
.calculations_controller
|
||||||
|
.did_receive_changes(CalculationChangeset::from_insert(calculation.clone()))
|
||||||
|
.await;
|
||||||
|
|
||||||
|
if let Some(changeset) = changeset {
|
||||||
|
if !changeset.insert_calculations.is_empty() {
|
||||||
|
for insert in changeset.insert_calculations.clone() {
|
||||||
|
let calculation: Calculation = Calculation::from(&insert);
|
||||||
|
self
|
||||||
|
.delegate
|
||||||
|
.update_calculation(¶ms.view_id, calculation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
notify_did_update_calculation(changeset).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn v_remove_calculation(
|
||||||
|
&self,
|
||||||
|
params: RemoveCalculationChangesetPB,
|
||||||
|
) -> FlowyResult<()> {
|
||||||
|
self
|
||||||
|
.delegate
|
||||||
|
.remove_calculation(¶ms.view_id, ¶ms.calculation_id);
|
||||||
|
|
||||||
|
let calculation = Calculation::none(params.calculation_id, params.field_id, None);
|
||||||
|
|
||||||
|
let changeset = self
|
||||||
|
.calculations_controller
|
||||||
|
.did_receive_changes(CalculationChangeset::from_delete(calculation.clone()))
|
||||||
|
.await;
|
||||||
|
|
||||||
|
if let Some(changeset) = changeset {
|
||||||
|
notify_did_update_calculation(changeset).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn v_get_all_filters(&self) -> Vec<Arc<Filter>> {
|
pub async fn v_get_all_filters(&self) -> Vec<Arc<Filter>> {
|
||||||
self.delegate.get_all_filters(&self.view_id)
|
self.delegate.get_all_filters(&self.view_id)
|
||||||
}
|
}
|
||||||
@ -912,6 +1021,20 @@ impl DatabaseViewEditor {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn v_did_delete_field(&self, field_id: &str) {
|
||||||
|
self
|
||||||
|
.calculations_controller
|
||||||
|
.did_receive_field_deleted(field_id.to_owned())
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn v_did_update_field_type(&self, field_id: &str, new_field_type: &FieldType) {
|
||||||
|
self
|
||||||
|
.calculations_controller
|
||||||
|
.did_receive_field_type_changed(field_id.to_owned(), new_field_type.to_owned())
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
async fn mut_group_controller<F, T>(&self, f: F) -> Option<T>
|
async fn mut_group_controller<F, T>(&self, f: F) -> Option<T>
|
||||||
where
|
where
|
||||||
F: FnOnce(&mut Box<dyn GroupController>, Field) -> FlowyResult<T>,
|
F: FnOnce(&mut Box<dyn GroupController>, Field) -> FlowyResult<T>,
|
||||||
|
@ -12,6 +12,7 @@ use lib_infra::future::{Fut, FutureResult};
|
|||||||
use lib_infra::priority_task::TaskDispatcher;
|
use lib_infra::priority_task::TaskDispatcher;
|
||||||
|
|
||||||
use crate::entities::{FieldType, FieldVisibility};
|
use crate::entities::{FieldType, FieldVisibility};
|
||||||
|
use crate::services::calculations::Calculation;
|
||||||
use crate::services::field::TypeOptionCellDataHandler;
|
use crate::services::field::TypeOptionCellDataHandler;
|
||||||
use crate::services::field_settings::FieldSettings;
|
use crate::services::field_settings::FieldSettings;
|
||||||
use crate::services::filter::Filter;
|
use crate::services::filter::Filter;
|
||||||
@ -80,6 +81,14 @@ pub trait DatabaseViewOperation: Send + Sync + 'static {
|
|||||||
|
|
||||||
fn remove_all_sorts(&self, view_id: &str);
|
fn remove_all_sorts(&self, view_id: &str);
|
||||||
|
|
||||||
|
fn get_all_calculations(&self, view_id: &str) -> Vec<Arc<Calculation>>;
|
||||||
|
|
||||||
|
fn get_calculation(&self, view_id: &str, field_id: &str) -> Option<Calculation>;
|
||||||
|
|
||||||
|
fn update_calculation(&self, view_id: &str, calculation: Calculation);
|
||||||
|
|
||||||
|
fn remove_calculation(&self, view_id: &str, calculation_id: &str);
|
||||||
|
|
||||||
fn get_all_filters(&self, view_id: &str) -> Vec<Arc<Filter>>;
|
fn get_all_filters(&self, view_id: &str) -> Vec<Arc<Filter>>;
|
||||||
|
|
||||||
fn delete_filter(&self, view_id: &str, filter_id: &str);
|
fn delete_filter(&self, view_id: &str, filter_id: &str);
|
||||||
|
@ -17,6 +17,7 @@ use crate::services::group::RowChangeset;
|
|||||||
pub type RowEventSender = broadcast::Sender<DatabaseRowEvent>;
|
pub type RowEventSender = broadcast::Sender<DatabaseRowEvent>;
|
||||||
pub type RowEventReceiver = broadcast::Receiver<DatabaseRowEvent>;
|
pub type RowEventReceiver = broadcast::Receiver<DatabaseRowEvent>;
|
||||||
pub type EditorByViewId = HashMap<String, Arc<DatabaseViewEditor>>;
|
pub type EditorByViewId = HashMap<String, Arc<DatabaseViewEditor>>;
|
||||||
|
|
||||||
pub struct DatabaseViews {
|
pub struct DatabaseViews {
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
database: Arc<MutexDatabase>,
|
database: Arc<MutexDatabase>,
|
||||||
|
@ -102,6 +102,15 @@ impl CellDataDecoder for CheckboxTypeOption {
|
|||||||
fn stringify_cell(&self, cell: &Cell) -> String {
|
fn stringify_cell(&self, cell: &Cell) -> String {
|
||||||
Self::CellData::from(cell).to_string()
|
Self::CellData::from(cell).to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn numeric_cell(&self, cell: &Cell) -> Option<f64> {
|
||||||
|
let cell_data = self.parse_cell(cell).ok()?;
|
||||||
|
if cell_data.is_check() {
|
||||||
|
Some(1.0)
|
||||||
|
} else {
|
||||||
|
Some(0.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type CheckboxCellChangeset = String;
|
pub type CheckboxCellChangeset = String;
|
||||||
|
@ -164,6 +164,11 @@ impl CellDataDecoder for ChecklistTypeOption {
|
|||||||
let cell_data = self.parse_cell(cell).unwrap_or_default();
|
let cell_data = self.parse_cell(cell).unwrap_or_default();
|
||||||
self.stringify_cell_data(cell_data)
|
self.stringify_cell_data(cell_data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn numeric_cell(&self, _cell: &Cell) -> Option<f64> {
|
||||||
|
// return the percentage complete if needed
|
||||||
|
None
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TypeOptionCellDataFilter for ChecklistTypeOption {
|
impl TypeOptionCellDataFilter for ChecklistTypeOption {
|
||||||
|
@ -251,6 +251,10 @@ impl CellDataDecoder for DateTypeOption {
|
|||||||
let cell_data = Self::CellData::from(cell);
|
let cell_data = Self::CellData::from(cell);
|
||||||
self.stringify_cell_data(cell_data)
|
self.stringify_cell_data(cell_data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn numeric_cell(&self, _cell: &Cell) -> Option<f64> {
|
||||||
|
None
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CellDataChangeset for DateTypeOption {
|
impl CellDataChangeset for DateTypeOption {
|
||||||
|
@ -208,6 +208,11 @@ impl CellDataDecoder for NumberTypeOption {
|
|||||||
let cell_data = Self::CellData::from(cell);
|
let cell_data = Self::CellData::from(cell);
|
||||||
self.stringify_cell_data(cell_data)
|
self.stringify_cell_data(cell_data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn numeric_cell(&self, cell: &Cell) -> Option<f64> {
|
||||||
|
let num_cell_data = self.parse_cell(cell).ok()?;
|
||||||
|
num_cell_data.0.parse::<f64>().ok()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type NumberCellChangeset = String;
|
pub type NumberCellChangeset = String;
|
||||||
|
@ -118,10 +118,10 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> CellDataDecoder for T
|
impl<T, C> CellDataDecoder for T
|
||||||
where
|
where
|
||||||
T:
|
C: Into<SelectOptionIds> + for<'a> From<&'a Cell>,
|
||||||
SelectTypeOptionSharedAction + TypeOption<CellData = SelectOptionIds> + TypeOptionCellDataSerde,
|
T: SelectTypeOptionSharedAction + TypeOption<CellData = C> + TypeOptionCellDataSerde,
|
||||||
{
|
{
|
||||||
fn decode_cell(
|
fn decode_cell(
|
||||||
&self,
|
&self,
|
||||||
@ -132,9 +132,9 @@ where
|
|||||||
self.parse_cell(cell)
|
self.parse_cell(cell)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn stringify_cell_data(&self, cell_data: <Self as TypeOption>::CellData) -> String {
|
fn stringify_cell_data(&self, cell_data: C) -> String {
|
||||||
self
|
self
|
||||||
.get_selected_options(cell_data)
|
.get_selected_options(cell_data.into())
|
||||||
.select_options
|
.select_options
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|option| option.name)
|
.map(|option| option.name)
|
||||||
@ -143,9 +143,13 @@ where
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn stringify_cell(&self, cell: &Cell) -> String {
|
fn stringify_cell(&self, cell: &Cell) -> String {
|
||||||
let cell_data = Self::CellData::from(cell);
|
let cell_data = C::from(cell);
|
||||||
self.stringify_cell_data(cell_data)
|
self.stringify_cell_data(cell_data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn numeric_cell(&self, _cell: &Cell) -> Option<f64> {
|
||||||
|
None
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn select_type_option_from_field(
|
pub fn select_type_option_from_field(
|
||||||
|
@ -116,6 +116,10 @@ impl CellDataDecoder for RichTextTypeOption {
|
|||||||
fn stringify_cell(&self, cell: &Cell) -> String {
|
fn stringify_cell(&self, cell: &Cell) -> String {
|
||||||
Self::CellData::from(cell).to_string()
|
Self::CellData::from(cell).to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn numeric_cell(&self, cell: &Cell) -> Option<f64> {
|
||||||
|
StrCellData::from(cell).0.parse::<f64>().ok()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CellDataChangeset for RichTextTypeOption {
|
impl CellDataChangeset for RichTextTypeOption {
|
||||||
|
@ -152,6 +152,10 @@ impl CellDataDecoder for TimestampTypeOption {
|
|||||||
let cell_data = Self::CellData::from(cell);
|
let cell_data = Self::CellData::from(cell);
|
||||||
self.stringify_cell_data(cell_data)
|
self.stringify_cell_data(cell_data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn numeric_cell(&self, _cell: &Cell) -> Option<f64> {
|
||||||
|
None
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CellDataChangeset for TimestampTypeOption {
|
impl CellDataChangeset for TimestampTypeOption {
|
||||||
|
@ -62,7 +62,9 @@ pub trait TypeOptionCellDataHandler: Send + Sync + 'static {
|
|||||||
/// For example, the field type of the [TypeOptionCellDataHandler] is [FieldType::Date], and
|
/// For example, the field type of the [TypeOptionCellDataHandler] is [FieldType::Date], and
|
||||||
/// the if field_type is [FieldType::RichText], then the string would be something like "Mar 14, 2022".
|
/// the if field_type is [FieldType::RichText], then the string would be something like "Mar 14, 2022".
|
||||||
///
|
///
|
||||||
fn stringify_cell_str(&self, cell: &Cell, field_type: &FieldType, field: &Field) -> String;
|
fn handle_stringify_cell(&self, cell: &Cell, field_type: &FieldType, field: &Field) -> String;
|
||||||
|
|
||||||
|
fn handle_numeric_cell(&self, cell: &Cell) -> Option<f64>;
|
||||||
|
|
||||||
/// Format the cell to [BoxCellData] using the passed-in [FieldType] and [Field].
|
/// Format the cell to [BoxCellData] using the passed-in [FieldType] and [Field].
|
||||||
/// The caller can get the cell data by calling [BoxCellData::unbox_or_none].
|
/// The caller can get the cell data by calling [BoxCellData::unbox_or_none].
|
||||||
@ -323,7 +325,7 @@ where
|
|||||||
/// is [FieldType::RichText], then the string will be transformed to a string that separated by comma with the
|
/// is [FieldType::RichText], then the string will be transformed to a string that separated by comma with the
|
||||||
/// option's name.
|
/// option's name.
|
||||||
///
|
///
|
||||||
fn stringify_cell_str(&self, cell: &Cell, field_type: &FieldType, field: &Field) -> String {
|
fn handle_stringify_cell(&self, cell: &Cell, field_type: &FieldType, field: &Field) -> String {
|
||||||
if self.transformable() {
|
if self.transformable() {
|
||||||
let cell_data = self.transform_type_option_cell(cell, field_type, field);
|
let cell_data = self.transform_type_option_cell(cell, field_type, field);
|
||||||
if let Some(cell_data) = cell_data {
|
if let Some(cell_data) = cell_data {
|
||||||
@ -333,6 +335,10 @@ where
|
|||||||
self.stringify_cell(cell)
|
self.stringify_cell(cell)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn handle_numeric_cell(&self, cell: &Cell) -> Option<f64> {
|
||||||
|
self.numeric_cell(cell)
|
||||||
|
}
|
||||||
|
|
||||||
fn get_cell_data(
|
fn get_cell_data(
|
||||||
&self,
|
&self,
|
||||||
cell: &Cell,
|
cell: &Cell,
|
||||||
|
@ -82,6 +82,10 @@ impl CellDataDecoder for URLTypeOption {
|
|||||||
let cell_data = Self::CellData::from(cell);
|
let cell_data = Self::CellData::from(cell);
|
||||||
self.stringify_cell_data(cell_data)
|
self.stringify_cell_data(cell_data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn numeric_cell(&self, _cell: &Cell) -> Option<f64> {
|
||||||
|
None
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type URLCellChangeset = String;
|
pub type URLCellChangeset = String;
|
||||||
|
@ -14,10 +14,11 @@ use lib_infra::priority_task::{QualityOfService, Task, TaskContent, TaskDispatch
|
|||||||
|
|
||||||
use crate::entities::filter_entities::*;
|
use crate::entities::filter_entities::*;
|
||||||
use crate::entities::{FieldType, InsertedRowPB, RowMetaPB};
|
use crate::entities::{FieldType, InsertedRowPB, RowMetaPB};
|
||||||
use crate::services::cell::{AnyTypeCache, CellCache, CellFilterCache};
|
use crate::services::cell::{CellCache, CellFilterCache};
|
||||||
use crate::services::database_view::{DatabaseViewChanged, DatabaseViewChangedNotifier};
|
use crate::services::database_view::{DatabaseViewChanged, DatabaseViewChangedNotifier};
|
||||||
use crate::services::field::*;
|
use crate::services::field::*;
|
||||||
use crate::services::filter::{Filter, FilterChangeset, FilterResult, FilterResultNotification};
|
use crate::services::filter::{Filter, FilterChangeset, FilterResult, FilterResultNotification};
|
||||||
|
use crate::utils::cache::AnyTypeCache;
|
||||||
|
|
||||||
pub trait FilterDelegate: Send + Sync + 'static {
|
pub trait FilterDelegate: Send + Sync + 'static {
|
||||||
fn get_filter(&self, view_id: &str, filter_id: &str) -> Fut<Option<Arc<Filter>>>;
|
fn get_filter(&self, view_id: &str, filter_id: &str) -> Fut<Option<Arc<Filter>>>;
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
pub mod calculations;
|
||||||
pub mod cell;
|
pub mod cell;
|
||||||
pub mod database;
|
pub mod database;
|
||||||
pub mod database_view;
|
pub mod database_view;
|
||||||
|
98
frontend/rust-lib/flowy-database2/src/utils/cache.rs
Normal file
98
frontend/rust-lib/flowy-database2/src/utils/cache.rs
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
use parking_lot::RwLock;
|
||||||
|
use std::any::{type_name, Any};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::fmt::Debug;
|
||||||
|
use std::hash::Hash;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
#[derive(Default, Debug)]
|
||||||
|
/// The better option is use LRU cache
|
||||||
|
pub struct AnyTypeCache<TypeValueKey>(HashMap<TypeValueKey, TypeValue>);
|
||||||
|
|
||||||
|
impl<TypeValueKey> AnyTypeCache<TypeValueKey>
|
||||||
|
where
|
||||||
|
TypeValueKey: Clone + Hash + Eq,
|
||||||
|
{
|
||||||
|
pub fn new() -> Arc<RwLock<AnyTypeCache<TypeValueKey>>> {
|
||||||
|
Arc::new(RwLock::new(AnyTypeCache(HashMap::default())))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn insert<T>(&mut self, key: &TypeValueKey, val: T) -> Option<T>
|
||||||
|
where
|
||||||
|
T: 'static + Send + Sync,
|
||||||
|
{
|
||||||
|
self
|
||||||
|
.0
|
||||||
|
.insert(key.clone(), TypeValue::new(val))
|
||||||
|
.and_then(downcast_owned)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remove(&mut self, key: &TypeValueKey) {
|
||||||
|
self.0.remove(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get<T>(&self, key: &TypeValueKey) -> Option<&T>
|
||||||
|
where
|
||||||
|
T: 'static + Send + Sync,
|
||||||
|
{
|
||||||
|
self
|
||||||
|
.0
|
||||||
|
.get(key)
|
||||||
|
.and_then(|type_value| type_value.boxed.downcast_ref())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_mut<T>(&mut self, key: &TypeValueKey) -> Option<&mut T>
|
||||||
|
where
|
||||||
|
T: 'static + Send + Sync,
|
||||||
|
{
|
||||||
|
self
|
||||||
|
.0
|
||||||
|
.get_mut(key)
|
||||||
|
.and_then(|type_value| type_value.boxed.downcast_mut())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn contains(&self, key: &TypeValueKey) -> bool {
|
||||||
|
self.0.contains_key(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_empty(&self) -> bool {
|
||||||
|
self.0.is_empty()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn downcast_owned<T: 'static + Send + Sync>(type_value: TypeValue) -> Option<T> {
|
||||||
|
type_value.boxed.downcast().ok().map(|boxed| *boxed)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct TypeValue {
|
||||||
|
boxed: Box<dyn Any + Send + Sync + 'static>,
|
||||||
|
#[allow(dead_code)]
|
||||||
|
ty: &'static str,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TypeValue {
|
||||||
|
pub fn new<T>(value: T) -> Self
|
||||||
|
where
|
||||||
|
T: Send + Sync + 'static,
|
||||||
|
{
|
||||||
|
Self {
|
||||||
|
boxed: Box::new(value),
|
||||||
|
ty: type_name::<T>(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::ops::Deref for TypeValue {
|
||||||
|
type Target = Box<dyn Any + Send + Sync + 'static>;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.boxed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::ops::DerefMut for TypeValue {
|
||||||
|
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||||
|
&mut self.boxed
|
||||||
|
}
|
||||||
|
}
|
1
frontend/rust-lib/flowy-database2/src/utils/mod.rs
Normal file
1
frontend/rust-lib/flowy-database2/src/utils/mod.rs
Normal file
@ -0,0 +1 @@
|
|||||||
|
pub mod cache;
|
@ -0,0 +1,87 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use crate::database::calculations_test::script::{CalculationScript::*, DatabaseCalculationTest};
|
||||||
|
|
||||||
|
use collab_database::fields::Field;
|
||||||
|
use flowy_database2::entities::{CalculationType, FieldType, UpdateCalculationChangesetPB};
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn calculations_test() {
|
||||||
|
let mut test = DatabaseCalculationTest::new().await;
|
||||||
|
|
||||||
|
let expected_sum = 25.00000;
|
||||||
|
let expected_min = 1.00000;
|
||||||
|
let expected_average = 5.00000;
|
||||||
|
let expected_max = 14.00000;
|
||||||
|
let expected_median = 3.00000;
|
||||||
|
|
||||||
|
let view_id = &test.view_id;
|
||||||
|
let number_fields = test
|
||||||
|
.fields
|
||||||
|
.clone()
|
||||||
|
.into_iter()
|
||||||
|
.filter(|field| field.field_type == FieldType::Number as i64)
|
||||||
|
.collect::<Vec<Arc<Field>>>();
|
||||||
|
let field_id = &number_fields.first().unwrap().id;
|
||||||
|
|
||||||
|
let calculation_id = "calc_id".to_owned();
|
||||||
|
let scripts = vec![
|
||||||
|
// Insert Sum calculation first time
|
||||||
|
InsertCalculation {
|
||||||
|
payload: UpdateCalculationChangesetPB {
|
||||||
|
view_id: view_id.to_owned(),
|
||||||
|
field_id: field_id.to_owned(),
|
||||||
|
calculation_id: Some(calculation_id.clone()),
|
||||||
|
calculation_type: CalculationType::Sum,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
AssertCalculationValue {
|
||||||
|
expected: expected_sum,
|
||||||
|
},
|
||||||
|
InsertCalculation {
|
||||||
|
payload: UpdateCalculationChangesetPB {
|
||||||
|
view_id: view_id.to_owned(),
|
||||||
|
field_id: field_id.to_owned(),
|
||||||
|
calculation_id: Some(calculation_id.clone()),
|
||||||
|
calculation_type: CalculationType::Min,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
AssertCalculationValue {
|
||||||
|
expected: expected_min,
|
||||||
|
},
|
||||||
|
InsertCalculation {
|
||||||
|
payload: UpdateCalculationChangesetPB {
|
||||||
|
view_id: view_id.to_owned(),
|
||||||
|
field_id: field_id.to_owned(),
|
||||||
|
calculation_id: Some(calculation_id.clone()),
|
||||||
|
calculation_type: CalculationType::Average,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
AssertCalculationValue {
|
||||||
|
expected: expected_average,
|
||||||
|
},
|
||||||
|
InsertCalculation {
|
||||||
|
payload: UpdateCalculationChangesetPB {
|
||||||
|
view_id: view_id.to_owned(),
|
||||||
|
field_id: field_id.to_owned(),
|
||||||
|
calculation_id: Some(calculation_id.clone()),
|
||||||
|
calculation_type: CalculationType::Max,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
AssertCalculationValue {
|
||||||
|
expected: expected_max,
|
||||||
|
},
|
||||||
|
InsertCalculation {
|
||||||
|
payload: UpdateCalculationChangesetPB {
|
||||||
|
view_id: view_id.to_owned(),
|
||||||
|
field_id: field_id.to_owned(),
|
||||||
|
calculation_id: Some(calculation_id),
|
||||||
|
calculation_type: CalculationType::Median,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
AssertCalculationValue {
|
||||||
|
expected: expected_median,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
test.run_scripts(scripts).await;
|
||||||
|
}
|
@ -0,0 +1,2 @@
|
|||||||
|
mod calculation_test;
|
||||||
|
mod script;
|
@ -0,0 +1,74 @@
|
|||||||
|
use tokio::sync::broadcast::Receiver;
|
||||||
|
|
||||||
|
use flowy_database2::entities::UpdateCalculationChangesetPB;
|
||||||
|
use flowy_database2::services::database_view::DatabaseViewChanged;
|
||||||
|
|
||||||
|
use crate::database::database_editor::DatabaseEditorTest;
|
||||||
|
|
||||||
|
pub enum CalculationScript {
|
||||||
|
InsertCalculation {
|
||||||
|
payload: UpdateCalculationChangesetPB,
|
||||||
|
},
|
||||||
|
AssertCalculationValue {
|
||||||
|
expected: f64,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct DatabaseCalculationTest {
|
||||||
|
inner: DatabaseEditorTest,
|
||||||
|
recv: Option<Receiver<DatabaseViewChanged>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DatabaseCalculationTest {
|
||||||
|
pub async fn new() -> Self {
|
||||||
|
let editor_test = DatabaseEditorTest::new_grid().await;
|
||||||
|
Self {
|
||||||
|
inner: editor_test,
|
||||||
|
recv: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn view_id(&self) -> String {
|
||||||
|
self.view_id.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn run_scripts(&mut self, scripts: Vec<CalculationScript>) {
|
||||||
|
for script in scripts {
|
||||||
|
self.run_script(script).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn run_script(&mut self, script: CalculationScript) {
|
||||||
|
match script {
|
||||||
|
CalculationScript::InsertCalculation { payload } => {
|
||||||
|
self.recv = Some(
|
||||||
|
self
|
||||||
|
.editor
|
||||||
|
.subscribe_view_changed(&self.view_id())
|
||||||
|
.await
|
||||||
|
.unwrap(),
|
||||||
|
);
|
||||||
|
self.editor.update_calculation(payload).await.unwrap();
|
||||||
|
},
|
||||||
|
CalculationScript::AssertCalculationValue { expected } => {
|
||||||
|
let calculations = self.editor.get_all_calculations(&self.view_id()).await;
|
||||||
|
let calculation = calculations.items.first().unwrap();
|
||||||
|
assert_eq!(calculation.value, format!("{:.5}", expected));
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::ops::Deref for DatabaseCalculationTest {
|
||||||
|
type Target = DatabaseEditorTest;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.inner
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::ops::DerefMut for DatabaseCalculationTest {
|
||||||
|
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||||
|
&mut self.inner
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,5 @@
|
|||||||
mod block_test;
|
mod block_test;
|
||||||
|
mod calculations_test;
|
||||||
mod cell_test;
|
mod cell_test;
|
||||||
mod database_editor;
|
mod database_editor;
|
||||||
mod field_settings_test;
|
mod field_settings_test;
|
||||||
|
Loading…
Reference in New Issue
Block a user