Browse Source

fix: some bugs (#2639)

* fix: invalid index when insert row

* fix: auto fill 0 in front of number start with .

* fix: #2591

* chore: split the update at and create at test

* chore: fix tauri build
Nathan.fooo 1 year ago
parent
commit
75d40b79d0
29 changed files with 301 additions and 217 deletions
  1. 2 2
      frontend/appflowy_flutter/lib/plugins/database_view/application/row/row_cache.dart
  2. 2 0
      frontend/appflowy_flutter/lib/plugins/database_view/application/row/row_data_controller.dart
  3. 7 1
      frontend/appflowy_flutter/lib/plugins/database_view/application/row/row_service.dart
  4. 4 4
      frontend/appflowy_flutter/lib/plugins/database_view/application/view/view_listener.dart
  5. 4 1
      frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/board_page.dart
  6. 1 1
      frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/calendar_page.dart
  7. 4 4
      frontend/appflowy_flutter/lib/plugins/database_view/grid/application/row/row_action_sheet_bloc.dart
  8. 7 3
      frontend/appflowy_flutter/lib/plugins/database_view/grid/application/row/row_detail_bloc.dart
  9. 1 1
      frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/grid_page.dart
  10. 22 12
      frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/row_detail.dart
  11. 10 10
      frontend/appflowy_tauri/src-tauri/Cargo.lock
  12. 6 6
      frontend/appflowy_tauri/src-tauri/Cargo.toml
  13. 17 21
      frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/row/row_cache.ts
  14. 12 13
      frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/view/view_row_observer.ts
  15. 10 10
      frontend/rust-lib/Cargo.lock
  16. 5 5
      frontend/rust-lib/Cargo.toml
  17. 13 0
      frontend/rust-lib/flowy-database2/src/entities/row_entities.rs
  18. 9 9
      frontend/rust-lib/flowy-database2/src/entities/view_entities.rs
  19. 21 2
      frontend/rust-lib/flowy-database2/src/event_handler.rs
  20. 29 29
      frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs
  21. 2 2
      frontend/rust-lib/flowy-database2/src/services/database_view/notifier.rs
  22. 12 12
      frontend/rust-lib/flowy-database2/src/services/database_view/view_editor.rs
  23. 2 2
      frontend/rust-lib/flowy-database2/src/services/field/type_options/number_type_option/number_tests.rs
  24. 24 7
      frontend/rust-lib/flowy-database2/src/services/field/type_options/number_type_option/number_type_option.rs
  25. 13 3
      frontend/rust-lib/flowy-database2/src/services/field/type_options/number_type_option/number_type_option_entities.rs
  26. 31 35
      frontend/rust-lib/flowy-database2/tests/database/block_test/row_test.rs
  27. 18 7
      frontend/rust-lib/flowy-database2/tests/database/block_test/script.rs
  28. 12 7
      frontend/rust-lib/flowy-database2/tests/database/group_test/script.rs
  29. 1 8
      frontend/rust-lib/flowy-database2/tests/database/mock_data/grid_mock_data.rs

+ 2 - 2
frontend/appflowy_flutter/lib/plugins/database_view/application/row/row_cache.dart

@@ -83,13 +83,13 @@ class RowCache {
     await _cellCache.dispose();
   }
 
-  void applyRowsChanged(RowsChangesetPB changeset) {
+  void applyRowsChanged(RowsChangePB changeset) {
     _deleteRows(changeset.deletedRows);
     _insertRows(changeset.insertedRows);
     _updateRows(changeset.updatedRows);
   }
 
-  void applyRowsVisibility(RowsVisibilityChangesetPB changeset) {
+  void applyRowsVisibility(RowsVisibilityChangePB changeset) {
     _hideRows(changeset.invisibleRows);
     _showRows(changeset.visibleRows);
   }

+ 2 - 0
frontend/appflowy_flutter/lib/plugins/database_view/application/row/row_data_controller.dart

@@ -7,6 +7,7 @@ typedef OnRowChanged = void Function(CellByFieldId, RowsChangedReason);
 
 class RowController {
   final RowId rowId;
+  final String? groupId;
   final String viewId;
   final List<VoidCallback> _onRowChangedListeners = [];
   final RowCache _rowCache;
@@ -17,6 +18,7 @@ class RowController {
     required this.rowId,
     required this.viewId,
     required RowCache rowCache,
+    this.groupId,
   }) : _rowCache = rowCache;
 
   CellByFieldId loadData() {

+ 7 - 1
frontend/appflowy_flutter/lib/plugins/database_view/application/row/row_service.dart

@@ -36,10 +36,16 @@ class RowBackendService {
     return DatabaseEventDeleteRow(payload).send();
   }
 
-  Future<Either<Unit, FlowyError>> duplicateRow(RowId rowId) {
+  Future<Either<Unit, FlowyError>> duplicateRow({
+    required RowId rowId,
+    String? groupId,
+  }) {
     final payload = RowIdPB.create()
       ..viewId = viewId
       ..rowId = rowId;
+    if (groupId != null) {
+      payload.groupId = groupId;
+    }
 
     return DatabaseEventDuplicateRow(payload).send();
   }

+ 4 - 4
frontend/appflowy_flutter/lib/plugins/database_view/application/view/view_listener.dart

@@ -9,9 +9,9 @@ import 'package:appflowy_backend/protobuf/flowy-database2/notification.pb.dart';
 import 'package:appflowy_backend/protobuf/flowy-database2/view_entities.pb.dart';
 
 typedef RowsVisibilityNotifierValue
-    = Either<RowsVisibilityChangesetPB, FlowyError>;
+    = Either<RowsVisibilityChangePB, FlowyError>;
 
-typedef NumberOfRowsNotifierValue = Either<RowsChangesetPB, FlowyError>;
+typedef NumberOfRowsNotifierValue = Either<RowsChangePB, FlowyError>;
 typedef ReorderAllRowsNotifierValue = Either<List<String>, FlowyError>;
 typedef SingleRowNotifierValue = Either<ReorderSingleRowPB, FlowyError>;
 
@@ -54,14 +54,14 @@ class DatabaseViewListener {
       case DatabaseNotification.DidUpdateViewRowsVisibility:
         result.fold(
           (payload) => _rowsVisibility?.value =
-              left(RowsVisibilityChangesetPB.fromBuffer(payload)),
+              left(RowsVisibilityChangePB.fromBuffer(payload)),
           (error) => _rowsVisibility?.value = right(error),
         );
         break;
       case DatabaseNotification.DidUpdateViewRows:
         result.fold(
           (payload) =>
-              _rowsNotifier?.value = left(RowsChangesetPB.fromBuffer(payload)),
+              _rowsNotifier?.value = left(RowsChangePB.fromBuffer(payload)),
           (error) => _rowsNotifier?.value = right(error),
         );
         break;

+ 4 - 1
frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/board_page.dart

@@ -265,6 +265,7 @@ class _BoardContentState extends State<BoardContent> {
         renderHook: renderHook,
         openCard: (context) => _openCard(
           viewId,
+          groupData.group.groupId,
           fieldController,
           rowPB,
           rowCache,
@@ -302,6 +303,7 @@ class _BoardContentState extends State<BoardContent> {
 
   void _openCard(
     String viewId,
+    String groupId,
     FieldController fieldController,
     RowPB rowPB,
     RowCache rowCache,
@@ -317,6 +319,7 @@ class _BoardContentState extends State<BoardContent> {
       rowId: rowInfo.rowPB.id,
       viewId: rowInfo.viewId,
       rowCache: rowCache,
+      groupId: groupId,
     );
 
     FlowyOverlay.show(
@@ -324,7 +327,7 @@ class _BoardContentState extends State<BoardContent> {
       builder: (BuildContext context) {
         return RowDetailPage(
           cellBuilder: GridCellBuilder(cellCache: dataController.cellCache),
-          dataController: dataController,
+          rowController: dataController,
         );
       },
     );

+ 1 - 1
frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/calendar_page.dart

@@ -241,7 +241,7 @@ void showEventDetails({
         cellBuilder: GridCellBuilder(
           cellCache: rowCache.cellCache,
         ),
-        dataController: dataController,
+        rowController: dataController,
       );
     },
   );

+ 4 - 4
frontend/appflowy_flutter/lib/plugins/database_view/grid/application/row/row_action_sheet_bloc.dart

@@ -18,14 +18,14 @@ class RowActionSheetBloc
         super(RowActionSheetState.initial(rowInfo)) {
     on<RowActionSheetEvent>(
       (event, emit) async {
-        await event.map(
-          deleteRow: (_DeleteRow value) async {
+        await event.when(
+          deleteRow: () async {
             final result = await _rowService.deleteRow(state.rowData.rowPB.id);
             logResult(result);
           },
-          duplicateRow: (_DuplicateRow value) async {
+          duplicateRow: () async {
             final result =
-                await _rowService.duplicateRow(state.rowData.rowPB.id);
+                await _rowService.duplicateRow(rowId: state.rowData.rowPB.id);
             logResult(result);
           },
         );

+ 7 - 3
frontend/appflowy_flutter/lib/plugins/database_view/grid/application/row/row_detail_bloc.dart

@@ -38,8 +38,11 @@ class RowDetailBloc extends Bloc<RowDetailEvent, RowDetailState> {
           deleteRow: (rowId) async {
             await rowService.deleteRow(rowId);
           },
-          duplicateRow: (String rowId) async {
-            await rowService.duplicateRow(rowId);
+          duplicateRow: (String rowId, String? groupId) async {
+            await rowService.duplicateRow(
+              rowId: rowId,
+              groupId: groupId,
+            );
           },
         );
       },
@@ -68,7 +71,8 @@ class RowDetailEvent with _$RowDetailEvent {
   const factory RowDetailEvent.initial() = _Initial;
   const factory RowDetailEvent.deleteField(String fieldId) = _DeleteField;
   const factory RowDetailEvent.deleteRow(String rowId) = _DeleteRow;
-  const factory RowDetailEvent.duplicateRow(String rowId) = _DuplicateRow;
+  const factory RowDetailEvent.duplicateRow(String rowId, String? groupId) =
+      _DuplicateRow;
   const factory RowDetailEvent.didReceiveCellDatas(
     List<CellIdentifier> gridCells,
   ) = _DidReceiveCellDatas;

+ 1 - 1
frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/grid_page.dart

@@ -348,7 +348,7 @@ class _GridRowsState extends State<_GridRows> {
       builder: (BuildContext context) {
         return RowDetailPage(
           cellBuilder: cellBuilder,
-          dataController: dataController,
+          rowController: dataController,
         );
       },
     );

+ 22 - 12
frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/row_detail.dart

@@ -26,11 +26,11 @@ import '../../grid/presentation/widgets/header/field_cell.dart';
 import '../../grid/presentation/widgets/header/field_editor.dart';
 
 class RowDetailPage extends StatefulWidget with FlowyOverlayDelegate {
-  final RowController dataController;
+  final RowController rowController;
   final GridCellBuilder cellBuilder;
 
   const RowDetailPage({
-    required this.dataController,
+    required this.rowController,
     required this.cellBuilder,
     Key? key,
   }) : super(key: key);
@@ -49,7 +49,7 @@ class _RowDetailPageState extends State<RowDetailPage> {
     return FlowyDialog(
       child: BlocProvider(
         create: (context) {
-          return RowDetailBloc(dataController: widget.dataController)
+          return RowDetailBloc(dataController: widget.rowController)
             ..add(const RowDetailEvent.initial());
         },
         child: ListView(
@@ -69,11 +69,11 @@ class _RowDetailPageState extends State<RowDetailPage> {
   Widget _responsiveRowInfo() {
     final rowDataColumn = _PropertyColumn(
       cellBuilder: widget.cellBuilder,
-      viewId: widget.dataController.viewId,
+      viewId: widget.rowController.viewId,
     );
     final rowOptionColumn = _RowOptionColumn(
-      viewId: widget.dataController.viewId,
-      rowId: widget.dataController.rowId,
+      viewId: widget.rowController.viewId,
+      rowController: widget.rowController,
     );
     if (MediaQuery.of(context).size.width > 800) {
       return Row(
@@ -372,10 +372,10 @@ GridCellStyle? _customCellStyle(FieldType fieldType) {
 }
 
 class _RowOptionColumn extends StatelessWidget {
-  final String rowId;
+  final RowController rowController;
   const _RowOptionColumn({
     required String viewId,
-    required this.rowId,
+    required this.rowController,
     Key? key,
   }) : super(key: key);
 
@@ -390,8 +390,11 @@ class _RowOptionColumn extends StatelessWidget {
           child: FlowyText(LocaleKeys.grid_row_action.tr()),
         ),
         const VSpace(15),
-        _DeleteButton(rowId: rowId),
-        _DuplicateButton(rowId: rowId),
+        _DeleteButton(rowId: rowController.rowId),
+        _DuplicateButton(
+          rowId: rowController.rowId,
+          groupId: rowController.groupId,
+        ),
       ],
     );
   }
@@ -419,7 +422,12 @@ class _DeleteButton extends StatelessWidget {
 
 class _DuplicateButton extends StatelessWidget {
   final String rowId;
-  const _DuplicateButton({required this.rowId, Key? key}) : super(key: key);
+  final String? groupId;
+  const _DuplicateButton({
+    required this.rowId,
+    this.groupId,
+    Key? key,
+  }) : super(key: key);
 
   @override
   Widget build(BuildContext context) {
@@ -429,7 +437,9 @@ class _DuplicateButton extends StatelessWidget {
         text: FlowyText.regular(LocaleKeys.grid_row_duplicate.tr()),
         leftIcon: const FlowySvg(name: "grid/duplicate"),
         onTap: () {
-          context.read<RowDetailBloc>().add(RowDetailEvent.duplicateRow(rowId));
+          context
+              .read<RowDetailBloc>()
+              .add(RowDetailEvent.duplicateRow(rowId, groupId));
           FlowyOverlay.pop(context);
         },
       ),

+ 10 - 10
frontend/appflowy_tauri/src-tauri/Cargo.lock

@@ -99,7 +99,7 @@ checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8"
 [[package]]
 name = "appflowy-integrate"
 version = "0.1.0"
-source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d6af3a#d6af3a7662a57202e4e5b0a297c28757a18f3372"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=173661#173661cc07cc78f4251983fcf7594533ba63bb5d"
 dependencies = [
  "anyhow",
  "collab",
@@ -1023,7 +1023,7 @@ dependencies = [
 [[package]]
 name = "collab"
 version = "0.1.0"
-source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d6af3a#d6af3a7662a57202e4e5b0a297c28757a18f3372"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=173661#173661cc07cc78f4251983fcf7594533ba63bb5d"
 dependencies = [
  "anyhow",
  "bytes",
@@ -1040,7 +1040,7 @@ dependencies = [
 [[package]]
 name = "collab-client-ws"
 version = "0.1.0"
-source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d6af3a#d6af3a7662a57202e4e5b0a297c28757a18f3372"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=173661#173661cc07cc78f4251983fcf7594533ba63bb5d"
 dependencies = [
  "bytes",
  "collab-sync",
@@ -1058,7 +1058,7 @@ dependencies = [
 [[package]]
 name = "collab-database"
 version = "0.1.0"
-source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d6af3a#d6af3a7662a57202e4e5b0a297c28757a18f3372"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=173661#173661cc07cc78f4251983fcf7594533ba63bb5d"
 dependencies = [
  "anyhow",
  "async-trait",
@@ -1083,7 +1083,7 @@ dependencies = [
 [[package]]
 name = "collab-derive"
 version = "0.1.0"
-source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d6af3a#d6af3a7662a57202e4e5b0a297c28757a18f3372"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=173661#173661cc07cc78f4251983fcf7594533ba63bb5d"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -1095,7 +1095,7 @@ dependencies = [
 [[package]]
 name = "collab-document"
 version = "0.1.0"
-source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d6af3a#d6af3a7662a57202e4e5b0a297c28757a18f3372"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=173661#173661cc07cc78f4251983fcf7594533ba63bb5d"
 dependencies = [
  "anyhow",
  "collab",
@@ -1112,7 +1112,7 @@ dependencies = [
 [[package]]
 name = "collab-folder"
 version = "0.1.0"
-source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d6af3a#d6af3a7662a57202e4e5b0a297c28757a18f3372"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=173661#173661cc07cc78f4251983fcf7594533ba63bb5d"
 dependencies = [
  "anyhow",
  "collab",
@@ -1130,7 +1130,7 @@ dependencies = [
 [[package]]
 name = "collab-persistence"
 version = "0.1.0"
-source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d6af3a#d6af3a7662a57202e4e5b0a297c28757a18f3372"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=173661#173661cc07cc78f4251983fcf7594533ba63bb5d"
 dependencies = [
  "bincode",
  "chrono",
@@ -1150,7 +1150,7 @@ dependencies = [
 [[package]]
 name = "collab-plugins"
 version = "0.1.0"
-source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d6af3a#d6af3a7662a57202e4e5b0a297c28757a18f3372"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=173661#173661cc07cc78f4251983fcf7594533ba63bb5d"
 dependencies = [
  "anyhow",
  "async-trait",
@@ -1180,7 +1180,7 @@ dependencies = [
 [[package]]
 name = "collab-sync"
 version = "0.1.0"
-source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d6af3a#d6af3a7662a57202e4e5b0a297c28757a18f3372"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=173661#173661cc07cc78f4251983fcf7594533ba63bb5d"
 dependencies = [
  "bytes",
  "collab",

+ 6 - 6
frontend/appflowy_tauri/src-tauri/Cargo.toml

@@ -34,12 +34,12 @@ default = ["custom-protocol"]
 custom-protocol = ["tauri/custom-protocol"]
 
 [patch.crates-io]
-collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "d6af3a" }
-collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "d6af3a" }
-collab-persistence = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "d6af3a" }
-collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "d6af3a" }
-collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "d6af3a" }
-appflowy-integrate = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "d6af3a" }
+collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "173661" }
+collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "173661" }
+collab-persistence = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "173661" }
+collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "173661" }
+collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "173661" }
+appflowy-integrate = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "173661" }
 
 #collab = { path = "../../AppFlowy-Collab/collab" }
 #collab-folder = { path = "../../AppFlowy-Collab/collab-folder" }

+ 17 - 21
frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/row/row_cache.ts

@@ -4,17 +4,17 @@ import {
   UpdatedRowPB,
   RowIdPB,
   OptionalRowPB,
-  RowsChangesetPB,
-  RowsVisibilityChangesetPB,
-  ReorderSingleRowPB
-} from "@/services/backend";
-import { ChangeNotifier } from "$app/utils/change_notifier";
-import { FieldInfo } from "../field/field_controller";
-import { CellCache, CellCacheKey } from "../cell/cell_cache";
-import { CellIdentifier } from "../cell/cell_bd_svc";
-import { DatabaseEventGetRow } from "@/services/backend/events/flowy-database2";
-import { None, Option, Some } from "ts-results";
-import { Log } from "$app/utils/log";
+  RowsChangePB,
+  RowsVisibilityChangePB,
+  ReorderSingleRowPB,
+} from '@/services/backend';
+import { ChangeNotifier } from '$app/utils/change_notifier';
+import { FieldInfo } from '../field/field_controller';
+import { CellCache, CellCacheKey } from '../cell/cell_cache';
+import { CellIdentifier } from '../cell/cell_bd_svc';
+import { DatabaseEventGetRow } from '@/services/backend/events/flowy-database2';
+import { None, Option, Some } from 'ts-results';
+import { Log } from '$app/utils/log';
 
 export type CellByFieldId = Map<string, CellIdentifier>;
 
@@ -82,13 +82,13 @@ export class RowCache {
     this.notifier.withChange(RowChangedReason.ReorderRows);
   };
 
-  applyRowsChanged = (changeset: RowsChangesetPB) => {
+  applyRowsChanged = (changeset: RowsChangePB) => {
     this._deleteRows(changeset.deleted_rows);
     this._insertRows(changeset.inserted_rows);
     this._updateRows(changeset.updated_rows);
   };
 
-  applyRowsVisibility = (changeset: RowsVisibilityChangesetPB) => {
+  applyRowsVisibility = (changeset: RowsVisibilityChangePB) => {
     this._hideRows(changeset.invisible_rows);
     this._displayRows(changeset.visible_rows);
   };
@@ -339,8 +339,7 @@ export class RowInfo {
     public readonly viewId: string,
     public readonly fieldInfos: readonly FieldInfo[],
     public readonly row: RowPB
-  ) {
-  }
+  ) {}
 
   copyWith = (params: { row?: RowPB; fieldInfos?: readonly FieldInfo[] }) => {
     return new RowInfo(this.viewId, params.fieldInfos || this.fieldInfos, params.row || this.row);
@@ -348,18 +347,15 @@ export class RowInfo {
 }
 
 export class DeletedRow {
-  constructor(public readonly index: number, public readonly rowInfo: RowInfo) {
-  }
+  constructor(public readonly index: number, public readonly rowInfo: RowInfo) {}
 }
 
 export class InsertedRow {
-  constructor(public readonly index: number, public readonly rowId: string) {
-  }
+  constructor(public readonly index: number, public readonly rowId: string) {}
 }
 
 export class RowChanged {
-  constructor(public readonly reason: RowChangedReason, public readonly rowId?: string) {
-  }
+  constructor(public readonly reason: RowChangedReason, public readonly rowId?: string) {}
 }
 
 // eslint-disable-next-line no-shadow

+ 12 - 13
frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/view/view_row_observer.ts

@@ -1,17 +1,17 @@
-import { Ok, Result } from "ts-results";
+import { Ok, Result } from 'ts-results';
 import {
   DatabaseNotification,
   FlowyError,
   ReorderAllRowsPB,
   ReorderSingleRowPB,
-  RowsChangesetPB,
-  RowsVisibilityChangesetPB
-} from "@/services/backend";
-import { ChangeNotifier } from "$app/utils/change_notifier";
-import { DatabaseNotificationObserver } from "../notifications/observer";
+  RowsChangePB,
+  RowsVisibilityChangePB,
+} from '@/services/backend';
+import { ChangeNotifier } from '$app/utils/change_notifier';
+import { DatabaseNotificationObserver } from '../notifications/observer';
 
-export type RowsVisibilityNotifyValue = Result<RowsVisibilityChangesetPB, FlowyError>;
-export type RowsNotifyValue = Result<RowsChangesetPB, FlowyError>;
+export type RowsVisibilityNotifyValue = Result<RowsVisibilityChangePB, FlowyError>;
+export type RowsNotifyValue = Result<RowsChangePB, FlowyError>;
 export type ReorderRowsNotifyValue = Result<string[], FlowyError>;
 export type ReorderSingleRowNotifyValue = Result<ReorderSingleRowPB, FlowyError>;
 
@@ -23,8 +23,7 @@ export class DatabaseViewRowsObserver {
 
   private _listener?: DatabaseNotificationObserver;
 
-  constructor(public readonly viewId: string) {
-  }
+  constructor(public readonly viewId: string) {}
 
   subscribe = async (callbacks: {
     onRowsVisibilityChanged?: (value: RowsVisibilityNotifyValue) => void;
@@ -44,14 +43,14 @@ export class DatabaseViewRowsObserver {
         switch (notification) {
           case DatabaseNotification.DidUpdateViewRowsVisibility:
             if (result.ok) {
-              this.rowsVisibilityNotifier.notify(Ok(RowsVisibilityChangesetPB.deserializeBinary(result.val)));
+              this.rowsVisibilityNotifier.notify(Ok(RowsVisibilityChangePB.deserializeBinary(result.val)));
             } else {
               this.rowsVisibilityNotifier.notify(result);
             }
             break;
           case DatabaseNotification.DidUpdateViewRows:
             if (result.ok) {
-              this.rowsNotifier.notify(Ok(RowsChangesetPB.deserializeBinary(result.val)));
+              this.rowsNotifier.notify(Ok(RowsChangePB.deserializeBinary(result.val)));
             } else {
               this.rowsNotifier.notify(result);
             }
@@ -73,7 +72,7 @@ export class DatabaseViewRowsObserver {
           default:
             break;
         }
-      }
+      },
     });
     await this._listener.start();
   };

+ 10 - 10
frontend/rust-lib/Cargo.lock

@@ -85,7 +85,7 @@ checksum = "7de8ce5e0f9f8d88245311066a578d72b7af3e7088f32783804676302df237e4"
 [[package]]
 name = "appflowy-integrate"
 version = "0.1.0"
-source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d6af3a#d6af3a7662a57202e4e5b0a297c28757a18f3372"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=173661#173661cc07cc78f4251983fcf7594533ba63bb5d"
 dependencies = [
  "anyhow",
  "collab",
@@ -886,7 +886,7 @@ dependencies = [
 [[package]]
 name = "collab"
 version = "0.1.0"
-source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d6af3a#d6af3a7662a57202e4e5b0a297c28757a18f3372"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=173661#173661cc07cc78f4251983fcf7594533ba63bb5d"
 dependencies = [
  "anyhow",
  "bytes",
@@ -903,7 +903,7 @@ dependencies = [
 [[package]]
 name = "collab-client-ws"
 version = "0.1.0"
-source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d6af3a#d6af3a7662a57202e4e5b0a297c28757a18f3372"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=173661#173661cc07cc78f4251983fcf7594533ba63bb5d"
 dependencies = [
  "bytes",
  "collab-sync",
@@ -921,7 +921,7 @@ dependencies = [
 [[package]]
 name = "collab-database"
 version = "0.1.0"
-source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d6af3a#d6af3a7662a57202e4e5b0a297c28757a18f3372"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=173661#173661cc07cc78f4251983fcf7594533ba63bb5d"
 dependencies = [
  "anyhow",
  "async-trait",
@@ -946,7 +946,7 @@ dependencies = [
 [[package]]
 name = "collab-derive"
 version = "0.1.0"
-source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d6af3a#d6af3a7662a57202e4e5b0a297c28757a18f3372"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=173661#173661cc07cc78f4251983fcf7594533ba63bb5d"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -958,7 +958,7 @@ dependencies = [
 [[package]]
 name = "collab-document"
 version = "0.1.0"
-source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d6af3a#d6af3a7662a57202e4e5b0a297c28757a18f3372"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=173661#173661cc07cc78f4251983fcf7594533ba63bb5d"
 dependencies = [
  "anyhow",
  "collab",
@@ -975,7 +975,7 @@ dependencies = [
 [[package]]
 name = "collab-folder"
 version = "0.1.0"
-source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d6af3a#d6af3a7662a57202e4e5b0a297c28757a18f3372"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=173661#173661cc07cc78f4251983fcf7594533ba63bb5d"
 dependencies = [
  "anyhow",
  "collab",
@@ -993,7 +993,7 @@ dependencies = [
 [[package]]
 name = "collab-persistence"
 version = "0.1.0"
-source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d6af3a#d6af3a7662a57202e4e5b0a297c28757a18f3372"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=173661#173661cc07cc78f4251983fcf7594533ba63bb5d"
 dependencies = [
  "bincode",
  "chrono",
@@ -1013,7 +1013,7 @@ dependencies = [
 [[package]]
 name = "collab-plugins"
 version = "0.1.0"
-source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d6af3a#d6af3a7662a57202e4e5b0a297c28757a18f3372"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=173661#173661cc07cc78f4251983fcf7594533ba63bb5d"
 dependencies = [
  "anyhow",
  "async-trait",
@@ -1043,7 +1043,7 @@ dependencies = [
 [[package]]
 name = "collab-sync"
 version = "0.1.0"
-source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d6af3a#d6af3a7662a57202e4e5b0a297c28757a18f3372"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=173661#173661cc07cc78f4251983fcf7594533ba63bb5d"
 dependencies = [
  "bytes",
  "collab",

+ 5 - 5
frontend/rust-lib/Cargo.toml

@@ -33,11 +33,11 @@ opt-level = 3
 incremental = false
 
 [patch.crates-io]
-collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "d6af3a"  }
-collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "d6af3a"  }
-collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "d6af3a" }
-collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "d6af3a" }
-appflowy-integrate = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "d6af3a" }
+collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "173661"  }
+collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "173661"  }
+collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "173661" }
+collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "173661" }
+appflowy-integrate = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "173661" }
 
 #collab = { path = "../AppFlowy-Collab/collab" }
 #collab-folder = { path = "../AppFlowy-Collab/collab-folder" }

+ 13 - 0
frontend/rust-lib/flowy-database2/src/entities/row_entities.rs

@@ -142,11 +142,15 @@ pub struct RowIdPB {
 
   #[pb(index = 2)]
   pub row_id: String,
+
+  #[pb(index = 3, one_of)]
+  pub group_id: Option<String>,
 }
 
 pub struct RowIdParams {
   pub view_id: String,
   pub row_id: RowId,
+  pub group_id: Option<String>,
 }
 
 impl TryInto<RowIdParams> for RowIdPB {
@@ -154,10 +158,19 @@ impl TryInto<RowIdParams> for RowIdPB {
 
   fn try_into(self) -> Result<RowIdParams, Self::Error> {
     let view_id = NotEmptyStr::parse(self.view_id).map_err(|_| ErrorCode::DatabaseIdIsEmpty)?;
+    let group_id = match self.group_id {
+      Some(group_id) => Some(
+        NotEmptyStr::parse(group_id)
+          .map_err(|_| ErrorCode::GroupIdIsEmpty)?
+          .0,
+      ),
+      None => None,
+    };
 
     Ok(RowIdParams {
       view_id: view_id.0,
       row_id: RowId::from(self.row_id),
+      group_id,
     })
   }
 }

+ 9 - 9
frontend/rust-lib/flowy-database2/src/entities/view_entities.rs

@@ -3,7 +3,7 @@ use flowy_derive::ProtoBuf;
 use crate::entities::{InsertedRowPB, UpdatedRowPB};
 
 #[derive(Debug, Default, Clone, ProtoBuf)]
-pub struct RowsVisibilityChangesetPB {
+pub struct RowsVisibilityChangePB {
   #[pb(index = 1)]
   pub view_id: String,
 
@@ -15,7 +15,7 @@ pub struct RowsVisibilityChangesetPB {
 }
 
 #[derive(Debug, Default, Clone, ProtoBuf)]
-pub struct RowsChangesetPB {
+pub struct RowsChangePB {
   #[pb(index = 1)]
   pub view_id: String,
 
@@ -29,27 +29,27 @@ pub struct RowsChangesetPB {
   pub updated_rows: Vec<UpdatedRowPB>,
 }
 
-impl RowsChangesetPB {
-  pub fn from_insert(view_id: String, inserted_rows: Vec<InsertedRowPB>) -> Self {
+impl RowsChangePB {
+  pub fn from_insert(view_id: String, inserted_row: InsertedRowPB) -> Self {
     Self {
       view_id,
-      inserted_rows,
+      inserted_rows: vec![inserted_row],
       ..Default::default()
     }
   }
 
-  pub fn from_delete(view_id: String, deleted_rows: Vec<String>) -> Self {
+  pub fn from_delete(view_id: String, deleted_row: String) -> Self {
     Self {
       view_id,
-      deleted_rows,
+      deleted_rows: vec![deleted_row],
       ..Default::default()
     }
   }
 
-  pub fn from_update(view_id: String, updated_rows: Vec<UpdatedRowPB>) -> Self {
+  pub fn from_update(view_id: String, updated_row: UpdatedRowPB) -> Self {
     Self {
       view_id,
-      updated_rows,
+      updated_rows: vec![updated_row],
       ..Default::default()
     }
   }

+ 21 - 2
frontend/rust-lib/flowy-database2/src/event_handler.rs

@@ -1,13 +1,16 @@
+use collab_database::database::gen_row_id;
 use std::sync::Arc;
 
 use collab_database::rows::RowId;
 use collab_database::views::DatabaseLayout;
+use lib_infra::util::timestamp;
 
 use flowy_error::{ErrorCode, FlowyError, FlowyResult};
 use lib_dispatch::prelude::{data_result_ok, AFPluginData, AFPluginState, DataResult};
 
 use crate::entities::*;
 use crate::manager::DatabaseManager2;
+use crate::services::cell::CellBuilder;
 
 use crate::services::field::{
   type_option_data_from_pb_or_default, DateCellChangeset, SelectOptionCellChangeset,
@@ -304,7 +307,7 @@ pub(crate) async fn duplicate_row_handler(
   let params: RowIdParams = data.into_inner().try_into()?;
   let database_editor = manager.get_database_with_view_id(&params.view_id).await?;
   database_editor
-    .duplicate_row(&params.view_id, &params.row_id)
+    .duplicate_row(&params.view_id, params.group_id, &params.row_id)
     .await;
   Ok(())
 }
@@ -329,7 +332,23 @@ pub(crate) async fn create_row_handler(
 ) -> DataResult<RowPB, FlowyError> {
   let params: CreateRowParams = data.into_inner().try_into()?;
   let database_editor = manager.get_database_with_view_id(&params.view_id).await?;
-  match database_editor.create_row(params).await? {
+  let fields = database_editor.get_fields(&params.view_id, None);
+  let cells =
+    CellBuilder::with_cells(params.cell_data_by_field_id.unwrap_or_default(), &fields).build();
+  let view_id = params.view_id;
+  let group_id = params.group_id;
+  let params = collab_database::rows::CreateRowParams {
+    id: gen_row_id(),
+    cells,
+    height: 60,
+    visibility: true,
+    prev_row_id: params.start_row_id,
+    timestamp: timestamp(),
+  };
+  match database_editor
+    .create_row(&view_id, group_id, params)
+    .await?
+  {
     None => Err(FlowyError::internal().context("Create row fail")),
     Some(row) => data_result_ok(RowPB::from(row)),
   }

+ 29 - 29
frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs

@@ -3,9 +3,9 @@ use std::ops::Deref;
 use std::sync::Arc;
 
 use bytes::Bytes;
-use collab_database::database::{gen_row_id, timestamp, Database as InnerDatabase};
+use collab_database::database::{timestamp, Database as InnerDatabase};
 use collab_database::fields::{Field, TypeOptionData};
-use collab_database::rows::{Cell, Cells, Row, RowCell, RowId};
+use collab_database::rows::{Cell, Cells, CreateRowParams, Row, RowCell, RowId};
 use collab_database::views::{DatabaseLayout, DatabaseView, LayoutSetting};
 use parking_lot::Mutex;
 use tokio::sync::{broadcast, RwLock};
@@ -16,14 +16,14 @@ use lib_infra::future::{to_fut, Fut};
 
 use crate::entities::{
   AlterFilterParams, AlterSortParams, CalendarEventPB, CellChangesetNotifyPB, CellPB,
-  CreateRowParams, DatabaseFieldChangesetPB, DatabasePB, DatabaseViewSettingPB, DeleteFilterParams,
+  DatabaseFieldChangesetPB, DatabasePB, DatabaseViewSettingPB, DeleteFilterParams,
   DeleteGroupParams, DeleteSortParams, FieldChangesetParams, FieldIdPB, FieldPB, FieldType,
   GroupPB, IndexFieldPB, InsertGroupParams, InsertedRowPB, LayoutSettingParams, RepeatedFilterPB,
-  RepeatedGroupPB, RepeatedSortPB, RowPB, RowsChangesetPB, SelectOptionCellDataPB, SelectOptionPB,
+  RepeatedGroupPB, RepeatedSortPB, RowPB, RowsChangePB, SelectOptionCellDataPB, SelectOptionPB,
 };
 use crate::notification::{send_notification, DatabaseNotification};
 use crate::services::cell::{
-  apply_cell_changeset, get_cell_protobuf, insert_date_cell, AnyTypeCache, CellBuilder, CellCache,
+  apply_cell_changeset, get_cell_protobuf, insert_date_cell, AnyTypeCache, CellCache,
   ToCellChangeset,
 };
 use crate::services::database::util::database_view_setting_pb_from_view;
@@ -274,8 +274,17 @@ impl DatabaseEditor {
     Ok(())
   }
 
-  pub async fn duplicate_row(&self, view_id: &str, row_id: &RowId) {
-    let _ = self.database.lock().duplicate_row(view_id, row_id);
+  // consider returning a result. But most of the time, it should be fine to just ignore the error.
+  pub async fn duplicate_row(&self, view_id: &str, group_id: Option<String>, row_id: &RowId) {
+    let params = self.database.lock().duplicate_row(row_id);
+    match params {
+      None => {
+        tracing::warn!("Failed to duplicate row: {}", row_id);
+      },
+      Some(params) => {
+        let _ = self.create_row(view_id, group_id, params).await;
+      },
+    }
   }
 
   pub async fn move_row(&self, view_id: &str, from: RowId, to: RowId) {
@@ -292,39 +301,30 @@ impl DatabaseEditor {
 
       let delete_row_id = from.into_inner();
       let insert_row = InsertedRowPB::from(&row).with_index(to_index as i32);
-      let changeset =
-        RowsChangesetPB::from_move(view_id.to_string(), vec![delete_row_id], vec![insert_row]);
+      let changes =
+        RowsChangePB::from_move(view_id.to_string(), vec![delete_row_id], vec![insert_row]);
       send_notification(view_id, DatabaseNotification::DidUpdateViewRows)
-        .payload(changeset)
+        .payload(changes)
         .send();
     }
   }
 
-  pub async fn create_row(&self, params: CreateRowParams) -> FlowyResult<Option<Row>> {
-    let fields = self.database.lock().get_fields(&params.view_id, None);
-    let mut cells =
-      CellBuilder::with_cells(params.cell_data_by_field_id.unwrap_or_default(), &fields).build();
+  pub async fn create_row(
+    &self,
+    view_id: &str,
+    group_id: Option<String>,
+    mut params: CreateRowParams,
+  ) -> FlowyResult<Option<Row>> {
     for view in self.database_views.editors().await {
-      view.v_will_create_row(&mut cells, &params.group_id).await;
+      view.v_will_create_row(&mut params.cells, &group_id).await;
     }
-
-    let result = self.database.lock().create_row_in_view(
-      &params.view_id,
-      collab_database::rows::CreateRowParams {
-        id: gen_row_id(),
-        cells,
-        height: 60,
-        visibility: true,
-        prev_row_id: params.start_row_id,
-        timestamp: timestamp(),
-      },
-    );
-
+    let result = self.database.lock().create_row_in_view(view_id, params);
     if let Some((index, row_order)) = result {
+      tracing::trace!("create row: {:?} at {}", row_order, index);
       let row = self.database.lock().get_row(&row_order.id);
       if let Some(row) = row {
         for view in self.database_views.editors().await {
-          view.v_did_create_row(&row, &params.group_id, index).await;
+          view.v_did_create_row(&row, &group_id, index).await;
         }
         return Ok(Some(row));
       }

+ 2 - 2
frontend/rust-lib/flowy-database2/src/services/database_view/notifier.rs

@@ -1,7 +1,7 @@
 #![allow(clippy::while_let_loop)]
 use crate::entities::{
   DatabaseViewSettingPB, FilterChangesetNotificationPB, GroupChangesetPB, GroupRowsNotificationPB,
-  ReorderAllRowsPB, ReorderSingleRowPB, RowsVisibilityChangesetPB, SortChangesetNotificationPB,
+  ReorderAllRowsPB, ReorderSingleRowPB, RowsVisibilityChangePB, SortChangesetNotificationPB,
 };
 use crate::notification::{send_notification, DatabaseNotification};
 use crate::services::filter::FilterResultNotification;
@@ -38,7 +38,7 @@ impl DatabaseViewChangedReceiverRunner {
       .for_each(|changed| async {
         match changed {
           DatabaseViewChanged::FilterNotification(notification) => {
-            let changeset = RowsVisibilityChangesetPB {
+            let changeset = RowsVisibilityChangePB {
               view_id: notification.view_id,
               visible_rows: notification.visible_rows,
               invisible_rows: notification

+ 12 - 12
frontend/rust-lib/flowy-database2/src/services/database_view/view_editor.rs

@@ -16,7 +16,7 @@ use crate::entities::{
   AlterFilterParams, AlterSortParams, CalendarEventPB, DeleteFilterParams, DeleteGroupParams,
   DeleteSortParams, FieldType, GroupChangesetPB, GroupPB, GroupRowsNotificationPB,
   InsertGroupParams, InsertedGroupPB, InsertedRowPB, LayoutSettingPB, LayoutSettingParams, RowPB,
-  RowsChangesetPB, SortChangesetNotificationPB, SortPB,
+  RowsChangePB, SortChangesetNotificationPB, SortPB,
 };
 use crate::notification::{send_notification, DatabaseNotification};
 use crate::services::cell::CellCache;
@@ -182,9 +182,10 @@ impl DatabaseViewEditor {
     // Send the group notification if the current view has groups
     match group_id.as_ref() {
       None => {
-        let changeset = RowsChangesetPB::from_insert(self.view_id.clone(), vec![row.into()]);
+        let row = InsertedRowPB::from(row).with_index(index as i32);
+        let changes = RowsChangePB::from_insert(self.view_id.clone(), row);
         send_notification(&self.view_id, DatabaseNotification::DidUpdateViewRows)
-          .payload(changeset)
+          .payload(changes)
           .send();
       },
       Some(group_id) => {
@@ -219,16 +220,15 @@ impl DatabaseViewEditor {
         notify_did_update_group_rows(changeset).await;
       }
     }
-    let changeset =
-      RowsChangesetPB::from_delete(self.view_id.clone(), vec![row.id.clone().into_inner()]);
+    let changes = RowsChangePB::from_delete(self.view_id.clone(), row.id.clone().into_inner());
     send_notification(&self.view_id, DatabaseNotification::DidUpdateViewRows)
-      .payload(changeset)
+      .payload(changes)
       .send();
   }
 
   /// Notify the view that the row has been updated. If the view has groups,
   /// send the group notification with [GroupRowsNotificationPB]. Otherwise,
-  /// send the view notification with [RowsChangesetPB]
+  /// send the view notification with [RowsChangePB]
   pub async fn v_did_update_row(&self, old_row: &Option<Row>, row: &Row, field_id: &str) {
     let result = self
       .mut_group_controller(|group_controller, field| {
@@ -263,7 +263,7 @@ impl DatabaseViewEditor {
         row: RowOrder::from(row),
         field_ids: vec![field_id.to_string()],
       };
-      let changeset = RowsChangesetPB::from_update(self.view_id.clone(), vec![update_row.into()]);
+      let changeset = RowsChangePB::from_update(self.view_id.clone(), update_row.into());
       send_notification(&self.view_id, DatabaseNotification::DidUpdateViewRows)
         .payload(changeset)
         .send();
@@ -784,18 +784,18 @@ impl DatabaseViewEditor {
   pub async fn handle_row_event(&self, event: Cow<'_, DatabaseRowEvent>) {
     let changeset = match event.into_owned() {
       DatabaseRowEvent::InsertRow(row) => {
-        RowsChangesetPB::from_insert(self.view_id.clone(), vec![row.into()])
+        RowsChangePB::from_insert(self.view_id.clone(), row.into())
       },
       DatabaseRowEvent::UpdateRow(row) => {
-        RowsChangesetPB::from_update(self.view_id.clone(), vec![row.into()])
+        RowsChangePB::from_update(self.view_id.clone(), row.into())
       },
       DatabaseRowEvent::DeleteRow(row_id) => {
-        RowsChangesetPB::from_delete(self.view_id.clone(), vec![row_id.into_inner()])
+        RowsChangePB::from_delete(self.view_id.clone(), row_id.into_inner())
       },
       DatabaseRowEvent::Move {
         deleted_row_id,
         inserted_row,
-      } => RowsChangesetPB::from_move(
+      } => RowsChangePB::from_move(
         self.view_id.clone(),
         vec![deleted_row_id.into_inner()],
         vec![inserted_row.into()],

+ 2 - 2
frontend/rust-lib/flowy-database2/src/services/field/type_options/number_type_option/number_tests.rs

@@ -16,8 +16,6 @@ mod tests {
 
     // Input is empty String
     assert_number(&type_option, "", "", &field_type, &field);
-
-    // Input is letter
     assert_number(&type_option, "abc", "", &field_type, &field);
     assert_number(&type_option, "-123", "-123", &field_type, &field);
     assert_number(&type_option, "abc-123", "-123", &field_type, &field);
@@ -25,6 +23,7 @@ mod tests {
     assert_number(&type_option, "0.2", "0.2", &field_type, &field);
     assert_number(&type_option, "-0.2", "-0.2", &field_type, &field);
     assert_number(&type_option, "-$0.2", "0.2", &field_type, &field);
+    assert_number(&type_option, ".2", "0.2", &field_type, &field);
   }
 
   #[test]
@@ -42,6 +41,7 @@ mod tests {
     assert_number(&type_option, "-0.2", "-$0.2", &field_type, &field);
     assert_number(&type_option, "-$0.2", "-$0.2", &field_type, &field);
     assert_number(&type_option, "-€0.2", "-$0.2", &field_type, &field);
+    assert_number(&type_option, ".2", "$0.2", &field_type, &field);
   }
 
   #[test]

+ 24 - 7
frontend/rust-lib/flowy-database2/src/services/field/type_options/number_type_option/number_type_option.rs

@@ -128,12 +128,25 @@ impl NumberTypeOption {
             Err(_) => Ok(NumberCellFormat::new()),
           }
         } else {
-          let num_str = match EXTRACT_NUM_REGEX.captures(&num_cell_data.0) {
-            Ok(Some(captures)) => captures
-              .get(0)
-              .map(|m| m.as_str().to_string())
-              .unwrap_or_default(),
-            _ => "".to_string(),
+          // Test the input string is start with dot and only contains number.
+          // If it is, add a 0 before the dot. For example, ".123" -> "0.123"
+          let num_str = match START_WITH_DOT_NUM_REGEX.captures(&num_cell_data.0) {
+            Ok(Some(captures)) => match captures.get(0).map(|m| m.as_str().to_string()) {
+              Some(s) => {
+                format!("0{}", s)
+              },
+              None => "".to_string(),
+            },
+            // Extract the number from the string.
+            // For example, "123abc" -> "123". check out the number_type_option_input_test test for
+            // more examples.
+            _ => match EXTRACT_NUM_REGEX.captures(&num_cell_data.0) {
+              Ok(Some(captures)) => captures
+                .get(0)
+                .map(|m| m.as_str().to_string())
+                .unwrap_or_default(),
+              _ => "".to_string(),
+            },
           };
 
           match Decimal::from_str(&num_str) {
@@ -142,7 +155,10 @@ impl NumberTypeOption {
           }
         }
       },
-      _ => NumberCellFormat::from_format_str(&num_cell_data.0, &self.format),
+      _ => {
+        // If the format is not number, use the format string to format the number.
+        NumberCellFormat::from_format_str(&num_cell_data.0, &self.format)
+      },
     }
   }
 
@@ -261,4 +277,5 @@ impl std::default::Default for NumberTypeOption {
 lazy_static! {
   static ref SCIENTIFIC_NOTATION_REGEX: Regex = Regex::new(r"([+-]?\d*\.?\d+)e([+-]?\d+)").unwrap();
   pub(crate) static ref EXTRACT_NUM_REGEX: Regex = Regex::new(r"-?\d+(\.\d+)?").unwrap();
+  pub(crate) static ref START_WITH_DOT_NUM_REGEX: Regex = Regex::new(r"^\.\d+").unwrap();
 }

+ 13 - 3
frontend/rust-lib/flowy-database2/src/services/field/type_options/number_type_option/number_type_option_entities.rs

@@ -1,6 +1,6 @@
 use crate::services::cell::{CellBytesCustomParser, CellProtobufBlobParser, DecodedCellData};
 use crate::services::field::number_currency::Currency;
-use crate::services::field::{NumberFormat, EXTRACT_NUM_REGEX};
+use crate::services::field::{NumberFormat, EXTRACT_NUM_REGEX, START_WITH_DOT_NUM_REGEX};
 use bytes::Bytes;
 use flowy_error::FlowyResult;
 use rust_decimal::Decimal;
@@ -32,8 +32,8 @@ impl NumberCellFormat {
       Some(offset) => offset != 0,
     };
 
-    // Extract number from string.
-    let num_str = extract_number(num_str);
+    let num_str = auto_fill_zero_at_start_if_need(num_str);
+    let num_str = extract_number(&num_str);
     match Decimal::from_str(&num_str) {
       Ok(mut decimal) => {
         decimal.set_sign_positive(sign_positive);
@@ -70,6 +70,16 @@ impl NumberCellFormat {
   }
 }
 
+fn auto_fill_zero_at_start_if_need(num_str: &str) -> String {
+  match START_WITH_DOT_NUM_REGEX.captures(num_str) {
+    Ok(Some(captures)) => match captures.get(0).map(|m| m.as_str().to_string()) {
+      Some(s) => format!("0{}", s),
+      None => num_str.to_string(),
+    },
+    _ => num_str.to_string(),
+  }
+}
+
 fn extract_number(num_str: &str) -> String {
   let mut matches = EXTRACT_NUM_REGEX.find_iter(num_str);
   let mut values = vec![];

+ 31 - 35
frontend/rust-lib/flowy-database2/tests/database/block_test/row_test.rs

@@ -2,52 +2,48 @@ use crate::database::block_test::script::DatabaseRowTest;
 use crate::database::block_test::script::RowScript::*;
 use flowy_database2::entities::FieldType;
 use flowy_database2::services::field::DateCellData;
+use lib_infra::util::timestamp;
 
+// Create a new row at the end of the grid and check the create time is valid.
 #[tokio::test]
-async fn set_created_at_field_on_create_row() {
+async fn created_at_field_test() {
   let mut test = DatabaseRowTest::new().await;
   let row_count = test.rows.len();
-
-  let before_create_timestamp = chrono::offset::Utc::now().timestamp();
   test
     .run_scripts(vec![CreateEmptyRow, AssertRowCount(row_count + 1)])
     .await;
-  let after_create_timestamp = chrono::offset::Utc::now().timestamp();
-
-  let mut rows = test.rows.clone();
-  rows.sort_by(|r1, r2| r1.created_at.cmp(&r2.created_at));
-  let row = rows.last().unwrap();
 
-  let fields = test.fields.clone();
-  let created_at_field = fields
-    .iter()
-    .find(|&f| FieldType::from(f.field_type) == FieldType::CreatedAt)
-    .unwrap();
-  let cell = row.cells.cell_for_field_id(&created_at_field.id).unwrap();
+  // Get created time of the new row.
+  let row = test.get_rows().await.last().cloned().unwrap();
+  let updated_at_field = test.get_first_field(FieldType::CreatedAt);
+  let cell = row.cells.cell_for_field_id(&updated_at_field.id).unwrap();
   let created_at_timestamp = DateCellData::from(cell).timestamp.unwrap();
 
-  assert!(
-    created_at_timestamp >= before_create_timestamp
-      && created_at_timestamp <= after_create_timestamp,
-    "timestamp: {}, before: {}, after: {}",
-    created_at_timestamp,
-    before_create_timestamp,
-    after_create_timestamp
-  );
+  assert!(created_at_timestamp > 0);
+  assert!(created_at_timestamp < timestamp());
+}
+
+// Update row and check the update time is valid.
+#[tokio::test]
+async fn update_at_field_test() {
+  let mut test = DatabaseRowTest::new().await;
+  let row = test.get_rows().await.remove(0);
+  let updated_at_field = test.get_first_field(FieldType::UpdatedAt);
+  let cell = row.cells.cell_for_field_id(&updated_at_field.id).unwrap();
+  let old_updated_at = DateCellData::from(cell).timestamp.unwrap();
+
+  test
+    .run_script(UpdateTextCell {
+      row_id: row.id.clone(),
+      content: "test".to_string(),
+    })
+    .await;
 
-  let updated_at_field = fields
-    .iter()
-    .find(|&f| FieldType::from(f.field_type) == FieldType::UpdatedAt)
-    .unwrap();
+  // Get the updated time of the row.
+  let row = test.get_rows().await.remove(0);
+  let updated_at_field = test.get_first_field(FieldType::UpdatedAt);
   let cell = row.cells.cell_for_field_id(&updated_at_field.id).unwrap();
-  let updated_at_timestamp = DateCellData::from(cell).timestamp.unwrap();
+  let new_updated_at = DateCellData::from(cell).timestamp.unwrap();
 
-  assert!(
-    updated_at_timestamp >= before_create_timestamp
-      && updated_at_timestamp <= after_create_timestamp,
-    "timestamp: {}, before: {}, after: {}",
-    updated_at_timestamp,
-    before_create_timestamp,
-    after_create_timestamp
-  );
+  assert!(old_updated_at < new_updated_at);
 }

+ 18 - 7
frontend/rust-lib/flowy-database2/tests/database/block_test/script.rs

@@ -1,8 +1,12 @@
 use crate::database::database_editor::DatabaseEditorTest;
-use flowy_database2::entities::CreateRowParams;
+use collab_database::database::gen_row_id;
+use collab_database::rows::RowId;
+
+use lib_infra::util::timestamp;
 
 pub enum RowScript {
   CreateEmptyRow,
+  UpdateTextCell { row_id: RowId, content: String },
   AssertRowCount(usize),
 }
 
@@ -25,18 +29,25 @@ impl DatabaseRowTest {
   pub async fn run_script(&mut self, script: RowScript) {
     match script {
       RowScript::CreateEmptyRow => {
-        let params = CreateRowParams {
-          view_id: self.view_id.clone(),
-          start_row_id: None,
-          group_id: None,
-          cell_data_by_field_id: None,
+        let params = collab_database::rows::CreateRowParams {
+          id: gen_row_id(),
+          timestamp: timestamp(),
+          ..Default::default()
         };
-        let row_order = self.editor.create_row(params).await.unwrap().unwrap();
+        let row_order = self
+          .editor
+          .create_row(&self.view_id, None, params)
+          .await
+          .unwrap()
+          .unwrap();
         self
           .row_by_row_id
           .insert(row_order.id.to_string(), row_order.into());
         self.rows = self.get_rows().await;
       },
+      RowScript::UpdateTextCell { row_id, content } => {
+        self.update_text_cell(row_id, &content).await.unwrap();
+      },
       RowScript::AssertRowCount(expected_row_count) => {
         assert_eq!(expected_row_count, self.rows.len());
       },

+ 12 - 7
frontend/rust-lib/flowy-database2/tests/database/group_test/script.rs

@@ -1,6 +1,7 @@
+use collab_database::database::gen_row_id;
 use collab_database::fields::Field;
-use collab_database::rows::RowId;
-use flowy_database2::entities::{CreateRowParams, FieldType, GroupPB, RowPB};
+use collab_database::rows::{CreateRowParams, RowId};
+use flowy_database2::entities::{FieldType, GroupPB, RowPB};
 use flowy_database2::services::cell::{
   delete_select_option_cell, insert_select_option_cell, insert_url_cell,
 };
@@ -8,6 +9,7 @@ use flowy_database2::services::field::{
   edit_single_select_type_option, SelectOption, SelectTypeOptionSharedAction,
   SingleSelectTypeOption,
 };
+use lib_infra::util::timestamp;
 
 use crate::database::database_editor::DatabaseEditorTest;
 
@@ -126,12 +128,15 @@ impl DatabaseGroupTest {
       GroupScript::CreateRow { group_index } => {
         let group = self.group_at_index(group_index).await;
         let params = CreateRowParams {
-          view_id: self.view_id.clone(),
-          start_row_id: None,
-          group_id: Some(group.group_id.clone()),
-          cell_data_by_field_id: None,
+          id: gen_row_id(),
+          timestamp: timestamp(),
+          ..Default::default()
         };
-        let _ = self.editor.create_row(params).await.unwrap();
+        let _ = self
+          .editor
+          .create_row(&self.view_id, Some(group.group_id.clone()), params)
+          .await
+          .unwrap();
       },
       GroupScript::DeleteRow {
         group_index,

+ 1 - 8
frontend/rust-lib/flowy-database2/tests/database/mock_data/grid_mock_data.rs

@@ -264,14 +264,7 @@ pub fn make_test_grid() -> DatabaseData {
     database_id: gen_database_id(),
     name: "".to_string(),
     layout: DatabaseLayout::Grid,
-    layout_settings: Default::default(),
-    filters: vec![],
-    group_settings: vec![],
-    sorts: vec![],
-    row_orders: vec![],
-    field_orders: vec![],
-    created_at: 0,
-    modified_at: 0,
+    ..Default::default()
   };
 
   DatabaseData { view, fields, rows }