浏览代码

chore: add field builder

appflowy 3 年之前
父节点
当前提交
1e6d82c0ec
共有 44 个文件被更改,包括 1545 次插入569 次删除
  1. 1 1
      frontend/app_flowy/packages/flowy_sdk/lib/protobuf/flowy-grid/protobuf.dart
  2. 458 0
      frontend/app_flowy/packages/flowy_sdk/lib/protobuf/flowy-grid/type_options.pb.dart
  3. 62 0
      frontend/app_flowy/packages/flowy_sdk/lib/protobuf/flowy-grid/type_options.pbenum.dart
  4. 125 0
      frontend/app_flowy/packages/flowy_sdk/lib/protobuf/flowy-grid/type_options.pbjson.dart
  5. 9 0
      frontend/app_flowy/packages/flowy_sdk/lib/protobuf/flowy-grid/type_options.pbserver.dart
  6. 2 0
      frontend/rust-lib/Cargo.lock
  7. 3 11
      frontend/rust-lib/flowy-block/src/editor.rs
  8. 3 3
      frontend/rust-lib/flowy-block/src/queue.rs
  9. 2 2
      frontend/rust-lib/flowy-block/tests/document/mod.rs
  10. 4 6
      frontend/rust-lib/flowy-block/tests/document/script.rs
  11. 17 17
      frontend/rust-lib/flowy-block/tests/document/text_block_test.rs
  12. 1 1
      frontend/rust-lib/flowy-folder/src/services/folder_editor.rs
  13. 12 5
      frontend/rust-lib/flowy-folder/tests/workspace/folder_test.rs
  14. 0 212
      frontend/rust-lib/flowy-folder/tests/workspace/helper.rs
  15. 0 1
      frontend/rust-lib/flowy-folder/tests/workspace/main.rs
  16. 239 11
      frontend/rust-lib/flowy-folder/tests/workspace/script.rs
  17. 6 1
      frontend/rust-lib/flowy-grid/Cargo.toml
  18. 1 1
      frontend/rust-lib/flowy-grid/Flowy.toml
  19. 1 0
      frontend/rust-lib/flowy-grid/src/lib.rs
  20. 12 23
      frontend/rust-lib/flowy-grid/src/macros.rs
  21. 1 1
      frontend/rust-lib/flowy-grid/src/manager.rs
  22. 2 2
      frontend/rust-lib/flowy-grid/src/protobuf/model/mod.rs
  23. 84 83
      frontend/rust-lib/flowy-grid/src/protobuf/model/type_options.rs
  24. 4 4
      frontend/rust-lib/flowy-grid/src/protobuf/proto/type_options.proto
  25. 34 0
      frontend/rust-lib/flowy-grid/src/services/field/cell_stringify.rs
  26. 217 0
      frontend/rust-lib/flowy-grid/src/services/field/field_builder.rs
  27. 7 0
      frontend/rust-lib/flowy-grid/src/services/field/mod.rs
  28. 59 116
      frontend/rust-lib/flowy-grid/src/services/field/type_options.rs
  29. 11 4
      frontend/rust-lib/flowy-grid/src/services/grid_editor.rs
  30. 1 1
      frontend/rust-lib/flowy-grid/src/services/grid_meta_editor.rs
  31. 1 2
      frontend/rust-lib/flowy-grid/src/services/mod.rs
  32. 0 29
      frontend/rust-lib/flowy-grid/src/services/stringify.rs
  33. 5 5
      frontend/rust-lib/flowy-grid/src/services/util.rs
  34. 30 0
      frontend/rust-lib/flowy-grid/src/util.rs
  35. 16 0
      frontend/rust-lib/flowy-grid/tests/grid/grid_test.rs
  36. 2 0
      frontend/rust-lib/flowy-grid/tests/grid/mod.rs
  37. 75 0
      frontend/rust-lib/flowy-grid/tests/grid/script.rs
  38. 0 0
      frontend/rust-lib/flowy-grid/tests/grid_test.rs
  39. 1 0
      frontend/rust-lib/flowy-grid/tests/main.rs
  40. 1 1
      frontend/rust-lib/flowy-sdk/src/deps_resolve/folder_deps.rs
  41. 2 2
      frontend/rust-lib/flowy-sync/src/rev_manager.rs
  42. 15 4
      frontend/rust-lib/flowy-test/src/helper.rs
  43. 4 16
      shared-lib/flowy-collaboration/src/client_grid/grid_builder.rs
  44. 15 4
      shared-lib/flowy-grid-data-model/src/entities/meta.rs

+ 1 - 1
frontend/app_flowy/packages/flowy_sdk/lib/protobuf/flowy-grid/protobuf.dart

@@ -1,3 +1,3 @@
 // Auto-generated, do not edit 
-export './cell_data.pb.dart';
+export './type_options.pb.dart';
 export './event_map.pb.dart';

+ 458 - 0
frontend/app_flowy/packages/flowy_sdk/lib/protobuf/flowy-grid/type_options.pb.dart

@@ -0,0 +1,458 @@
+///
+//  Generated code. Do not modify.
+//  source: type_options.proto
+//
+// @dart = 2.12
+// ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields
+
+import 'dart:core' as $core;
+
+import 'package:protobuf/protobuf.dart' as $pb;
+
+import 'type_options.pbenum.dart';
+
+export 'type_options.pbenum.dart';
+
+class RichTextDescription extends $pb.GeneratedMessage {
+  static final $pb.BuilderInfo _i = $pb.BuilderInfo(const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'RichTextDescription', createEmptyInstance: create)
+    ..aOS(1, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'format')
+    ..hasRequiredFields = false
+  ;
+
+  RichTextDescription._() : super();
+  factory RichTextDescription({
+    $core.String? format,
+  }) {
+    final _result = create();
+    if (format != null) {
+      _result.format = format;
+    }
+    return _result;
+  }
+  factory RichTextDescription.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
+  factory RichTextDescription.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
+  @$core.Deprecated(
+  'Using this can add significant overhead to your binary. '
+  'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
+  'Will be removed in next major version')
+  RichTextDescription clone() => RichTextDescription()..mergeFromMessage(this);
+  @$core.Deprecated(
+  'Using this can add significant overhead to your binary. '
+  'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
+  'Will be removed in next major version')
+  RichTextDescription copyWith(void Function(RichTextDescription) updates) => super.copyWith((message) => updates(message as RichTextDescription)) as RichTextDescription; // ignore: deprecated_member_use
+  $pb.BuilderInfo get info_ => _i;
+  @$core.pragma('dart2js:noInline')
+  static RichTextDescription create() => RichTextDescription._();
+  RichTextDescription createEmptyInstance() => create();
+  static $pb.PbList<RichTextDescription> createRepeated() => $pb.PbList<RichTextDescription>();
+  @$core.pragma('dart2js:noInline')
+  static RichTextDescription getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<RichTextDescription>(create);
+  static RichTextDescription? _defaultInstance;
+
+  @$pb.TagNumber(1)
+  $core.String get format => $_getSZ(0);
+  @$pb.TagNumber(1)
+  set format($core.String v) { $_setString(0, v); }
+  @$pb.TagNumber(1)
+  $core.bool hasFormat() => $_has(0);
+  @$pb.TagNumber(1)
+  void clearFormat() => clearField(1);
+}
+
+class CheckboxDescription extends $pb.GeneratedMessage {
+  static final $pb.BuilderInfo _i = $pb.BuilderInfo(const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'CheckboxDescription', createEmptyInstance: create)
+    ..aOB(1, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'isSelected')
+    ..hasRequiredFields = false
+  ;
+
+  CheckboxDescription._() : super();
+  factory CheckboxDescription({
+    $core.bool? isSelected,
+  }) {
+    final _result = create();
+    if (isSelected != null) {
+      _result.isSelected = isSelected;
+    }
+    return _result;
+  }
+  factory CheckboxDescription.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
+  factory CheckboxDescription.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
+  @$core.Deprecated(
+  'Using this can add significant overhead to your binary. '
+  'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
+  'Will be removed in next major version')
+  CheckboxDescription clone() => CheckboxDescription()..mergeFromMessage(this);
+  @$core.Deprecated(
+  'Using this can add significant overhead to your binary. '
+  'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
+  'Will be removed in next major version')
+  CheckboxDescription copyWith(void Function(CheckboxDescription) updates) => super.copyWith((message) => updates(message as CheckboxDescription)) as CheckboxDescription; // ignore: deprecated_member_use
+  $pb.BuilderInfo get info_ => _i;
+  @$core.pragma('dart2js:noInline')
+  static CheckboxDescription create() => CheckboxDescription._();
+  CheckboxDescription createEmptyInstance() => create();
+  static $pb.PbList<CheckboxDescription> createRepeated() => $pb.PbList<CheckboxDescription>();
+  @$core.pragma('dart2js:noInline')
+  static CheckboxDescription getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<CheckboxDescription>(create);
+  static CheckboxDescription? _defaultInstance;
+
+  @$pb.TagNumber(1)
+  $core.bool get isSelected => $_getBF(0);
+  @$pb.TagNumber(1)
+  set isSelected($core.bool v) { $_setBool(0, v); }
+  @$pb.TagNumber(1)
+  $core.bool hasIsSelected() => $_has(0);
+  @$pb.TagNumber(1)
+  void clearIsSelected() => clearField(1);
+}
+
+class DateDescription extends $pb.GeneratedMessage {
+  static final $pb.BuilderInfo _i = $pb.BuilderInfo(const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'DateDescription', createEmptyInstance: create)
+    ..e<DateFormat>(1, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'dateFormat', $pb.PbFieldType.OE, defaultOrMaker: DateFormat.Local, valueOf: DateFormat.valueOf, enumValues: DateFormat.values)
+    ..e<TimeFormat>(2, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'timeFormat', $pb.PbFieldType.OE, defaultOrMaker: TimeFormat.TwelveHour, valueOf: TimeFormat.valueOf, enumValues: TimeFormat.values)
+    ..hasRequiredFields = false
+  ;
+
+  DateDescription._() : super();
+  factory DateDescription({
+    DateFormat? dateFormat,
+    TimeFormat? timeFormat,
+  }) {
+    final _result = create();
+    if (dateFormat != null) {
+      _result.dateFormat = dateFormat;
+    }
+    if (timeFormat != null) {
+      _result.timeFormat = timeFormat;
+    }
+    return _result;
+  }
+  factory DateDescription.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
+  factory DateDescription.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
+  @$core.Deprecated(
+  'Using this can add significant overhead to your binary. '
+  'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
+  'Will be removed in next major version')
+  DateDescription clone() => DateDescription()..mergeFromMessage(this);
+  @$core.Deprecated(
+  'Using this can add significant overhead to your binary. '
+  'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
+  'Will be removed in next major version')
+  DateDescription copyWith(void Function(DateDescription) updates) => super.copyWith((message) => updates(message as DateDescription)) as DateDescription; // ignore: deprecated_member_use
+  $pb.BuilderInfo get info_ => _i;
+  @$core.pragma('dart2js:noInline')
+  static DateDescription create() => DateDescription._();
+  DateDescription createEmptyInstance() => create();
+  static $pb.PbList<DateDescription> createRepeated() => $pb.PbList<DateDescription>();
+  @$core.pragma('dart2js:noInline')
+  static DateDescription getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<DateDescription>(create);
+  static DateDescription? _defaultInstance;
+
+  @$pb.TagNumber(1)
+  DateFormat get dateFormat => $_getN(0);
+  @$pb.TagNumber(1)
+  set dateFormat(DateFormat v) { setField(1, v); }
+  @$pb.TagNumber(1)
+  $core.bool hasDateFormat() => $_has(0);
+  @$pb.TagNumber(1)
+  void clearDateFormat() => clearField(1);
+
+  @$pb.TagNumber(2)
+  TimeFormat get timeFormat => $_getN(1);
+  @$pb.TagNumber(2)
+  set timeFormat(TimeFormat v) { setField(2, v); }
+  @$pb.TagNumber(2)
+  $core.bool hasTimeFormat() => $_has(1);
+  @$pb.TagNumber(2)
+  void clearTimeFormat() => clearField(2);
+}
+
+class SingleSelectDescription extends $pb.GeneratedMessage {
+  static final $pb.BuilderInfo _i = $pb.BuilderInfo(const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'SingleSelectDescription', createEmptyInstance: create)
+    ..pc<SelectOption>(1, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'options', $pb.PbFieldType.PM, subBuilder: SelectOption.create)
+    ..aOB(2, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'disableColor')
+    ..hasRequiredFields = false
+  ;
+
+  SingleSelectDescription._() : super();
+  factory SingleSelectDescription({
+    $core.Iterable<SelectOption>? options,
+    $core.bool? disableColor,
+  }) {
+    final _result = create();
+    if (options != null) {
+      _result.options.addAll(options);
+    }
+    if (disableColor != null) {
+      _result.disableColor = disableColor;
+    }
+    return _result;
+  }
+  factory SingleSelectDescription.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
+  factory SingleSelectDescription.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
+  @$core.Deprecated(
+  'Using this can add significant overhead to your binary. '
+  'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
+  'Will be removed in next major version')
+  SingleSelectDescription clone() => SingleSelectDescription()..mergeFromMessage(this);
+  @$core.Deprecated(
+  'Using this can add significant overhead to your binary. '
+  'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
+  'Will be removed in next major version')
+  SingleSelectDescription copyWith(void Function(SingleSelectDescription) updates) => super.copyWith((message) => updates(message as SingleSelectDescription)) as SingleSelectDescription; // ignore: deprecated_member_use
+  $pb.BuilderInfo get info_ => _i;
+  @$core.pragma('dart2js:noInline')
+  static SingleSelectDescription create() => SingleSelectDescription._();
+  SingleSelectDescription createEmptyInstance() => create();
+  static $pb.PbList<SingleSelectDescription> createRepeated() => $pb.PbList<SingleSelectDescription>();
+  @$core.pragma('dart2js:noInline')
+  static SingleSelectDescription getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<SingleSelectDescription>(create);
+  static SingleSelectDescription? _defaultInstance;
+
+  @$pb.TagNumber(1)
+  $core.List<SelectOption> get options => $_getList(0);
+
+  @$pb.TagNumber(2)
+  $core.bool get disableColor => $_getBF(1);
+  @$pb.TagNumber(2)
+  set disableColor($core.bool v) { $_setBool(1, v); }
+  @$pb.TagNumber(2)
+  $core.bool hasDisableColor() => $_has(1);
+  @$pb.TagNumber(2)
+  void clearDisableColor() => clearField(2);
+}
+
+class MultiSelectDescription extends $pb.GeneratedMessage {
+  static final $pb.BuilderInfo _i = $pb.BuilderInfo(const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'MultiSelectDescription', createEmptyInstance: create)
+    ..pc<SelectOption>(1, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'options', $pb.PbFieldType.PM, subBuilder: SelectOption.create)
+    ..aOB(2, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'disableColor')
+    ..hasRequiredFields = false
+  ;
+
+  MultiSelectDescription._() : super();
+  factory MultiSelectDescription({
+    $core.Iterable<SelectOption>? options,
+    $core.bool? disableColor,
+  }) {
+    final _result = create();
+    if (options != null) {
+      _result.options.addAll(options);
+    }
+    if (disableColor != null) {
+      _result.disableColor = disableColor;
+    }
+    return _result;
+  }
+  factory MultiSelectDescription.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
+  factory MultiSelectDescription.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
+  @$core.Deprecated(
+  'Using this can add significant overhead to your binary. '
+  'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
+  'Will be removed in next major version')
+  MultiSelectDescription clone() => MultiSelectDescription()..mergeFromMessage(this);
+  @$core.Deprecated(
+  'Using this can add significant overhead to your binary. '
+  'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
+  'Will be removed in next major version')
+  MultiSelectDescription copyWith(void Function(MultiSelectDescription) updates) => super.copyWith((message) => updates(message as MultiSelectDescription)) as MultiSelectDescription; // ignore: deprecated_member_use
+  $pb.BuilderInfo get info_ => _i;
+  @$core.pragma('dart2js:noInline')
+  static MultiSelectDescription create() => MultiSelectDescription._();
+  MultiSelectDescription createEmptyInstance() => create();
+  static $pb.PbList<MultiSelectDescription> createRepeated() => $pb.PbList<MultiSelectDescription>();
+  @$core.pragma('dart2js:noInline')
+  static MultiSelectDescription getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<MultiSelectDescription>(create);
+  static MultiSelectDescription? _defaultInstance;
+
+  @$pb.TagNumber(1)
+  $core.List<SelectOption> get options => $_getList(0);
+
+  @$pb.TagNumber(2)
+  $core.bool get disableColor => $_getBF(1);
+  @$pb.TagNumber(2)
+  set disableColor($core.bool v) { $_setBool(1, v); }
+  @$pb.TagNumber(2)
+  $core.bool hasDisableColor() => $_has(1);
+  @$pb.TagNumber(2)
+  void clearDisableColor() => clearField(2);
+}
+
+class SelectOption extends $pb.GeneratedMessage {
+  static final $pb.BuilderInfo _i = $pb.BuilderInfo(const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'SelectOption', createEmptyInstance: create)
+    ..aOS(1, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'id')
+    ..aOS(2, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'name')
+    ..aOS(3, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'color')
+    ..hasRequiredFields = false
+  ;
+
+  SelectOption._() : super();
+  factory SelectOption({
+    $core.String? id,
+    $core.String? name,
+    $core.String? color,
+  }) {
+    final _result = create();
+    if (id != null) {
+      _result.id = id;
+    }
+    if (name != null) {
+      _result.name = name;
+    }
+    if (color != null) {
+      _result.color = color;
+    }
+    return _result;
+  }
+  factory SelectOption.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
+  factory SelectOption.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
+  @$core.Deprecated(
+  'Using this can add significant overhead to your binary. '
+  'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
+  'Will be removed in next major version')
+  SelectOption clone() => SelectOption()..mergeFromMessage(this);
+  @$core.Deprecated(
+  'Using this can add significant overhead to your binary. '
+  'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
+  'Will be removed in next major version')
+  SelectOption copyWith(void Function(SelectOption) updates) => super.copyWith((message) => updates(message as SelectOption)) as SelectOption; // ignore: deprecated_member_use
+  $pb.BuilderInfo get info_ => _i;
+  @$core.pragma('dart2js:noInline')
+  static SelectOption create() => SelectOption._();
+  SelectOption createEmptyInstance() => create();
+  static $pb.PbList<SelectOption> createRepeated() => $pb.PbList<SelectOption>();
+  @$core.pragma('dart2js:noInline')
+  static SelectOption getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<SelectOption>(create);
+  static SelectOption? _defaultInstance;
+
+  @$pb.TagNumber(1)
+  $core.String get id => $_getSZ(0);
+  @$pb.TagNumber(1)
+  set id($core.String v) { $_setString(0, v); }
+  @$pb.TagNumber(1)
+  $core.bool hasId() => $_has(0);
+  @$pb.TagNumber(1)
+  void clearId() => clearField(1);
+
+  @$pb.TagNumber(2)
+  $core.String get name => $_getSZ(1);
+  @$pb.TagNumber(2)
+  set name($core.String v) { $_setString(1, v); }
+  @$pb.TagNumber(2)
+  $core.bool hasName() => $_has(1);
+  @$pb.TagNumber(2)
+  void clearName() => clearField(2);
+
+  @$pb.TagNumber(3)
+  $core.String get color => $_getSZ(2);
+  @$pb.TagNumber(3)
+  set color($core.String v) { $_setString(2, v); }
+  @$pb.TagNumber(3)
+  $core.bool hasColor() => $_has(2);
+  @$pb.TagNumber(3)
+  void clearColor() => clearField(3);
+}
+
+class NumberDescription extends $pb.GeneratedMessage {
+  static final $pb.BuilderInfo _i = $pb.BuilderInfo(const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'NumberDescription', createEmptyInstance: create)
+    ..e<MoneySymbol>(1, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'money', $pb.PbFieldType.OE, defaultOrMaker: MoneySymbol.CNY, valueOf: MoneySymbol.valueOf, enumValues: MoneySymbol.values)
+    ..a<$core.int>(2, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'scale', $pb.PbFieldType.OU3)
+    ..aOS(3, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'symbol')
+    ..aOB(4, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'signPositive')
+    ..aOS(5, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'name')
+    ..hasRequiredFields = false
+  ;
+
+  NumberDescription._() : super();
+  factory NumberDescription({
+    MoneySymbol? money,
+    $core.int? scale,
+    $core.String? symbol,
+    $core.bool? signPositive,
+    $core.String? name,
+  }) {
+    final _result = create();
+    if (money != null) {
+      _result.money = money;
+    }
+    if (scale != null) {
+      _result.scale = scale;
+    }
+    if (symbol != null) {
+      _result.symbol = symbol;
+    }
+    if (signPositive != null) {
+      _result.signPositive = signPositive;
+    }
+    if (name != null) {
+      _result.name = name;
+    }
+    return _result;
+  }
+  factory NumberDescription.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
+  factory NumberDescription.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
+  @$core.Deprecated(
+  'Using this can add significant overhead to your binary. '
+  'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
+  'Will be removed in next major version')
+  NumberDescription clone() => NumberDescription()..mergeFromMessage(this);
+  @$core.Deprecated(
+  'Using this can add significant overhead to your binary. '
+  'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
+  'Will be removed in next major version')
+  NumberDescription copyWith(void Function(NumberDescription) updates) => super.copyWith((message) => updates(message as NumberDescription)) as NumberDescription; // ignore: deprecated_member_use
+  $pb.BuilderInfo get info_ => _i;
+  @$core.pragma('dart2js:noInline')
+  static NumberDescription create() => NumberDescription._();
+  NumberDescription createEmptyInstance() => create();
+  static $pb.PbList<NumberDescription> createRepeated() => $pb.PbList<NumberDescription>();
+  @$core.pragma('dart2js:noInline')
+  static NumberDescription getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<NumberDescription>(create);
+  static NumberDescription? _defaultInstance;
+
+  @$pb.TagNumber(1)
+  MoneySymbol get money => $_getN(0);
+  @$pb.TagNumber(1)
+  set money(MoneySymbol v) { setField(1, v); }
+  @$pb.TagNumber(1)
+  $core.bool hasMoney() => $_has(0);
+  @$pb.TagNumber(1)
+  void clearMoney() => clearField(1);
+
+  @$pb.TagNumber(2)
+  $core.int get scale => $_getIZ(1);
+  @$pb.TagNumber(2)
+  set scale($core.int v) { $_setUnsignedInt32(1, v); }
+  @$pb.TagNumber(2)
+  $core.bool hasScale() => $_has(1);
+  @$pb.TagNumber(2)
+  void clearScale() => clearField(2);
+
+  @$pb.TagNumber(3)
+  $core.String get symbol => $_getSZ(2);
+  @$pb.TagNumber(3)
+  set symbol($core.String v) { $_setString(2, v); }
+  @$pb.TagNumber(3)
+  $core.bool hasSymbol() => $_has(2);
+  @$pb.TagNumber(3)
+  void clearSymbol() => clearField(3);
+
+  @$pb.TagNumber(4)
+  $core.bool get signPositive => $_getBF(3);
+  @$pb.TagNumber(4)
+  set signPositive($core.bool v) { $_setBool(3, v); }
+  @$pb.TagNumber(4)
+  $core.bool hasSignPositive() => $_has(3);
+  @$pb.TagNumber(4)
+  void clearSignPositive() => clearField(4);
+
+  @$pb.TagNumber(5)
+  $core.String get name => $_getSZ(4);
+  @$pb.TagNumber(5)
+  set name($core.String v) { $_setString(4, v); }
+  @$pb.TagNumber(5)
+  $core.bool hasName() => $_has(4);
+  @$pb.TagNumber(5)
+  void clearName() => clearField(5);
+}
+

+ 62 - 0
frontend/app_flowy/packages/flowy_sdk/lib/protobuf/flowy-grid/type_options.pbenum.dart

@@ -0,0 +1,62 @@
+///
+//  Generated code. Do not modify.
+//  source: type_options.proto
+//
+// @dart = 2.12
+// ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields
+
+// ignore_for_file: UNDEFINED_SHOWN_NAME
+import 'dart:core' as $core;
+import 'package:protobuf/protobuf.dart' as $pb;
+
+class DateFormat extends $pb.ProtobufEnum {
+  static const DateFormat Local = DateFormat._(0, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'Local');
+  static const DateFormat US = DateFormat._(1, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'US');
+  static const DateFormat ISO = DateFormat._(2, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'ISO');
+  static const DateFormat Friendly = DateFormat._(3, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'Friendly');
+
+  static const $core.List<DateFormat> values = <DateFormat> [
+    Local,
+    US,
+    ISO,
+    Friendly,
+  ];
+
+  static final $core.Map<$core.int, DateFormat> _byValue = $pb.ProtobufEnum.initByValue(values);
+  static DateFormat? valueOf($core.int value) => _byValue[value];
+
+  const DateFormat._($core.int v, $core.String n) : super(v, n);
+}
+
+class TimeFormat extends $pb.ProtobufEnum {
+  static const TimeFormat TwelveHour = TimeFormat._(0, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'TwelveHour');
+  static const TimeFormat TwentyFourHour = TimeFormat._(1, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'TwentyFourHour');
+
+  static const $core.List<TimeFormat> values = <TimeFormat> [
+    TwelveHour,
+    TwentyFourHour,
+  ];
+
+  static final $core.Map<$core.int, TimeFormat> _byValue = $pb.ProtobufEnum.initByValue(values);
+  static TimeFormat? valueOf($core.int value) => _byValue[value];
+
+  const TimeFormat._($core.int v, $core.String n) : super(v, n);
+}
+
+class MoneySymbol extends $pb.ProtobufEnum {
+  static const MoneySymbol CNY = MoneySymbol._(0, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'CNY');
+  static const MoneySymbol EUR = MoneySymbol._(1, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'EUR');
+  static const MoneySymbol USD = MoneySymbol._(2, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'USD');
+
+  static const $core.List<MoneySymbol> values = <MoneySymbol> [
+    CNY,
+    EUR,
+    USD,
+  ];
+
+  static final $core.Map<$core.int, MoneySymbol> _byValue = $pb.ProtobufEnum.initByValue(values);
+  static MoneySymbol? valueOf($core.int value) => _byValue[value];
+
+  const MoneySymbol._($core.int v, $core.String n) : super(v, n);
+}
+

+ 125 - 0
frontend/app_flowy/packages/flowy_sdk/lib/protobuf/flowy-grid/type_options.pbjson.dart

@@ -0,0 +1,125 @@
+///
+//  Generated code. Do not modify.
+//  source: type_options.proto
+//
+// @dart = 2.12
+// ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields,deprecated_member_use_from_same_package
+
+import 'dart:core' as $core;
+import 'dart:convert' as $convert;
+import 'dart:typed_data' as $typed_data;
+@$core.Deprecated('Use dateFormatDescriptor instead')
+const DateFormat$json = const {
+  '1': 'DateFormat',
+  '2': const [
+    const {'1': 'Local', '2': 0},
+    const {'1': 'US', '2': 1},
+    const {'1': 'ISO', '2': 2},
+    const {'1': 'Friendly', '2': 3},
+  ],
+};
+
+/// Descriptor for `DateFormat`. Decode as a `google.protobuf.EnumDescriptorProto`.
+final $typed_data.Uint8List dateFormatDescriptor = $convert.base64Decode('CgpEYXRlRm9ybWF0EgkKBUxvY2FsEAASBgoCVVMQARIHCgNJU08QAhIMCghGcmllbmRseRAD');
+@$core.Deprecated('Use timeFormatDescriptor instead')
+const TimeFormat$json = const {
+  '1': 'TimeFormat',
+  '2': const [
+    const {'1': 'TwelveHour', '2': 0},
+    const {'1': 'TwentyFourHour', '2': 1},
+  ],
+};
+
+/// Descriptor for `TimeFormat`. Decode as a `google.protobuf.EnumDescriptorProto`.
+final $typed_data.Uint8List timeFormatDescriptor = $convert.base64Decode('CgpUaW1lRm9ybWF0Eg4KClR3ZWx2ZUhvdXIQABISCg5Ud2VudHlGb3VySG91chAB');
+@$core.Deprecated('Use moneySymbolDescriptor instead')
+const MoneySymbol$json = const {
+  '1': 'MoneySymbol',
+  '2': const [
+    const {'1': 'CNY', '2': 0},
+    const {'1': 'EUR', '2': 1},
+    const {'1': 'USD', '2': 2},
+  ],
+};
+
+/// Descriptor for `MoneySymbol`. Decode as a `google.protobuf.EnumDescriptorProto`.
+final $typed_data.Uint8List moneySymbolDescriptor = $convert.base64Decode('CgtNb25leVN5bWJvbBIHCgNDTlkQABIHCgNFVVIQARIHCgNVU0QQAg==');
+@$core.Deprecated('Use richTextDescriptionDescriptor instead')
+const RichTextDescription$json = const {
+  '1': 'RichTextDescription',
+  '2': const [
+    const {'1': 'format', '3': 1, '4': 1, '5': 9, '10': 'format'},
+  ],
+};
+
+/// Descriptor for `RichTextDescription`. Decode as a `google.protobuf.DescriptorProto`.
+final $typed_data.Uint8List richTextDescriptionDescriptor = $convert.base64Decode('ChNSaWNoVGV4dERlc2NyaXB0aW9uEhYKBmZvcm1hdBgBIAEoCVIGZm9ybWF0');
+@$core.Deprecated('Use checkboxDescriptionDescriptor instead')
+const CheckboxDescription$json = const {
+  '1': 'CheckboxDescription',
+  '2': const [
+    const {'1': 'is_selected', '3': 1, '4': 1, '5': 8, '10': 'isSelected'},
+  ],
+};
+
+/// Descriptor for `CheckboxDescription`. Decode as a `google.protobuf.DescriptorProto`.
+final $typed_data.Uint8List checkboxDescriptionDescriptor = $convert.base64Decode('ChNDaGVja2JveERlc2NyaXB0aW9uEh8KC2lzX3NlbGVjdGVkGAEgASgIUgppc1NlbGVjdGVk');
+@$core.Deprecated('Use dateDescriptionDescriptor instead')
+const DateDescription$json = const {
+  '1': 'DateDescription',
+  '2': const [
+    const {'1': 'date_format', '3': 1, '4': 1, '5': 14, '6': '.DateFormat', '10': 'dateFormat'},
+    const {'1': 'time_format', '3': 2, '4': 1, '5': 14, '6': '.TimeFormat', '10': 'timeFormat'},
+  ],
+};
+
+/// Descriptor for `DateDescription`. Decode as a `google.protobuf.DescriptorProto`.
+final $typed_data.Uint8List dateDescriptionDescriptor = $convert.base64Decode('Cg9EYXRlRGVzY3JpcHRpb24SLAoLZGF0ZV9mb3JtYXQYASABKA4yCy5EYXRlRm9ybWF0UgpkYXRlRm9ybWF0EiwKC3RpbWVfZm9ybWF0GAIgASgOMgsuVGltZUZvcm1hdFIKdGltZUZvcm1hdA==');
+@$core.Deprecated('Use singleSelectDescriptionDescriptor instead')
+const SingleSelectDescription$json = const {
+  '1': 'SingleSelectDescription',
+  '2': const [
+    const {'1': 'options', '3': 1, '4': 3, '5': 11, '6': '.SelectOption', '10': 'options'},
+    const {'1': 'disable_color', '3': 2, '4': 1, '5': 8, '10': 'disableColor'},
+  ],
+};
+
+/// Descriptor for `SingleSelectDescription`. Decode as a `google.protobuf.DescriptorProto`.
+final $typed_data.Uint8List singleSelectDescriptionDescriptor = $convert.base64Decode('ChdTaW5nbGVTZWxlY3REZXNjcmlwdGlvbhInCgdvcHRpb25zGAEgAygLMg0uU2VsZWN0T3B0aW9uUgdvcHRpb25zEiMKDWRpc2FibGVfY29sb3IYAiABKAhSDGRpc2FibGVDb2xvcg==');
+@$core.Deprecated('Use multiSelectDescriptionDescriptor instead')
+const MultiSelectDescription$json = const {
+  '1': 'MultiSelectDescription',
+  '2': const [
+    const {'1': 'options', '3': 1, '4': 3, '5': 11, '6': '.SelectOption', '10': 'options'},
+    const {'1': 'disable_color', '3': 2, '4': 1, '5': 8, '10': 'disableColor'},
+  ],
+};
+
+/// Descriptor for `MultiSelectDescription`. Decode as a `google.protobuf.DescriptorProto`.
+final $typed_data.Uint8List multiSelectDescriptionDescriptor = $convert.base64Decode('ChZNdWx0aVNlbGVjdERlc2NyaXB0aW9uEicKB29wdGlvbnMYASADKAsyDS5TZWxlY3RPcHRpb25SB29wdGlvbnMSIwoNZGlzYWJsZV9jb2xvchgCIAEoCFIMZGlzYWJsZUNvbG9y');
+@$core.Deprecated('Use selectOptionDescriptor instead')
+const SelectOption$json = const {
+  '1': 'SelectOption',
+  '2': const [
+    const {'1': 'id', '3': 1, '4': 1, '5': 9, '10': 'id'},
+    const {'1': 'name', '3': 2, '4': 1, '5': 9, '10': 'name'},
+    const {'1': 'color', '3': 3, '4': 1, '5': 9, '10': 'color'},
+  ],
+};
+
+/// Descriptor for `SelectOption`. Decode as a `google.protobuf.DescriptorProto`.
+final $typed_data.Uint8List selectOptionDescriptor = $convert.base64Decode('CgxTZWxlY3RPcHRpb24SDgoCaWQYASABKAlSAmlkEhIKBG5hbWUYAiABKAlSBG5hbWUSFAoFY29sb3IYAyABKAlSBWNvbG9y');
+@$core.Deprecated('Use numberDescriptionDescriptor instead')
+const NumberDescription$json = const {
+  '1': 'NumberDescription',
+  '2': const [
+    const {'1': 'money', '3': 1, '4': 1, '5': 14, '6': '.MoneySymbol', '10': 'money'},
+    const {'1': 'scale', '3': 2, '4': 1, '5': 13, '10': 'scale'},
+    const {'1': 'symbol', '3': 3, '4': 1, '5': 9, '10': 'symbol'},
+    const {'1': 'sign_positive', '3': 4, '4': 1, '5': 8, '10': 'signPositive'},
+    const {'1': 'name', '3': 5, '4': 1, '5': 9, '10': 'name'},
+  ],
+};
+
+/// Descriptor for `NumberDescription`. Decode as a `google.protobuf.DescriptorProto`.
+final $typed_data.Uint8List numberDescriptionDescriptor = $convert.base64Decode('ChFOdW1iZXJEZXNjcmlwdGlvbhIiCgVtb25leRgBIAEoDjIMLk1vbmV5U3ltYm9sUgVtb25leRIUCgVzY2FsZRgCIAEoDVIFc2NhbGUSFgoGc3ltYm9sGAMgASgJUgZzeW1ib2wSIwoNc2lnbl9wb3NpdGl2ZRgEIAEoCFIMc2lnblBvc2l0aXZlEhIKBG5hbWUYBSABKAlSBG5hbWU=');

+ 9 - 0
frontend/app_flowy/packages/flowy_sdk/lib/protobuf/flowy-grid/type_options.pbserver.dart

@@ -0,0 +1,9 @@
+///
+//  Generated code. Do not modify.
+//  source: type_options.proto
+//
+// @dart = 2.12
+// ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields,deprecated_member_use_from_same_package
+
+export 'type_options.pb.dart';
+

+ 2 - 0
frontend/rust-lib/Cargo.lock

@@ -1059,8 +1059,10 @@ dependencies = [
  "flowy-database",
  "flowy-derive",
  "flowy-error",
+ "flowy-grid",
  "flowy-grid-data-model",
  "flowy-sync",
+ "flowy-test",
  "lazy_static",
  "lib-dispatch",
  "lib-infra",

+ 3 - 11
frontend/rust-lib/flowy-block/src/editor.rs

@@ -39,7 +39,7 @@ impl ClientTextBlockEditor {
         rev_web_socket: Arc<dyn RevisionWebSocket>,
         cloud_service: Arc<dyn RevisionCloudService>,
     ) -> FlowyResult<Arc<Self>> {
-        let document_info = rev_manager.load::<TextBlockInfoBuilder>(cloud_service).await?;
+        let document_info = rev_manager.load::<TextBlockInfoBuilder>(Some(cloud_service)).await?;
         let delta = document_info.delta()?;
         let rev_manager = Arc::new(rev_manager);
         let doc_id = doc_id.to_string();
@@ -191,17 +191,9 @@ fn spawn_edit_queue(
 
 #[cfg(feature = "flowy_unit_test")]
 impl ClientTextBlockEditor {
-    pub async fn doc_json(&self) -> FlowyResult<String> {
-        let (ret, rx) = oneshot::channel::<CollaborateResult<String>>();
-        let msg = EditorCommand::ReadDeltaStr { ret };
-        let _ = self.edit_cmd_tx.send(msg).await;
-        let s = rx.await.map_err(internal_error)??;
-        Ok(s)
-    }
-
-    pub async fn doc_delta(&self) -> FlowyResult<RichTextDelta> {
+    pub async fn text_block_delta(&self) -> FlowyResult<RichTextDelta> {
         let (ret, rx) = oneshot::channel::<CollaborateResult<RichTextDelta>>();
-        let msg = EditorCommand::ReadBlockDelta { ret };
+        let msg = EditorCommand::ReadDelta { ret };
         let _ = self.edit_cmd_tx.send(msg).await;
         let delta = rx.await.map_err(internal_error)??;
         Ok(delta)

+ 3 - 3
frontend/rust-lib/flowy-block/src/queue.rs

@@ -166,7 +166,7 @@ impl EditBlockQueue {
                 let data = self.document.read().await.delta_str();
                 let _ = ret.send(Ok(data));
             }
-            EditorCommand::ReadBlockDelta { ret } => {
+            EditorCommand::ReadDelta { ret } => {
                 let delta = self.document.read().await.delta().clone();
                 let _ = ret.send(Ok(delta));
             }
@@ -256,7 +256,7 @@ pub(crate) enum EditorCommand {
         ret: Ret<String>,
     },
     #[allow(dead_code)]
-    ReadBlockDelta {
+    ReadDelta {
         ret: Ret<RichTextDelta>,
     },
 }
@@ -277,7 +277,7 @@ impl std::fmt::Debug for EditorCommand {
             EditorCommand::Undo { .. } => "Undo",
             EditorCommand::Redo { .. } => "Redo",
             EditorCommand::ReadDeltaStr { .. } => "ReadDeltaStr",
-            EditorCommand::ReadBlockDelta { .. } => "ReadDocumentAsDelta",
+            EditorCommand::ReadDelta { .. } => "ReadDocumentAsDelta",
         };
         f.write_str(s)
     }

+ 2 - 2
frontend/rust-lib/flowy-block/tests/document/mod.rs

@@ -1,2 +1,2 @@
-mod document_test;
-mod edit_script;
+mod script;
+mod text_block_test;

+ 4 - 6
frontend/rust-lib/flowy-block/tests/document/edit_script.rs → frontend/rust-lib/flowy-block/tests/document/script.rs

@@ -17,16 +17,16 @@ pub enum EditorScript {
     AssertJson(&'static str),
 }
 
-pub struct EditorTest {
+pub struct TextBlockEditorTest {
     pub sdk: FlowySDKTest,
     pub editor: Arc<ClientTextBlockEditor>,
 }
 
-impl EditorTest {
+impl TextBlockEditorTest {
     pub async fn new() -> Self {
         let sdk = FlowySDKTest::default();
         let _ = sdk.init_user().await;
-        let test = ViewTest::new(&sdk).await;
+        let test = ViewTest::new_grid_view(&sdk).await;
         let editor = sdk.text_block_manager.open_block(&test.view.id).await.unwrap();
         Self { sdk, editor }
     }
@@ -41,8 +41,6 @@ impl EditorTest {
         let rev_manager = self.editor.rev_manager();
         let cache = rev_manager.revision_cache().await;
         let _user_id = self.sdk.user_session.user_id().unwrap();
-        // let ws_manager = self.sdk.ws_conn.clone();
-        // let token = self.sdk.user_session.token().unwrap();
 
         match script {
             EditorScript::InsertText(s, offset) => {
@@ -74,7 +72,7 @@ impl EditorTest {
             }
             EditorScript::AssertJson(expected) => {
                 let expected_delta: RichTextDelta = serde_json::from_str(expected).unwrap();
-                let delta = self.editor.doc_delta().await.unwrap();
+                let delta = self.editor.text_block_delta().await.unwrap();
                 if expected_delta != delta {
                     eprintln!("✅ expect: {}", expected,);
                     eprintln!("❌ receive: {}", delta.to_delta_str());

+ 17 - 17
frontend/rust-lib/flowy-block/tests/document/document_test.rs → frontend/rust-lib/flowy-block/tests/document/text_block_test.rs

@@ -1,9 +1,9 @@
-use crate::document::edit_script::{EditorScript::*, *};
+use crate::document::script::{EditorScript::*, *};
 use flowy_sync::disk::RevisionState;
 use lib_ot::core::{count_utf16_code_units, Interval};
 
 #[tokio::test]
-async fn document_sync_current_rev_id_check() {
+async fn text_block_sync_current_rev_id_check() {
     let scripts = vec![
         InsertText("1", 0),
         AssertCurrentRevId(1),
@@ -14,11 +14,11 @@ async fn document_sync_current_rev_id_check() {
         AssertNextSyncRevId(None),
         AssertJson(r#"[{"insert":"123\n"}]"#),
     ];
-    EditorTest::new().await.run_scripts(scripts).await;
+    TextBlockEditorTest::new().await.run_scripts(scripts).await;
 }
 
 #[tokio::test]
-async fn document_sync_state_check() {
+async fn text_block_sync_state_check() {
     let scripts = vec![
         InsertText("1", 0),
         InsertText("2", 1),
@@ -28,11 +28,11 @@ async fn document_sync_state_check() {
         AssertRevisionState(3, RevisionState::Ack),
         AssertJson(r#"[{"insert":"123\n"}]"#),
     ];
-    EditorTest::new().await.run_scripts(scripts).await;
+    TextBlockEditorTest::new().await.run_scripts(scripts).await;
 }
 
 #[tokio::test]
-async fn document_sync_insert_test() {
+async fn text_block_sync_insert_test() {
     let scripts = vec![
         InsertText("1", 0),
         InsertText("2", 1),
@@ -40,11 +40,11 @@ async fn document_sync_insert_test() {
         AssertJson(r#"[{"insert":"123\n"}]"#),
         AssertNextSyncRevId(None),
     ];
-    EditorTest::new().await.run_scripts(scripts).await;
+    TextBlockEditorTest::new().await.run_scripts(scripts).await;
 }
 
 #[tokio::test]
-async fn document_sync_insert_in_chinese() {
+async fn text_block_sync_insert_in_chinese() {
     let s = "好".to_owned();
     let offset = count_utf16_code_units(&s);
     let scripts = vec![
@@ -52,11 +52,11 @@ async fn document_sync_insert_in_chinese() {
         InsertText("好", offset),
         AssertJson(r#"[{"insert":"你好\n"}]"#),
     ];
-    EditorTest::new().await.run_scripts(scripts).await;
+    TextBlockEditorTest::new().await.run_scripts(scripts).await;
 }
 
 #[tokio::test]
-async fn document_sync_insert_with_emoji() {
+async fn text_block_sync_insert_with_emoji() {
     let s = "😁".to_owned();
     let offset = count_utf16_code_units(&s);
     let scripts = vec![
@@ -64,11 +64,11 @@ async fn document_sync_insert_with_emoji() {
         InsertText("☺️", offset),
         AssertJson(r#"[{"insert":"😁☺️\n"}]"#),
     ];
-    EditorTest::new().await.run_scripts(scripts).await;
+    TextBlockEditorTest::new().await.run_scripts(scripts).await;
 }
 
 #[tokio::test]
-async fn document_sync_delete_in_english() {
+async fn text_block_sync_delete_in_english() {
     let scripts = vec![
         InsertText("1", 0),
         InsertText("2", 1),
@@ -76,11 +76,11 @@ async fn document_sync_delete_in_english() {
         Delete(Interval::new(0, 2)),
         AssertJson(r#"[{"insert":"3\n"}]"#),
     ];
-    EditorTest::new().await.run_scripts(scripts).await;
+    TextBlockEditorTest::new().await.run_scripts(scripts).await;
 }
 
 #[tokio::test]
-async fn document_sync_delete_in_chinese() {
+async fn text_block_sync_delete_in_chinese() {
     let s = "好".to_owned();
     let offset = count_utf16_code_units(&s);
     let scripts = vec![
@@ -89,11 +89,11 @@ async fn document_sync_delete_in_chinese() {
         Delete(Interval::new(0, offset)),
         AssertJson(r#"[{"insert":"好\n"}]"#),
     ];
-    EditorTest::new().await.run_scripts(scripts).await;
+    TextBlockEditorTest::new().await.run_scripts(scripts).await;
 }
 
 #[tokio::test]
-async fn document_sync_replace_test() {
+async fn text_block_sync_replace_test() {
     let scripts = vec![
         InsertText("1", 0),
         InsertText("2", 1),
@@ -101,5 +101,5 @@ async fn document_sync_replace_test() {
         Replace(Interval::new(0, 3), "abc"),
         AssertJson(r#"[{"insert":"abc\n"}]"#),
     ];
-    EditorTest::new().await.run_scripts(scripts).await;
+    TextBlockEditorTest::new().await.run_scripts(scripts).await;
 }

+ 1 - 1
frontend/rust-lib/flowy-folder/src/services/folder_editor.rs

@@ -38,7 +38,7 @@ impl ClientFolderEditor {
         let cloud = Arc::new(FolderRevisionCloudService {
             token: token.to_string(),
         });
-        let folder = Arc::new(RwLock::new(rev_manager.load::<FolderPadBuilder>(cloud).await?));
+        let folder = Arc::new(RwLock::new(rev_manager.load::<FolderPadBuilder>(Some(cloud)).await?));
         let rev_manager = Arc::new(rev_manager);
         let ws_manager = make_folder_ws_manager(
             user_id,

+ 12 - 5
frontend/rust-lib/flowy-folder/tests/workspace/folder_test.rs

@@ -1,6 +1,7 @@
 use crate::script::{invalid_workspace_name_test_case, FolderScript::*, FolderTest};
 
 use flowy_folder::entities::workspace::CreateWorkspacePayload;
+use flowy_folder_data_model::entities::view::ViewDataType;
 use flowy_sync::disk::RevisionState;
 use flowy_test::{event_builder::*, FlowySDKTest};
 
@@ -136,10 +137,12 @@ async fn app_create_with_view() {
         CreateView {
             name: "View A".to_owned(),
             desc: "View A description".to_owned(),
+            data_type: ViewDataType::TextBlock,
         },
         CreateView {
-            name: "View B".to_owned(),
-            desc: "View B description".to_owned(),
+            name: "Grid".to_owned(),
+            desc: "Grid description".to_owned(),
+            data_type: ViewDataType::Grid,
         },
         ReadApp(app.id),
     ])
@@ -148,7 +151,7 @@ async fn app_create_with_view() {
     app = test.app.clone();
     assert_eq!(app.belongings.len(), 3);
     assert_eq!(app.belongings[1].name, "View A");
-    assert_eq!(app.belongings[2].name, "View B")
+    assert_eq!(app.belongings[2].name, "Grid")
 }
 
 #[tokio::test]
@@ -198,10 +201,12 @@ async fn view_delete_all() {
         CreateView {
             name: "View A".to_owned(),
             desc: "View A description".to_owned(),
+            data_type: ViewDataType::TextBlock,
         },
         CreateView {
-            name: "View B".to_owned(),
-            desc: "View B description".to_owned(),
+            name: "Grid".to_owned(),
+            desc: "Grid description".to_owned(),
+            data_type: ViewDataType::Grid,
         },
         ReadApp(app.id.clone()),
     ])
@@ -229,6 +234,7 @@ async fn view_delete_all_permanent() {
         CreateView {
             name: "View A".to_owned(),
             desc: "View A description".to_owned(),
+            data_type: ViewDataType::TextBlock,
         },
         ReadApp(app.id.clone()),
     ])
@@ -327,6 +333,7 @@ async fn folder_sync_revision_with_new_view() {
         CreateView {
             name: view_name.clone(),
             desc: view_desc.clone(),
+            data_type: ViewDataType::TextBlock,
         },
         AssertCurrentRevId(3),
         AssertNextSyncRevId(Some(3)),

+ 0 - 212
frontend/rust-lib/flowy-folder/tests/workspace/helper.rs

@@ -1,212 +0,0 @@
-use flowy_collaboration::entities::text_block_info::TextBlockInfo;
-use flowy_folder::event_map::FolderEvent::*;
-use flowy_folder_data_model::entities::view::{RepeatedViewId, ViewId};
-use flowy_folder_data_model::entities::workspace::WorkspaceId;
-use flowy_folder_data_model::entities::{
-    app::{App, AppId, CreateAppPayload, UpdateAppPayload},
-    trash::{RepeatedTrash, TrashId, TrashType},
-    view::{CreateViewPayload, UpdateViewPayload, View, ViewDataType},
-    workspace::{CreateWorkspacePayload, RepeatedWorkspace, Workspace},
-};
-use flowy_test::{event_builder::*, FlowySDKTest};
-
-pub async fn create_workspace(sdk: &FlowySDKTest, name: &str, desc: &str) -> Workspace {
-    let request = CreateWorkspacePayload {
-        name: name.to_owned(),
-        desc: desc.to_owned(),
-    };
-
-    let workspace = FolderEventBuilder::new(sdk.clone())
-        .event(CreateWorkspace)
-        .payload(request)
-        .async_send()
-        .await
-        .parse::<Workspace>();
-    workspace
-}
-
-pub async fn read_workspace(sdk: &FlowySDKTest, workspace_id: Option<String>) -> Vec<Workspace> {
-    let request = WorkspaceId { value: workspace_id };
-    let repeated_workspace = FolderEventBuilder::new(sdk.clone())
-        .event(ReadWorkspaces)
-        .payload(request.clone())
-        .async_send()
-        .await
-        .parse::<RepeatedWorkspace>();
-
-    let workspaces;
-    if let Some(workspace_id) = &request.value {
-        workspaces = repeated_workspace
-            .into_inner()
-            .into_iter()
-            .filter(|workspace| &workspace.id == workspace_id)
-            .collect::<Vec<Workspace>>();
-        debug_assert_eq!(workspaces.len(), 1);
-    } else {
-        workspaces = repeated_workspace.items;
-    }
-
-    workspaces
-}
-
-pub async fn create_app(sdk: &FlowySDKTest, workspace_id: &str, name: &str, desc: &str) -> App {
-    let create_app_request = CreateAppPayload {
-        workspace_id: workspace_id.to_owned(),
-        name: name.to_string(),
-        desc: desc.to_string(),
-        color_style: Default::default(),
-    };
-
-    let app = FolderEventBuilder::new(sdk.clone())
-        .event(CreateApp)
-        .payload(create_app_request)
-        .async_send()
-        .await
-        .parse::<App>();
-    app
-}
-
-pub async fn read_app(sdk: &FlowySDKTest, app_id: &str) -> App {
-    let request = AppId {
-        value: app_id.to_owned(),
-    };
-
-    let app = FolderEventBuilder::new(sdk.clone())
-        .event(ReadApp)
-        .payload(request)
-        .async_send()
-        .await
-        .parse::<App>();
-
-    app
-}
-
-pub async fn update_app(sdk: &FlowySDKTest, app_id: &str, name: Option<String>, desc: Option<String>) {
-    let request = UpdateAppPayload {
-        app_id: app_id.to_string(),
-        name,
-        desc,
-        color_style: None,
-        is_trash: None,
-    };
-
-    FolderEventBuilder::new(sdk.clone())
-        .event(UpdateApp)
-        .payload(request)
-        .async_send()
-        .await;
-}
-
-pub async fn delete_app(sdk: &FlowySDKTest, app_id: &str) {
-    let request = AppId {
-        value: app_id.to_string(),
-    };
-
-    FolderEventBuilder::new(sdk.clone())
-        .event(DeleteApp)
-        .payload(request)
-        .async_send()
-        .await;
-}
-
-pub async fn create_view(sdk: &FlowySDKTest, app_id: &str, name: &str, desc: &str, data_type: ViewDataType) -> View {
-    let request = CreateViewPayload {
-        belong_to_id: app_id.to_string(),
-        name: name.to_string(),
-        desc: desc.to_string(),
-        thumbnail: None,
-        data_type,
-        ext_data: "".to_string(),
-        plugin_type: 0,
-    };
-    let view = FolderEventBuilder::new(sdk.clone())
-        .event(CreateView)
-        .payload(request)
-        .async_send()
-        .await
-        .parse::<View>();
-    view
-}
-
-pub async fn read_view(sdk: &FlowySDKTest, view_id: &str) -> View {
-    let view_id: ViewId = view_id.into();
-    FolderEventBuilder::new(sdk.clone())
-        .event(ReadView)
-        .payload(view_id)
-        .async_send()
-        .await
-        .parse::<View>()
-}
-
-pub async fn update_view(sdk: &FlowySDKTest, view_id: &str, name: Option<String>, desc: Option<String>) {
-    let request = UpdateViewPayload {
-        view_id: view_id.to_string(),
-        name,
-        desc,
-        thumbnail: None,
-    };
-    FolderEventBuilder::new(sdk.clone())
-        .event(UpdateView)
-        .payload(request)
-        .async_send()
-        .await;
-}
-
-pub async fn delete_view(sdk: &FlowySDKTest, view_ids: Vec<String>) {
-    let request = RepeatedViewId { items: view_ids };
-    FolderEventBuilder::new(sdk.clone())
-        .event(DeleteView)
-        .payload(request)
-        .async_send()
-        .await;
-}
-
-#[allow(dead_code)]
-pub async fn set_latest_view(sdk: &FlowySDKTest, view_id: &str) -> TextBlockInfo {
-    let view_id: ViewId = view_id.into();
-    FolderEventBuilder::new(sdk.clone())
-        .event(SetLatestView)
-        .payload(view_id)
-        .async_send()
-        .await
-        .parse::<TextBlockInfo>()
-}
-
-pub async fn read_trash(sdk: &FlowySDKTest) -> RepeatedTrash {
-    FolderEventBuilder::new(sdk.clone())
-        .event(ReadTrash)
-        .async_send()
-        .await
-        .parse::<RepeatedTrash>()
-}
-
-pub async fn restore_app_from_trash(sdk: &FlowySDKTest, app_id: &str) {
-    let id = TrashId {
-        id: app_id.to_owned(),
-        ty: TrashType::TrashApp,
-    };
-    FolderEventBuilder::new(sdk.clone())
-        .event(PutbackTrash)
-        .payload(id)
-        .async_send()
-        .await;
-}
-
-pub async fn restore_view_from_trash(sdk: &FlowySDKTest, view_id: &str) {
-    let id = TrashId {
-        id: view_id.to_owned(),
-        ty: TrashType::TrashView,
-    };
-    FolderEventBuilder::new(sdk.clone())
-        .event(PutbackTrash)
-        .payload(id)
-        .async_send()
-        .await;
-}
-
-pub async fn delete_all_trash(sdk: &FlowySDKTest) {
-    FolderEventBuilder::new(sdk.clone())
-        .event(DeleteAllTrash)
-        .async_send()
-        .await;
-}

+ 0 - 1
frontend/rust-lib/flowy-folder/tests/workspace/main.rs

@@ -1,3 +1,2 @@
 mod folder_test;
-mod helper;
 mod script;

+ 239 - 11
frontend/rust-lib/flowy-folder/tests/workspace/script.rs

@@ -1,39 +1,63 @@
-use crate::helper::*;
-
+use flowy_collaboration::entities::text_block_info::TextBlockInfo;
+use flowy_folder::event_map::FolderEvent::*;
 use flowy_folder::{errors::ErrorCode, services::folder_editor::ClientFolderEditor};
+use flowy_folder_data_model::entities::view::{RepeatedViewId, ViewId};
+use flowy_folder_data_model::entities::workspace::WorkspaceId;
 use flowy_folder_data_model::entities::{
     app::{App, RepeatedApp},
     trash::Trash,
     view::{RepeatedView, View, ViewDataType},
     workspace::Workspace,
 };
+use flowy_folder_data_model::entities::{
+    app::{AppId, CreateAppPayload, UpdateAppPayload},
+    trash::{RepeatedTrash, TrashId, TrashType},
+    view::{CreateViewPayload, UpdateViewPayload},
+    workspace::{CreateWorkspacePayload, RepeatedWorkspace},
+};
 use flowy_sync::disk::RevisionState;
 use flowy_sync::REVISION_WRITE_INTERVAL_IN_MILLIS;
-use flowy_test::FlowySDKTest;
+use flowy_test::{event_builder::*, FlowySDKTest};
 use std::{sync::Arc, time::Duration};
 use tokio::time::sleep;
 
 pub enum FolderScript {
     // Workspace
     ReadAllWorkspaces,
-    CreateWorkspace { name: String, desc: String },
+    CreateWorkspace {
+        name: String,
+        desc: String,
+    },
     AssertWorkspaceJson(String),
     AssertWorkspace(Workspace),
     ReadWorkspace(Option<String>),
 
     // App
-    CreateApp { name: String, desc: String },
+    CreateApp {
+        name: String,
+        desc: String,
+    },
     AssertAppJson(String),
     AssertApp(App),
     ReadApp(String),
-    UpdateApp { name: Option<String>, desc: Option<String> },
+    UpdateApp {
+        name: Option<String>,
+        desc: Option<String>,
+    },
     DeleteApp,
 
     // View
-    CreateView { name: String, desc: String },
+    CreateView {
+        name: String,
+        desc: String,
+        data_type: ViewDataType,
+    },
     AssertView(View),
     ReadView(String),
-    UpdateView { name: Option<String>, desc: Option<String> },
+    UpdateView {
+        name: Option<String>,
+        desc: Option<String>,
+    },
     DeleteView,
     DeleteViews(Vec<String>),
 
@@ -46,7 +70,10 @@ pub enum FolderScript {
     // Sync
     AssertCurrentRevId(i64),
     AssertNextSyncRevId(Option<i64>),
-    AssertRevisionState { rev_id: i64, state: RevisionState },
+    AssertRevisionState {
+        rev_id: i64,
+        state: RevisionState,
+    },
 }
 
 pub struct FolderTest {
@@ -148,8 +175,8 @@ impl FolderTest {
                 delete_app(sdk, &self.app.id).await;
             }
 
-            FolderScript::CreateView { name, desc } => {
-                let view = create_view(sdk, &self.app.id, &name, &desc, ViewDataType::TextBlock).await;
+            FolderScript::CreateView { name, desc, data_type } => {
+                let view = create_view(sdk, &self.app.id, &name, &desc, data_type).await;
                 self.view = view;
             }
             FolderScript::AssertView(view) => {
@@ -216,3 +243,204 @@ pub fn invalid_workspace_name_test_case() -> Vec<(String, ErrorCode)> {
         ("1234".repeat(100), ErrorCode::WorkspaceNameTooLong),
     ]
 }
+
+pub async fn create_workspace(sdk: &FlowySDKTest, name: &str, desc: &str) -> Workspace {
+    let request = CreateWorkspacePayload {
+        name: name.to_owned(),
+        desc: desc.to_owned(),
+    };
+
+    let workspace = FolderEventBuilder::new(sdk.clone())
+        .event(CreateWorkspace)
+        .payload(request)
+        .async_send()
+        .await
+        .parse::<Workspace>();
+    workspace
+}
+
+pub async fn read_workspace(sdk: &FlowySDKTest, workspace_id: Option<String>) -> Vec<Workspace> {
+    let request = WorkspaceId { value: workspace_id };
+    let repeated_workspace = FolderEventBuilder::new(sdk.clone())
+        .event(ReadWorkspaces)
+        .payload(request.clone())
+        .async_send()
+        .await
+        .parse::<RepeatedWorkspace>();
+
+    let workspaces;
+    if let Some(workspace_id) = &request.value {
+        workspaces = repeated_workspace
+            .into_inner()
+            .into_iter()
+            .filter(|workspace| &workspace.id == workspace_id)
+            .collect::<Vec<Workspace>>();
+        debug_assert_eq!(workspaces.len(), 1);
+    } else {
+        workspaces = repeated_workspace.items;
+    }
+
+    workspaces
+}
+
+pub async fn create_app(sdk: &FlowySDKTest, workspace_id: &str, name: &str, desc: &str) -> App {
+    let create_app_request = CreateAppPayload {
+        workspace_id: workspace_id.to_owned(),
+        name: name.to_string(),
+        desc: desc.to_string(),
+        color_style: Default::default(),
+    };
+
+    let app = FolderEventBuilder::new(sdk.clone())
+        .event(CreateApp)
+        .payload(create_app_request)
+        .async_send()
+        .await
+        .parse::<App>();
+    app
+}
+
+pub async fn read_app(sdk: &FlowySDKTest, app_id: &str) -> App {
+    let request = AppId {
+        value: app_id.to_owned(),
+    };
+
+    let app = FolderEventBuilder::new(sdk.clone())
+        .event(ReadApp)
+        .payload(request)
+        .async_send()
+        .await
+        .parse::<App>();
+
+    app
+}
+
+pub async fn update_app(sdk: &FlowySDKTest, app_id: &str, name: Option<String>, desc: Option<String>) {
+    let request = UpdateAppPayload {
+        app_id: app_id.to_string(),
+        name,
+        desc,
+        color_style: None,
+        is_trash: None,
+    };
+
+    FolderEventBuilder::new(sdk.clone())
+        .event(UpdateApp)
+        .payload(request)
+        .async_send()
+        .await;
+}
+
+pub async fn delete_app(sdk: &FlowySDKTest, app_id: &str) {
+    let request = AppId {
+        value: app_id.to_string(),
+    };
+
+    FolderEventBuilder::new(sdk.clone())
+        .event(DeleteApp)
+        .payload(request)
+        .async_send()
+        .await;
+}
+
+pub async fn create_view(sdk: &FlowySDKTest, app_id: &str, name: &str, desc: &str, data_type: ViewDataType) -> View {
+    let request = CreateViewPayload {
+        belong_to_id: app_id.to_string(),
+        name: name.to_string(),
+        desc: desc.to_string(),
+        thumbnail: None,
+        data_type,
+        ext_data: "".to_string(),
+        plugin_type: 0,
+    };
+    let view = FolderEventBuilder::new(sdk.clone())
+        .event(CreateView)
+        .payload(request)
+        .async_send()
+        .await
+        .parse::<View>();
+    view
+}
+
+pub async fn read_view(sdk: &FlowySDKTest, view_id: &str) -> View {
+    let view_id: ViewId = view_id.into();
+    FolderEventBuilder::new(sdk.clone())
+        .event(ReadView)
+        .payload(view_id)
+        .async_send()
+        .await
+        .parse::<View>()
+}
+
+pub async fn update_view(sdk: &FlowySDKTest, view_id: &str, name: Option<String>, desc: Option<String>) {
+    let request = UpdateViewPayload {
+        view_id: view_id.to_string(),
+        name,
+        desc,
+        thumbnail: None,
+    };
+    FolderEventBuilder::new(sdk.clone())
+        .event(UpdateView)
+        .payload(request)
+        .async_send()
+        .await;
+}
+
+pub async fn delete_view(sdk: &FlowySDKTest, view_ids: Vec<String>) {
+    let request = RepeatedViewId { items: view_ids };
+    FolderEventBuilder::new(sdk.clone())
+        .event(DeleteView)
+        .payload(request)
+        .async_send()
+        .await;
+}
+
+#[allow(dead_code)]
+pub async fn set_latest_view(sdk: &FlowySDKTest, view_id: &str) -> TextBlockInfo {
+    let view_id: ViewId = view_id.into();
+    FolderEventBuilder::new(sdk.clone())
+        .event(SetLatestView)
+        .payload(view_id)
+        .async_send()
+        .await
+        .parse::<TextBlockInfo>()
+}
+
+pub async fn read_trash(sdk: &FlowySDKTest) -> RepeatedTrash {
+    FolderEventBuilder::new(sdk.clone())
+        .event(ReadTrash)
+        .async_send()
+        .await
+        .parse::<RepeatedTrash>()
+}
+
+pub async fn restore_app_from_trash(sdk: &FlowySDKTest, app_id: &str) {
+    let id = TrashId {
+        id: app_id.to_owned(),
+        ty: TrashType::TrashApp,
+    };
+    FolderEventBuilder::new(sdk.clone())
+        .event(PutbackTrash)
+        .payload(id)
+        .async_send()
+        .await;
+}
+
+pub async fn restore_view_from_trash(sdk: &FlowySDKTest, view_id: &str) {
+    let id = TrashId {
+        id: view_id.to_owned(),
+        ty: TrashType::TrashView,
+    };
+    FolderEventBuilder::new(sdk.clone())
+        .event(PutbackTrash)
+        .payload(id)
+        .async_send()
+        .await;
+}
+
+pub async fn delete_all_trash(sdk: &FlowySDKTest) {
+    FolderEventBuilder::new(sdk.clone())
+        .event(DeleteAllTrash)
+        .async_send()
+        .await;
+}

+ 6 - 1
frontend/rust-lib/flowy-grid/Cargo.toml

@@ -34,10 +34,15 @@ tokio = {version = "1", features = ["sync"]}
 rayon = "1.5"
 parking_lot = "0.11"
 
+[dev-dependencies]
+flowy-test = { path = "../flowy-test" }
+flowy-grid = { path = "../flowy-grid", features = ["flowy_unit_test"]}
+
 [build-dependencies]
 lib-infra = { path = "../../../shared-lib/lib-infra", features = ["protobuf_file_gen", "proto_gen"] }
 
 
 [features]
 default = []
-dart = ["lib-infra/dart"]
+dart = ["lib-infra/dart"]
+flowy_unit_test = ["flowy-sync/flowy_unit_test"]

+ 1 - 1
frontend/rust-lib/flowy-grid/Flowy.toml

@@ -1,3 +1,3 @@
 
-proto_crates = ["src/event_map.rs", "src/services/cell_data.rs"]
+proto_crates = ["src/event_map.rs", "src/services/field/type_options.rs"]
 event_files = ["src/event_map.rs"]

+ 1 - 0
frontend/rust-lib/flowy-grid/src/lib.rs

@@ -7,3 +7,4 @@ pub mod manager;
 
 mod protobuf;
 pub mod services;
+pub mod util;

+ 12 - 23
frontend/rust-lib/flowy-grid/src/macros.rs

@@ -1,29 +1,17 @@
 #[macro_export]
-macro_rules! impl_any_data {
+macro_rules! impl_from_and_to_type_option {
     ($target: ident, $field_type:expr) => {
-        impl_field_type_data_from_field!($target);
-        impl_field_type_data_from_field_type_option!($target);
-        impl_type_option_from_field_data!($target, $field_type);
+        impl_from_field_type_option!($target);
+        impl_to_field_type_option!($target, $field_type);
     };
 }
 
 #[macro_export]
-macro_rules! impl_field_type_data_from_field {
+macro_rules! impl_from_field_type_option {
     ($target: ident) => {
         impl std::convert::From<&Field> for $target {
             fn from(field: &Field) -> $target {
-                $target::from(&field.type_options)
-            }
-        }
-    };
-}
-
-#[macro_export]
-macro_rules! impl_field_type_data_from_field_type_option {
-    ($target: ident) => {
-        impl std::convert::From<&AnyData> for $target {
-            fn from(any_data: &AnyData) -> $target {
-                match $target::try_from(Bytes::from(any_data.value.clone())) {
+                match $target::try_from(Bytes::from(field.type_options.value.clone())) {
                     Ok(obj) => obj,
                     Err(err) => {
                         tracing::error!("{} convert from any data failed, {:?}", stringify!($target), err);
@@ -36,26 +24,27 @@ macro_rules! impl_field_type_data_from_field_type_option {
 }
 
 #[macro_export]
-macro_rules! impl_type_option_from_field_data {
+macro_rules! impl_to_field_type_option {
     ($target: ident, $field_type:expr) => {
         impl $target {
-            pub fn field_type() -> FieldType {
+            pub fn field_type(&self) -> FieldType {
                 $field_type
             }
         }
 
         impl std::convert::From<$target> for AnyData {
-            fn from(field_data: $target) -> Self {
-                match field_data.try_into() {
+            fn from(field_description: $target) -> Self {
+                let field_type = field_description.field_type();
+                match field_description.try_into() {
                     Ok(bytes) => {
                         let bytes: Bytes = bytes;
-                        AnyData::from_bytes(&$target::field_type(), bytes)
+                        AnyData::from_bytes(field_type, bytes)
                     }
                     Err(e) => {
                         tracing::error!("Field type data convert to AnyData fail, error: {:?}", e);
                         // it's impossible to fail when unwrapping the default field type data
                         let default_bytes: Bytes = $target::default().try_into().unwrap();
-                        AnyData::from_bytes(&$target::field_type(), default_bytes)
+                        AnyData::from_bytes(field_type, default_bytes)
                     }
                 }
             }

+ 1 - 1
frontend/rust-lib/flowy-grid/src/manager.rs

@@ -113,7 +113,7 @@ impl GridManager {
         Ok(grid_editor)
     }
 
-    fn make_grid_rev_manager(&self, grid_id: &str, pool: Arc<ConnectionPool>) -> FlowyResult<RevisionManager> {
+    pub fn make_grid_rev_manager(&self, grid_id: &str, pool: Arc<ConnectionPool>) -> FlowyResult<RevisionManager> {
         let user_id = self.grid_user.user_id()?;
 
         let disk_cache = Arc::new(SQLiteGridRevisionPersistence::new(&user_id, pool));

+ 2 - 2
frontend/rust-lib/flowy-grid/src/protobuf/model/mod.rs

@@ -1,8 +1,8 @@
 #![cfg_attr(rustfmt, rustfmt::skip)]
 // Auto-generated, do not edit
 
-mod cell_data;
-pub use cell_data::*;
+mod type_options;
+pub use type_options::*;
 
 mod event_map;
 pub use event_map::*;

+ 84 - 83
frontend/rust-lib/flowy-grid/src/protobuf/model/cell_data.rs → frontend/rust-lib/flowy-grid/src/protobuf/model/type_options.rs

@@ -17,7 +17,7 @@
 #![allow(trivial_casts)]
 #![allow(unused_imports)]
 #![allow(unused_results)]
-//! Generated file from `cell_data.proto`
+//! Generated file from `type_options.proto`
 
 /// Generated files are compatible only with the same version
 /// of protobuf runtime.
@@ -514,7 +514,7 @@ impl ::protobuf::reflect::ProtobufValue for DateDescription {
 }
 
 #[derive(PartialEq,Clone,Default)]
-pub struct SingleSelect {
+pub struct SingleSelectDescription {
     // message fields
     pub options: ::protobuf::RepeatedField<SelectOption>,
     pub disable_color: bool,
@@ -523,14 +523,14 @@ pub struct SingleSelect {
     pub cached_size: ::protobuf::CachedSize,
 }
 
-impl<'a> ::std::default::Default for &'a SingleSelect {
-    fn default() -> &'a SingleSelect {
-        <SingleSelect as ::protobuf::Message>::default_instance()
+impl<'a> ::std::default::Default for &'a SingleSelectDescription {
+    fn default() -> &'a SingleSelectDescription {
+        <SingleSelectDescription as ::protobuf::Message>::default_instance()
     }
 }
 
-impl SingleSelect {
-    pub fn new() -> SingleSelect {
+impl SingleSelectDescription {
+    pub fn new() -> SingleSelectDescription {
         ::std::default::Default::default()
     }
 
@@ -575,7 +575,7 @@ impl SingleSelect {
     }
 }
 
-impl ::protobuf::Message for SingleSelect {
+impl ::protobuf::Message for SingleSelectDescription {
     fn is_initialized(&self) -> bool {
         for v in &self.options {
             if !v.is_initialized() {
@@ -662,8 +662,8 @@ impl ::protobuf::Message for SingleSelect {
         Self::descriptor_static()
     }
 
-    fn new() -> SingleSelect {
-        SingleSelect::new()
+    fn new() -> SingleSelectDescription {
+        SingleSelectDescription::new()
     }
 
     fn descriptor_static() -> &'static ::protobuf::reflect::MessageDescriptor {
@@ -672,29 +672,29 @@ impl ::protobuf::Message for SingleSelect {
             let mut fields = ::std::vec::Vec::new();
             fields.push(::protobuf::reflect::accessor::make_repeated_field_accessor::<_, ::protobuf::types::ProtobufTypeMessage<SelectOption>>(
                 "options",
-                |m: &SingleSelect| { &m.options },
-                |m: &mut SingleSelect| { &mut m.options },
+                |m: &SingleSelectDescription| { &m.options },
+                |m: &mut SingleSelectDescription| { &mut m.options },
             ));
             fields.push(::protobuf::reflect::accessor::make_simple_field_accessor::<_, ::protobuf::types::ProtobufTypeBool>(
                 "disable_color",
-                |m: &SingleSelect| { &m.disable_color },
-                |m: &mut SingleSelect| { &mut m.disable_color },
+                |m: &SingleSelectDescription| { &m.disable_color },
+                |m: &mut SingleSelectDescription| { &mut m.disable_color },
             ));
-            ::protobuf::reflect::MessageDescriptor::new_pb_name::<SingleSelect>(
-                "SingleSelect",
+            ::protobuf::reflect::MessageDescriptor::new_pb_name::<SingleSelectDescription>(
+                "SingleSelectDescription",
                 fields,
                 file_descriptor_proto()
             )
         })
     }
 
-    fn default_instance() -> &'static SingleSelect {
-        static instance: ::protobuf::rt::LazyV2<SingleSelect> = ::protobuf::rt::LazyV2::INIT;
-        instance.get(SingleSelect::new)
+    fn default_instance() -> &'static SingleSelectDescription {
+        static instance: ::protobuf::rt::LazyV2<SingleSelectDescription> = ::protobuf::rt::LazyV2::INIT;
+        instance.get(SingleSelectDescription::new)
     }
 }
 
-impl ::protobuf::Clear for SingleSelect {
+impl ::protobuf::Clear for SingleSelectDescription {
     fn clear(&mut self) {
         self.options.clear();
         self.disable_color = false;
@@ -702,20 +702,20 @@ impl ::protobuf::Clear for SingleSelect {
     }
 }
 
-impl ::std::fmt::Debug for SingleSelect {
+impl ::std::fmt::Debug for SingleSelectDescription {
     fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
         ::protobuf::text_format::fmt(self, f)
     }
 }
 
-impl ::protobuf::reflect::ProtobufValue for SingleSelect {
+impl ::protobuf::reflect::ProtobufValue for SingleSelectDescription {
     fn as_ref(&self) -> ::protobuf::reflect::ReflectValueRef {
         ::protobuf::reflect::ReflectValueRef::Message(self)
     }
 }
 
 #[derive(PartialEq,Clone,Default)]
-pub struct MultiSelect {
+pub struct MultiSelectDescription {
     // message fields
     pub options: ::protobuf::RepeatedField<SelectOption>,
     pub disable_color: bool,
@@ -724,14 +724,14 @@ pub struct MultiSelect {
     pub cached_size: ::protobuf::CachedSize,
 }
 
-impl<'a> ::std::default::Default for &'a MultiSelect {
-    fn default() -> &'a MultiSelect {
-        <MultiSelect as ::protobuf::Message>::default_instance()
+impl<'a> ::std::default::Default for &'a MultiSelectDescription {
+    fn default() -> &'a MultiSelectDescription {
+        <MultiSelectDescription as ::protobuf::Message>::default_instance()
     }
 }
 
-impl MultiSelect {
-    pub fn new() -> MultiSelect {
+impl MultiSelectDescription {
+    pub fn new() -> MultiSelectDescription {
         ::std::default::Default::default()
     }
 
@@ -776,7 +776,7 @@ impl MultiSelect {
     }
 }
 
-impl ::protobuf::Message for MultiSelect {
+impl ::protobuf::Message for MultiSelectDescription {
     fn is_initialized(&self) -> bool {
         for v in &self.options {
             if !v.is_initialized() {
@@ -863,8 +863,8 @@ impl ::protobuf::Message for MultiSelect {
         Self::descriptor_static()
     }
 
-    fn new() -> MultiSelect {
-        MultiSelect::new()
+    fn new() -> MultiSelectDescription {
+        MultiSelectDescription::new()
     }
 
     fn descriptor_static() -> &'static ::protobuf::reflect::MessageDescriptor {
@@ -873,29 +873,29 @@ impl ::protobuf::Message for MultiSelect {
             let mut fields = ::std::vec::Vec::new();
             fields.push(::protobuf::reflect::accessor::make_repeated_field_accessor::<_, ::protobuf::types::ProtobufTypeMessage<SelectOption>>(
                 "options",
-                |m: &MultiSelect| { &m.options },
-                |m: &mut MultiSelect| { &mut m.options },
+                |m: &MultiSelectDescription| { &m.options },
+                |m: &mut MultiSelectDescription| { &mut m.options },
             ));
             fields.push(::protobuf::reflect::accessor::make_simple_field_accessor::<_, ::protobuf::types::ProtobufTypeBool>(
                 "disable_color",
-                |m: &MultiSelect| { &m.disable_color },
-                |m: &mut MultiSelect| { &mut m.disable_color },
+                |m: &MultiSelectDescription| { &m.disable_color },
+                |m: &mut MultiSelectDescription| { &mut m.disable_color },
             ));
-            ::protobuf::reflect::MessageDescriptor::new_pb_name::<MultiSelect>(
-                "MultiSelect",
+            ::protobuf::reflect::MessageDescriptor::new_pb_name::<MultiSelectDescription>(
+                "MultiSelectDescription",
                 fields,
                 file_descriptor_proto()
             )
         })
     }
 
-    fn default_instance() -> &'static MultiSelect {
-        static instance: ::protobuf::rt::LazyV2<MultiSelect> = ::protobuf::rt::LazyV2::INIT;
-        instance.get(MultiSelect::new)
+    fn default_instance() -> &'static MultiSelectDescription {
+        static instance: ::protobuf::rt::LazyV2<MultiSelectDescription> = ::protobuf::rt::LazyV2::INIT;
+        instance.get(MultiSelectDescription::new)
     }
 }
 
-impl ::protobuf::Clear for MultiSelect {
+impl ::protobuf::Clear for MultiSelectDescription {
     fn clear(&mut self) {
         self.options.clear();
         self.disable_color = false;
@@ -903,13 +903,13 @@ impl ::protobuf::Clear for MultiSelect {
     }
 }
 
-impl ::std::fmt::Debug for MultiSelect {
+impl ::std::fmt::Debug for MultiSelectDescription {
     fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
         ::protobuf::text_format::fmt(self, f)
     }
 }
 
-impl ::protobuf::reflect::ProtobufValue for MultiSelect {
+impl ::protobuf::reflect::ProtobufValue for MultiSelectDescription {
     fn as_ref(&self) -> ::protobuf::reflect::ReflectValueRef {
         ::protobuf::reflect::ReflectValueRef::Message(self)
     }
@@ -1161,7 +1161,7 @@ impl ::protobuf::reflect::ProtobufValue for SelectOption {
 #[derive(PartialEq,Clone,Default)]
 pub struct NumberDescription {
     // message fields
-    pub money: FlowyMoney,
+    pub money: MoneySymbol,
     pub scale: u32,
     pub symbol: ::std::string::String,
     pub sign_positive: bool,
@@ -1182,18 +1182,18 @@ impl NumberDescription {
         ::std::default::Default::default()
     }
 
-    // .FlowyMoney money = 1;
+    // .MoneySymbol money = 1;
 
 
-    pub fn get_money(&self) -> FlowyMoney {
+    pub fn get_money(&self) -> MoneySymbol {
         self.money
     }
     pub fn clear_money(&mut self) {
-        self.money = FlowyMoney::CNY;
+        self.money = MoneySymbol::CNY;
     }
 
     // Param is passed by value, moved
-    pub fn set_money(&mut self, v: FlowyMoney) {
+    pub fn set_money(&mut self, v: MoneySymbol) {
         self.money = v;
     }
 
@@ -1324,7 +1324,7 @@ impl ::protobuf::Message for NumberDescription {
     #[allow(unused_variables)]
     fn compute_size(&self) -> u32 {
         let mut my_size = 0;
-        if self.money != FlowyMoney::CNY {
+        if self.money != MoneySymbol::CNY {
             my_size += ::protobuf::rt::enum_size(1, self.money);
         }
         if self.scale != 0 {
@@ -1345,7 +1345,7 @@ impl ::protobuf::Message for NumberDescription {
     }
 
     fn write_to_with_cached_sizes(&self, os: &mut ::protobuf::CodedOutputStream<'_>) -> ::protobuf::ProtobufResult<()> {
-        if self.money != FlowyMoney::CNY {
+        if self.money != MoneySymbol::CNY {
             os.write_enum(1, ::protobuf::ProtobufEnum::value(&self.money))?;
         }
         if self.scale != 0 {
@@ -1398,7 +1398,7 @@ impl ::protobuf::Message for NumberDescription {
         static descriptor: ::protobuf::rt::LazyV2<::protobuf::reflect::MessageDescriptor> = ::protobuf::rt::LazyV2::INIT;
         descriptor.get(|| {
             let mut fields = ::std::vec::Vec::new();
-            fields.push(::protobuf::reflect::accessor::make_simple_field_accessor::<_, ::protobuf::types::ProtobufTypeEnum<FlowyMoney>>(
+            fields.push(::protobuf::reflect::accessor::make_simple_field_accessor::<_, ::protobuf::types::ProtobufTypeEnum<MoneySymbol>>(
                 "money",
                 |m: &NumberDescription| { &m.money },
                 |m: &mut NumberDescription| { &mut m.money },
@@ -1439,7 +1439,7 @@ impl ::protobuf::Message for NumberDescription {
 
 impl ::protobuf::Clear for NumberDescription {
     fn clear(&mut self) {
-        self.money = FlowyMoney::CNY;
+        self.money = MoneySymbol::CNY;
         self.scale = 0;
         self.symbol.clear();
         self.sign_positive = false;
@@ -1567,31 +1567,31 @@ impl ::protobuf::reflect::ProtobufValue for TimeFormat {
 }
 
 #[derive(Clone,PartialEq,Eq,Debug,Hash)]
-pub enum FlowyMoney {
+pub enum MoneySymbol {
     CNY = 0,
     EUR = 1,
     USD = 2,
 }
 
-impl ::protobuf::ProtobufEnum for FlowyMoney {
+impl ::protobuf::ProtobufEnum for MoneySymbol {
     fn value(&self) -> i32 {
         *self as i32
     }
 
-    fn from_i32(value: i32) -> ::std::option::Option<FlowyMoney> {
+    fn from_i32(value: i32) -> ::std::option::Option<MoneySymbol> {
         match value {
-            0 => ::std::option::Option::Some(FlowyMoney::CNY),
-            1 => ::std::option::Option::Some(FlowyMoney::EUR),
-            2 => ::std::option::Option::Some(FlowyMoney::USD),
+            0 => ::std::option::Option::Some(MoneySymbol::CNY),
+            1 => ::std::option::Option::Some(MoneySymbol::EUR),
+            2 => ::std::option::Option::Some(MoneySymbol::USD),
             _ => ::std::option::Option::None
         }
     }
 
     fn values() -> &'static [Self] {
-        static values: &'static [FlowyMoney] = &[
-            FlowyMoney::CNY,
-            FlowyMoney::EUR,
-            FlowyMoney::USD,
+        static values: &'static [MoneySymbol] = &[
+            MoneySymbol::CNY,
+            MoneySymbol::EUR,
+            MoneySymbol::USD,
         ];
         values
     }
@@ -1599,47 +1599,48 @@ impl ::protobuf::ProtobufEnum for FlowyMoney {
     fn enum_descriptor_static() -> &'static ::protobuf::reflect::EnumDescriptor {
         static descriptor: ::protobuf::rt::LazyV2<::protobuf::reflect::EnumDescriptor> = ::protobuf::rt::LazyV2::INIT;
         descriptor.get(|| {
-            ::protobuf::reflect::EnumDescriptor::new_pb_name::<FlowyMoney>("FlowyMoney", file_descriptor_proto())
+            ::protobuf::reflect::EnumDescriptor::new_pb_name::<MoneySymbol>("MoneySymbol", file_descriptor_proto())
         })
     }
 }
 
-impl ::std::marker::Copy for FlowyMoney {
+impl ::std::marker::Copy for MoneySymbol {
 }
 
-impl ::std::default::Default for FlowyMoney {
+impl ::std::default::Default for MoneySymbol {
     fn default() -> Self {
-        FlowyMoney::CNY
+        MoneySymbol::CNY
     }
 }
 
-impl ::protobuf::reflect::ProtobufValue for FlowyMoney {
+impl ::protobuf::reflect::ProtobufValue for MoneySymbol {
     fn as_ref(&self) -> ::protobuf::reflect::ReflectValueRef {
         ::protobuf::reflect::ReflectValueRef::Enum(::protobuf::ProtobufEnum::descriptor(self))
     }
 }
 
 static file_descriptor_proto_data: &'static [u8] = b"\
-    \n\x0fcell_data.proto\"-\n\x13RichTextDescription\x12\x16\n\x06format\
+    \n\x12type_options.proto\"-\n\x13RichTextDescription\x12\x16\n\x06format\
     \x18\x01\x20\x01(\tR\x06format\"6\n\x13CheckboxDescription\x12\x1f\n\x0b\
     is_selected\x18\x01\x20\x01(\x08R\nisSelected\"m\n\x0fDateDescription\
     \x12,\n\x0bdate_format\x18\x01\x20\x01(\x0e2\x0b.DateFormatR\ndateFormat\
     \x12,\n\x0btime_format\x18\x02\x20\x01(\x0e2\x0b.TimeFormatR\ntimeFormat\
-    \"\\\n\x0cSingleSelect\x12'\n\x07options\x18\x01\x20\x03(\x0b2\r.SelectO\
-    ptionR\x07options\x12#\n\rdisable_color\x18\x02\x20\x01(\x08R\x0cdisable\
-    Color\"[\n\x0bMultiSelect\x12'\n\x07options\x18\x01\x20\x03(\x0b2\r.Sele\
-    ctOptionR\x07options\x12#\n\rdisable_color\x18\x02\x20\x01(\x08R\x0cdisa\
-    bleColor\"H\n\x0cSelectOption\x12\x0e\n\x02id\x18\x01\x20\x01(\tR\x02id\
-    \x12\x12\n\x04name\x18\x02\x20\x01(\tR\x04name\x12\x14\n\x05color\x18\
-    \x03\x20\x01(\tR\x05color\"\x9d\x01\n\x11NumberDescription\x12!\n\x05mon\
-    ey\x18\x01\x20\x01(\x0e2\x0b.FlowyMoneyR\x05money\x12\x14\n\x05scale\x18\
-    \x02\x20\x01(\rR\x05scale\x12\x16\n\x06symbol\x18\x03\x20\x01(\tR\x06sym\
-    bol\x12#\n\rsign_positive\x18\x04\x20\x01(\x08R\x0csignPositive\x12\x12\
-    \n\x04name\x18\x05\x20\x01(\tR\x04name*6\n\nDateFormat\x12\t\n\x05Local\
-    \x10\0\x12\x06\n\x02US\x10\x01\x12\x07\n\x03ISO\x10\x02\x12\x0c\n\x08Fri\
-    endly\x10\x03*0\n\nTimeFormat\x12\x0e\n\nTwelveHour\x10\0\x12\x12\n\x0eT\
-    wentyFourHour\x10\x01*'\n\nFlowyMoney\x12\x07\n\x03CNY\x10\0\x12\x07\n\
-    \x03EUR\x10\x01\x12\x07\n\x03USD\x10\x02b\x06proto3\
+    \"g\n\x17SingleSelectDescription\x12'\n\x07options\x18\x01\x20\x03(\x0b2\
+    \r.SelectOptionR\x07options\x12#\n\rdisable_color\x18\x02\x20\x01(\x08R\
+    \x0cdisableColor\"f\n\x16MultiSelectDescription\x12'\n\x07options\x18\
+    \x01\x20\x03(\x0b2\r.SelectOptionR\x07options\x12#\n\rdisable_color\x18\
+    \x02\x20\x01(\x08R\x0cdisableColor\"H\n\x0cSelectOption\x12\x0e\n\x02id\
+    \x18\x01\x20\x01(\tR\x02id\x12\x12\n\x04name\x18\x02\x20\x01(\tR\x04name\
+    \x12\x14\n\x05color\x18\x03\x20\x01(\tR\x05color\"\x9e\x01\n\x11NumberDe\
+    scription\x12\"\n\x05money\x18\x01\x20\x01(\x0e2\x0c.MoneySymbolR\x05mon\
+    ey\x12\x14\n\x05scale\x18\x02\x20\x01(\rR\x05scale\x12\x16\n\x06symbol\
+    \x18\x03\x20\x01(\tR\x06symbol\x12#\n\rsign_positive\x18\x04\x20\x01(\
+    \x08R\x0csignPositive\x12\x12\n\x04name\x18\x05\x20\x01(\tR\x04name*6\n\
+    \nDateFormat\x12\t\n\x05Local\x10\0\x12\x06\n\x02US\x10\x01\x12\x07\n\
+    \x03ISO\x10\x02\x12\x0c\n\x08Friendly\x10\x03*0\n\nTimeFormat\x12\x0e\n\
+    \nTwelveHour\x10\0\x12\x12\n\x0eTwentyFourHour\x10\x01*(\n\x0bMoneySymbo\
+    l\x12\x07\n\x03CNY\x10\0\x12\x07\n\x03EUR\x10\x01\x12\x07\n\x03USD\x10\
+    \x02b\x06proto3\
 ";
 
 static file_descriptor_proto_lazy: ::protobuf::rt::LazyV2<::protobuf::descriptor::FileDescriptorProto> = ::protobuf::rt::LazyV2::INIT;

+ 4 - 4
frontend/rust-lib/flowy-grid/src/protobuf/proto/cell_data.proto → frontend/rust-lib/flowy-grid/src/protobuf/proto/type_options.proto

@@ -10,11 +10,11 @@ message DateDescription {
     DateFormat date_format = 1;
     TimeFormat time_format = 2;
 }
-message SingleSelect {
+message SingleSelectDescription {
     repeated SelectOption options = 1;
     bool disable_color = 2;
 }
-message MultiSelect {
+message MultiSelectDescription {
     repeated SelectOption options = 1;
     bool disable_color = 2;
 }
@@ -24,7 +24,7 @@ message SelectOption {
     string color = 3;
 }
 message NumberDescription {
-    FlowyMoney money = 1;
+    MoneySymbol money = 1;
     uint32 scale = 2;
     string symbol = 3;
     bool sign_positive = 4;
@@ -40,7 +40,7 @@ enum TimeFormat {
     TwelveHour = 0;
     TwentyFourHour = 1;
 }
-enum FlowyMoney {
+enum MoneySymbol {
     CNY = 0;
     EUR = 1;
     USD = 2;

+ 34 - 0
frontend/rust-lib/flowy-grid/src/services/field/cell_stringify.rs

@@ -0,0 +1,34 @@
+use crate::services::field::*;
+use crate::services::util::*;
+use flowy_error::FlowyError;
+use flowy_grid_data_model::entities::{AnyData, Field, FieldType};
+
+pub trait StringifyCellData {
+    fn str_from_cell_data(&self, data: AnyData) -> String;
+    fn str_to_cell_data(&self, s: &str) -> Result<AnyData, FlowyError>;
+}
+
+#[allow(dead_code)]
+pub fn stringify_serialize(field: &Field, s: &str) -> Result<AnyData, FlowyError> {
+    match field.field_type {
+        FieldType::RichText => RichTextDescription::from(field).str_to_cell_data(s),
+        FieldType::Number => NumberDescription::from(field).str_to_cell_data(s),
+        FieldType::DateTime => DateDescription::from(field).str_to_cell_data(s),
+        FieldType::SingleSelect => SingleSelectDescription::from(field).str_to_cell_data(s),
+        FieldType::MultiSelect => MultiSelectDescription::from(field).str_to_cell_data(s),
+        FieldType::Checkbox => CheckboxDescription::from(field).str_to_cell_data(s),
+    }
+}
+
+pub(crate) fn stringify_deserialize(data: AnyData, field: &Field) -> Result<String, FlowyError> {
+    let _ = check_type_id(&data, field)?;
+    let s = match field.field_type {
+        FieldType::RichText => RichTextDescription::from(field).str_from_cell_data(data),
+        FieldType::Number => NumberDescription::from(field).str_from_cell_data(data),
+        FieldType::DateTime => DateDescription::from(field).str_from_cell_data(data),
+        FieldType::SingleSelect => SingleSelectDescription::from(field).str_from_cell_data(data),
+        FieldType::MultiSelect => MultiSelectDescription::from(field).str_from_cell_data(data),
+        FieldType::Checkbox => CheckboxDescription::from(field).str_from_cell_data(data),
+    };
+    Ok(s)
+}

+ 217 - 0
frontend/rust-lib/flowy-grid/src/services/field/field_builder.rs

@@ -0,0 +1,217 @@
+use crate::services::field::{
+    CheckboxDescription, DateDescription, DateFormat, MoneySymbol, MultiSelectDescription, NumberDescription,
+    RichTextDescription, SelectOption, SingleSelectDescription, TimeFormat,
+};
+use flowy_grid_data_model::entities::{AnyData, Field, FieldType};
+
+pub struct FieldBuilder {
+    field: Field,
+    type_options_builder: Box<dyn TypeOptionsBuilder>,
+}
+
+impl FieldBuilder {
+    pub fn new<T: TypeOptionsBuilder + 'static>(type_options_builder: T) -> Self {
+        let field = Field::new("Name", "", FieldType::RichText);
+        Self {
+            field,
+            type_options_builder: Box::new(type_options_builder),
+        }
+    }
+
+    pub fn name(mut self, name: &str) -> Self {
+        self.field.name = name.to_owned();
+        self
+    }
+
+    pub fn desc(mut self, desc: &str) -> Self {
+        self.field.desc = desc.to_owned();
+        self
+    }
+
+    pub fn field_type(mut self, field_type: FieldType) -> Self {
+        self.field.field_type = field_type;
+        self
+    }
+
+    pub fn visibility(mut self, visibility: bool) -> Self {
+        self.field.visibility = visibility;
+        self
+    }
+
+    pub fn width(mut self, width: i32) -> Self {
+        self.field.width = width;
+        self
+    }
+
+    pub fn frozen(mut self, frozen: bool) -> Self {
+        self.field.frozen = frozen;
+        self
+    }
+
+    pub fn build(mut self) -> Field {
+        assert_eq!(self.field.field_type, self.type_options_builder.field_type());
+
+        let type_options = self.type_options_builder.build();
+        self.field.type_options = type_options;
+        self.field
+    }
+}
+
+pub trait TypeOptionsBuilder {
+    fn field_type(&self) -> FieldType;
+    fn build(&self) -> AnyData;
+}
+
+// Text
+pub struct RichTextTypeOptionsBuilder(RichTextDescription);
+
+impl RichTextTypeOptionsBuilder {
+    pub fn new() -> Self {
+        Self(RichTextDescription::default())
+    }
+}
+
+impl TypeOptionsBuilder for RichTextTypeOptionsBuilder {
+    fn field_type(&self) -> FieldType {
+        self.0.field_type()
+    }
+
+    fn build(&self) -> AnyData {
+        self.0.clone().into()
+    }
+}
+
+// Number
+pub struct NumberTypeOptionsBuilder(NumberDescription);
+
+impl NumberTypeOptionsBuilder {
+    pub fn new() -> Self {
+        Self(NumberDescription::default())
+    }
+
+    pub fn name(mut self, name: &str) -> Self {
+        self.0.name = name.to_string();
+        self
+    }
+
+    pub fn set_money_symbol(mut self, money_symbol: MoneySymbol) -> Self {
+        self.0.set_money_symbol(money_symbol);
+        self
+    }
+
+    pub fn scale(mut self, scale: u32) -> Self {
+        self.0.scale = scale;
+        self
+    }
+
+    pub fn positive(mut self, positive: bool) -> Self {
+        self.0.sign_positive = positive;
+        self
+    }
+}
+
+impl TypeOptionsBuilder for NumberTypeOptionsBuilder {
+    fn field_type(&self) -> FieldType {
+        self.0.field_type()
+    }
+
+    fn build(&self) -> AnyData {
+        self.0.clone().into()
+    }
+}
+
+// Date
+pub struct DateTypeOptionsBuilder(DateDescription);
+impl DateTypeOptionsBuilder {
+    pub fn new() -> Self {
+        Self(DateDescription::default())
+    }
+
+    pub fn date_format(mut self, date_format: DateFormat) -> Self {
+        self.0.date_format = date_format;
+        self
+    }
+
+    pub fn time_format(mut self, time_format: TimeFormat) -> Self {
+        self.0.time_format = time_format;
+        self
+    }
+}
+impl TypeOptionsBuilder for DateTypeOptionsBuilder {
+    fn field_type(&self) -> FieldType {
+        self.0.field_type()
+    }
+
+    fn build(&self) -> AnyData {
+        self.0.clone().into()
+    }
+}
+
+// Single Select
+pub struct SingleSelectTypeOptionsBuilder(SingleSelectDescription);
+
+impl SingleSelectTypeOptionsBuilder {
+    pub fn new() -> Self {
+        Self(SingleSelectDescription::default())
+    }
+
+    pub fn option(mut self, opt: SelectOption) -> Self {
+        self.0.options.push(opt);
+        self
+    }
+}
+impl TypeOptionsBuilder for SingleSelectTypeOptionsBuilder {
+    fn field_type(&self) -> FieldType {
+        self.0.field_type()
+    }
+
+    fn build(&self) -> AnyData {
+        self.0.clone().into()
+    }
+}
+
+// Multi Select
+pub struct MultiSelectTypeOptionsBuilder(MultiSelectDescription);
+
+impl MultiSelectTypeOptionsBuilder {
+    pub fn new() -> Self {
+        Self(MultiSelectDescription::default())
+    }
+
+    pub fn option(mut self, opt: SelectOption) -> Self {
+        self.0.options.push(opt);
+        self
+    }
+}
+
+impl TypeOptionsBuilder for MultiSelectTypeOptionsBuilder {
+    fn field_type(&self) -> FieldType {
+        self.0.field_type()
+    }
+
+    fn build(&self) -> AnyData {
+        self.0.clone().into()
+    }
+}
+
+// Checkbox
+pub struct CheckboxTypeOptionsBuilder(CheckboxDescription);
+impl CheckboxTypeOptionsBuilder {
+    pub fn new() -> Self {
+        Self(CheckboxDescription::default())
+    }
+
+    pub fn set_selected(mut self, is_selected: bool) -> Self {
+        self.0.is_selected = is_selected;
+        self
+    }
+}
+impl TypeOptionsBuilder for CheckboxTypeOptionsBuilder {
+    fn field_type(&self) -> FieldType {
+        self.0.field_type()
+    }
+
+    fn build(&self) -> AnyData {
+        self.0.clone().into()
+    }
+}

+ 7 - 0
frontend/rust-lib/flowy-grid/src/services/field/mod.rs

@@ -0,0 +1,7 @@
+mod cell_stringify;
+mod field_builder;
+mod type_options;
+
+pub use cell_stringify::*;
+pub use field_builder::*;
+pub use type_options::*;

+ 59 - 116
frontend/rust-lib/flowy-grid/src/services/cell_data.rs → frontend/rust-lib/flowy-grid/src/services/field/type_options.rs

@@ -1,5 +1,6 @@
 #![allow(clippy::upper_case_acronyms)]
-use crate::impl_any_data;
+use crate::impl_from_and_to_type_option;
+use crate::services::field::StringifyCellData;
 use crate::services::util::*;
 use bytes::Bytes;
 use chrono::format::strftime::StrftimeItems;
@@ -15,63 +16,42 @@ use rusty_money::{
 use std::str::FromStr;
 use strum_macros::EnumIter;
 
-pub trait StringifyAnyData {
-    fn stringify_any_data(&self, data: AnyData) -> String;
-    fn str_to_any_data(&self, s: &str) -> Result<AnyData, FlowyError>;
-}
-
-pub trait DisplayCell {
-    fn display_content(&self, s: &str) -> String;
-}
-
 #[derive(Debug, Clone, ProtoBuf, Default)]
 pub struct RichTextDescription {
     #[pb(index = 1)]
     pub format: String,
 }
-impl_any_data!(RichTextDescription, FieldType::RichText);
+impl_from_and_to_type_option!(RichTextDescription, FieldType::RichText);
 
-impl StringifyAnyData for RichTextDescription {
-    fn stringify_any_data(&self, data: AnyData) -> String {
+impl StringifyCellData for RichTextDescription {
+    fn str_from_cell_data(&self, data: AnyData) -> String {
         data.to_string()
     }
 
-    fn str_to_any_data(&self, s: &str) -> Result<AnyData, FlowyError> {
-        Ok(AnyData::from_str(&RichTextDescription::field_type(), s))
-    }
-}
-
-impl DisplayCell for RichTextDescription {
-    fn display_content(&self, s: &str) -> String {
-        s.to_string()
+    fn str_to_cell_data(&self, s: &str) -> Result<AnyData, FlowyError> {
+        Ok(AnyData::from_str(self.field_type(), s))
     }
 }
 
 // Checkbox
-#[derive(Debug, ProtoBuf, Default)]
+#[derive(Debug, Clone, ProtoBuf, Default)]
 pub struct CheckboxDescription {
     #[pb(index = 1)]
     pub is_selected: bool,
 }
-impl_any_data!(CheckboxDescription, FieldType::Checkbox);
+impl_from_and_to_type_option!(CheckboxDescription, FieldType::Checkbox);
 
-impl StringifyAnyData for CheckboxDescription {
-    fn stringify_any_data(&self, data: AnyData) -> String {
+impl StringifyCellData for CheckboxDescription {
+    fn str_from_cell_data(&self, data: AnyData) -> String {
         data.to_string()
     }
 
-    fn str_to_any_data(&self, s: &str) -> Result<AnyData, FlowyError> {
+    fn str_to_cell_data(&self, s: &str) -> Result<AnyData, FlowyError> {
         let s = match string_to_bool(s) {
             true => "1",
             false => "0",
         };
-        Ok(AnyData::from_str(&CheckboxDescription::field_type(), s))
-    }
-}
-
-impl DisplayCell for CheckboxDescription {
-    fn display_content(&self, s: &str) -> String {
-        s.to_string()
+        Ok(AnyData::from_str(self.field_type(), s))
     }
 }
 
@@ -84,7 +64,7 @@ pub struct DateDescription {
     #[pb(index = 2)]
     pub time_format: TimeFormat,
 }
-impl_any_data!(DateDescription, FieldType::DateTime);
+impl_from_and_to_type_option!(DateDescription, FieldType::DateTime);
 
 impl DateDescription {
     fn date_time_format_str(&self) -> String {
@@ -107,23 +87,8 @@ impl DateDescription {
     }
 }
 
-impl DisplayCell for DateDescription {
-    fn display_content(&self, s: &str) -> String {
-        match s.parse::<i64>() {
-            Ok(timestamp) => {
-                let native = NaiveDateTime::from_timestamp(timestamp, 0);
-                self.today_from_native(native)
-            }
-            Err(e) => {
-                tracing::debug!("DateDescription format {} fail. error: {:?}", s, e);
-                String::new()
-            }
-        }
-    }
-}
-
-impl StringifyAnyData for DateDescription {
-    fn stringify_any_data(&self, data: AnyData) -> String {
+impl StringifyCellData for DateDescription {
+    fn str_from_cell_data(&self, data: AnyData) -> String {
         match String::from_utf8(data.value) {
             Ok(s) => match s.parse::<i64>() {
                 Ok(timestamp) => {
@@ -142,14 +107,11 @@ impl StringifyAnyData for DateDescription {
         }
     }
 
-    fn str_to_any_data(&self, s: &str) -> Result<AnyData, FlowyError> {
+    fn str_to_cell_data(&self, s: &str) -> Result<AnyData, FlowyError> {
         let timestamp = s
             .parse::<i64>()
             .map_err(|e| FlowyError::internal().context(format!("Parse {} to i64 failed: {}", s, e)))?;
-        Ok(AnyData::from_str(
-            &DateDescription::field_type(),
-            &format!("{}", timestamp),
-        ))
+        Ok(AnyData::from_str(self.field_type(), &format!("{}", timestamp)))
     }
 }
 
@@ -237,54 +199,42 @@ impl std::default::Default for TimeFormat {
 
 // Single select
 #[derive(Clone, Debug, ProtoBuf, Default)]
-pub struct SingleSelect {
+pub struct SingleSelectDescription {
     #[pb(index = 1)]
     pub options: Vec<SelectOption>,
 
     #[pb(index = 2)]
     pub disable_color: bool,
 }
-impl_any_data!(SingleSelect, FieldType::SingleSelect);
+impl_from_and_to_type_option!(SingleSelectDescription, FieldType::SingleSelect);
 
-impl StringifyAnyData for SingleSelect {
-    fn stringify_any_data(&self, data: AnyData) -> String {
+impl StringifyCellData for SingleSelectDescription {
+    fn str_from_cell_data(&self, data: AnyData) -> String {
         data.to_string()
     }
 
-    fn str_to_any_data(&self, s: &str) -> Result<AnyData, FlowyError> {
-        Ok(AnyData::from_str(&SingleSelect::field_type(), s))
-    }
-}
-
-impl DisplayCell for SingleSelect {
-    fn display_content(&self, s: &str) -> String {
-        s.to_string()
+    fn str_to_cell_data(&self, s: &str) -> Result<AnyData, FlowyError> {
+        Ok(AnyData::from_str(self.field_type(), s))
     }
 }
 
 // Multiple select
 #[derive(Clone, Debug, ProtoBuf, Default)]
-pub struct MultiSelect {
+pub struct MultiSelectDescription {
     #[pb(index = 1)]
     pub options: Vec<SelectOption>,
 
     #[pb(index = 2)]
     pub disable_color: bool,
 }
-impl_any_data!(MultiSelect, FieldType::MultiSelect);
-impl StringifyAnyData for MultiSelect {
-    fn stringify_any_data(&self, data: AnyData) -> String {
+impl_from_and_to_type_option!(MultiSelectDescription, FieldType::MultiSelect);
+impl StringifyCellData for MultiSelectDescription {
+    fn str_from_cell_data(&self, data: AnyData) -> String {
         data.to_string()
     }
 
-    fn str_to_any_data(&self, s: &str) -> Result<AnyData, FlowyError> {
-        Ok(AnyData::from_str(&MultiSelect::field_type(), s))
-    }
-}
-
-impl DisplayCell for MultiSelect {
-    fn display_content(&self, s: &str) -> String {
-        s.to_string()
+    fn str_to_cell_data(&self, s: &str) -> Result<AnyData, FlowyError> {
+        Ok(AnyData::from_str(self.field_type(), s))
     }
 }
 
@@ -314,7 +264,7 @@ impl SelectOption {
 #[derive(Clone, Debug, ProtoBuf)]
 pub struct NumberDescription {
     #[pb(index = 1)]
-    pub money: FlowyMoney,
+    pub money: MoneySymbol,
 
     #[pb(index = 2)]
     pub scale: u32,
@@ -328,24 +278,26 @@ pub struct NumberDescription {
     #[pb(index = 5)]
     pub name: String,
 }
-impl_any_data!(NumberDescription, FieldType::Number);
+impl_from_and_to_type_option!(NumberDescription, FieldType::Number);
 
 impl std::default::Default for NumberDescription {
     fn default() -> Self {
+        let money = MoneySymbol::default();
+        let symbol = money.symbol_str();
         NumberDescription {
-            money: FlowyMoney::default(),
+            money,
             scale: 0,
-            symbol: String::new(),
+            symbol,
             sign_positive: true,
-            name: String::new(),
+            name: "Number".to_string(),
         }
     }
 }
 
 impl NumberDescription {
-    pub fn set_money(&mut self, money: FlowyMoney) {
-        self.money = money;
-        self.symbol = money.symbol();
+    pub fn set_money_symbol(&mut self, money_symbol: MoneySymbol) {
+        self.money = money_symbol;
+        self.symbol = money_symbol.symbol_str();
     }
 
     fn money_from_str(&self, s: &str) -> Option<String> {
@@ -368,17 +320,8 @@ impl NumberDescription {
     }
 }
 
-impl DisplayCell for NumberDescription {
-    fn display_content(&self, s: &str) -> String {
-        match self.money_from_str(s) {
-            Some(money_str) => money_str,
-            None => String::default(),
-        }
-    }
-}
-
-impl StringifyAnyData for NumberDescription {
-    fn stringify_any_data(&self, data: AnyData) -> String {
+impl StringifyCellData for NumberDescription {
+    fn str_from_cell_data(&self, data: AnyData) -> String {
         match String::from_utf8(data.value) {
             Ok(s) => match self.money_from_str(&s) {
                 Some(money_str) => money_str,
@@ -391,47 +334,47 @@ impl StringifyAnyData for NumberDescription {
         }
     }
 
-    fn str_to_any_data(&self, s: &str) -> Result<AnyData, FlowyError> {
+    fn str_to_cell_data(&self, s: &str) -> Result<AnyData, FlowyError> {
         let strip_symbol_money = strip_money_symbol(s);
         let decimal = Decimal::from_str(&strip_symbol_money).map_err(|err| FlowyError::internal().context(err))?;
         let money_str = decimal.to_string();
-        Ok(AnyData::from_str(&NumberDescription::field_type(), &money_str))
+        Ok(AnyData::from_str(self.field_type(), &money_str))
     }
 }
 
 #[derive(Clone, Copy, Debug, EnumIter, ProtoBuf_Enum)]
-pub enum FlowyMoney {
+pub enum MoneySymbol {
     CNY = 0,
     EUR = 1,
     USD = 2,
 }
 
-impl std::default::Default for FlowyMoney {
+impl std::default::Default for MoneySymbol {
     fn default() -> Self {
-        FlowyMoney::USD
+        MoneySymbol::USD
     }
 }
 
-impl FlowyMoney {
+impl MoneySymbol {
     // Currency list https://docs.rs/rusty-money/0.4.0/rusty_money/iso/index.html
-    pub fn from_symbol_str(s: &str) -> FlowyMoney {
+    pub fn from_symbol_str(s: &str) -> MoneySymbol {
         match s {
-            "CNY" => FlowyMoney::CNY,
-            "EUR" => FlowyMoney::EUR,
-            "USD" => FlowyMoney::USD,
-            _ => FlowyMoney::CNY,
+            "CNY" => MoneySymbol::CNY,
+            "EUR" => MoneySymbol::EUR,
+            "USD" => MoneySymbol::USD,
+            _ => MoneySymbol::CNY,
         }
     }
 
-    pub fn from_money(money: &rusty_money::Money<Currency>) -> FlowyMoney {
-        FlowyMoney::from_symbol_str(&money.currency().symbol.to_string())
+    pub fn from_money(money: &rusty_money::Money<Currency>) -> MoneySymbol {
+        MoneySymbol::from_symbol_str(&money.currency().symbol.to_string())
     }
 
     pub fn currency(&self) -> &'static Currency {
         match self {
-            FlowyMoney::CNY => CNY,
-            FlowyMoney::EUR => EUR,
-            FlowyMoney::USD => USD,
+            MoneySymbol::CNY => CNY,
+            MoneySymbol::EUR => EUR,
+            MoneySymbol::USD => USD,
         }
     }
 
@@ -442,7 +385,7 @@ impl FlowyMoney {
         self.currency().iso_alpha_code.to_string()
     }
 
-    pub fn symbol(&self) -> String {
+    pub fn symbol_str(&self) -> String {
         self.currency().symbol.to_string()
     }
 

+ 11 - 4
frontend/rust-lib/flowy-grid/src/services/grid_editor.rs

@@ -38,7 +38,7 @@ impl ClientGridEditor {
     ) -> FlowyResult<Arc<Self>> {
         let token = user.token()?;
         let cloud = Arc::new(GridRevisionCloudService { token });
-        let grid_pad = rev_manager.load::<GridPadBuilder>(cloud).await?;
+        let grid_pad = rev_manager.load::<GridPadBuilder>(Some(cloud)).await?;
         let rev_manager = Arc::new(rev_manager);
         let grid_meta_pad = Arc::new(RwLock::new(grid_pad));
 
@@ -55,12 +55,12 @@ impl ClientGridEditor {
         }))
     }
 
-    pub async fn create_field(&mut self, field: Field) -> FlowyResult<()> {
+    pub async fn create_field(&self, field: Field) -> FlowyResult<()> {
         let _ = self.modify(|grid| Ok(grid.create_field(field)?)).await?;
         Ok(())
     }
 
-    pub async fn delete_field(&mut self, field_id: &str) -> FlowyResult<()> {
+    pub async fn delete_field(&self, field_id: &str) -> FlowyResult<()> {
         let _ = self.modify(|grid| Ok(grid.delete_field(field_id)?)).await?;
         Ok(())
     }
@@ -125,6 +125,13 @@ impl ClientGridEditor {
     }
 }
 
+#[cfg(feature = "flowy_unit_test")]
+impl ClientGridEditor {
+    pub fn rev_manager(&self) -> Arc<RevisionManager> {
+        self.rev_manager.clone()
+    }
+}
+
 async fn load_all_fields(
     grid_pad: &GridMetaPad,
     kv_persistence: &Arc<GridKVPersistence>,
@@ -143,7 +150,7 @@ async fn load_all_fields(
     Ok(map)
 }
 
-struct GridPadBuilder();
+pub struct GridPadBuilder();
 impl RevisionObjectBuilder for GridPadBuilder {
     type Output = GridMetaPad;
 

+ 1 - 1
frontend/rust-lib/flowy-grid/src/services/grid_meta_editor.rs

@@ -27,7 +27,7 @@ impl ClientGridBlockMetaEditor {
         let cloud = Arc::new(GridBlockMetaRevisionCloudService {
             token: token.to_owned(),
         });
-        let block_meta_pad = rev_manager.load::<GridBlockMetaPadBuilder>(cloud).await?;
+        let block_meta_pad = rev_manager.load::<GridBlockMetaPadBuilder>(Some(cloud)).await?;
         let meta_pad = Arc::new(RwLock::new(block_meta_pad));
         let rev_manager = Arc::new(rev_manager);
         let user_id = user_id.to_owned();

+ 1 - 2
frontend/rust-lib/flowy-grid/src/services/mod.rs

@@ -1,7 +1,6 @@
 mod util;
 
-pub mod cell_data;
+pub mod field;
 pub mod grid_editor;
 pub mod grid_meta_editor;
 pub mod kv_persistence;
-pub mod stringify;

+ 0 - 29
frontend/rust-lib/flowy-grid/src/services/stringify.rs

@@ -1,29 +0,0 @@
-use crate::services::cell_data::*;
-use crate::services::util::*;
-use flowy_error::FlowyError;
-use flowy_grid_data_model::entities::{AnyData, Field, FieldType};
-
-#[allow(dead_code)]
-pub fn stringify_serialize(field: &Field, s: &str) -> Result<AnyData, FlowyError> {
-    match field.field_type {
-        FieldType::RichText => RichTextDescription::from(field).str_to_any_data(s),
-        FieldType::Number => NumberDescription::from(field).str_to_any_data(s),
-        FieldType::DateTime => DateDescription::from(field).str_to_any_data(s),
-        FieldType::SingleSelect => SingleSelect::from(field).str_to_any_data(s),
-        FieldType::MultiSelect => MultiSelect::from(field).str_to_any_data(s),
-        FieldType::Checkbox => CheckboxDescription::from(field).str_to_any_data(s),
-    }
-}
-
-pub(crate) fn stringify_deserialize(data: AnyData, field: &Field) -> Result<String, FlowyError> {
-    let _ = check_type_id(&data, field)?;
-    let s = match field.field_type {
-        FieldType::RichText => RichTextDescription::from(field).stringify_any_data(data),
-        FieldType::Number => NumberDescription::from(field).stringify_any_data(data),
-        FieldType::DateTime => DateDescription::from(field).stringify_any_data(data),
-        FieldType::SingleSelect => SingleSelect::from(field).stringify_any_data(data),
-        FieldType::MultiSelect => MultiSelect::from(field).stringify_any_data(data),
-        FieldType::Checkbox => CheckboxDescription::from(field).stringify_any_data(data),
-    };
-    Ok(s)
-}

+ 5 - 5
frontend/rust-lib/flowy-grid/src/services/util.rs

@@ -1,4 +1,4 @@
-use crate::services::cell_data::FlowyMoney;
+use crate::services::field::MoneySymbol;
 use flowy_error::FlowyError;
 use flowy_grid_data_model::entities::{AnyData, Field, FieldType};
 use lazy_static::lazy_static;
@@ -16,8 +16,8 @@ lazy_static! {
 fn generate_currency_by_symbol() -> HashMap<String, &'static Currency> {
     let mut map: HashMap<String, &'static Currency> = HashMap::new();
 
-    for money in FlowyMoney::iter() {
-        map.insert(money.symbol(), money.currency());
+    for money in MoneySymbol::iter() {
+        map.insert(money.symbol_str(), money.currency());
     }
     map
 }
@@ -25,7 +25,7 @@ fn generate_currency_by_symbol() -> HashMap<String, &'static Currency> {
 #[allow(dead_code)]
 pub fn string_to_money(money_str: &str) -> Option<Money<Currency>> {
     let mut process_money_str = String::from(money_str);
-    let default_currency = FlowyMoney::from_symbol_str("CNY").currency();
+    let default_currency = MoneySymbol::from_symbol_str("CNY").currency();
 
     if process_money_str.is_empty() {
         return None;
@@ -65,7 +65,7 @@ pub fn money_from_str(s: &str) -> Option<String> {
                 }
             }
             decimal.set_sign_positive(true);
-            Some(FlowyMoney::USD.with_decimal(decimal).to_string())
+            Some(MoneySymbol::USD.with_decimal(decimal).to_string())
         }
         Err(e) => {
             tracing::debug!("Format {} to money failed, {:?}", s, e);

+ 30 - 0
frontend/rust-lib/flowy-grid/src/util.rs

@@ -0,0 +1,30 @@
+use crate::services::field::*;
+use flowy_collaboration::client_grid::{BuildGridInfo, GridBuilder};
+use flowy_grid_data_model::entities::{Field, FieldType};
+
+pub fn make_default_grid(grid_id: &str) -> BuildGridInfo {
+    let text_field = FieldBuilder::new(RichTextTypeOptionsBuilder::new())
+        .name("Name")
+        .visibility(true)
+        .field_type(FieldType::RichText)
+        .build();
+
+    let single_select = SingleSelectTypeOptionsBuilder::new()
+        .option(SelectOption::new("Done"))
+        .option(SelectOption::new("Progress"));
+
+    let single_select_field = FieldBuilder::new(single_select)
+        .name("Name")
+        .visibility(true)
+        .field_type(FieldType::SingleSelect)
+        .build();
+
+    GridBuilder::new(grid_id)
+        .add_field(text_field)
+        .add_field(single_select_field)
+        .add_empty_row()
+        .add_empty_row()
+        .add_empty_row()
+        .build()
+        .unwrap()
+}

+ 16 - 0
frontend/rust-lib/flowy-grid/tests/grid/grid_test.rs

@@ -0,0 +1,16 @@
+use crate::grid::script::EditorScript::*;
+use crate::grid::script::*;
+
+#[tokio::test]
+async fn grid_creat_field_test() {
+    let scripts = vec![
+        CreateField {
+            field: create_text_field(),
+        },
+        CreateField {
+            field: create_single_select_field(),
+        },
+        AssertGridMetaPad,
+    ];
+    GridEditorTest::new().await.run_scripts(scripts).await;
+}

+ 2 - 0
frontend/rust-lib/flowy-grid/tests/grid/mod.rs

@@ -0,0 +1,2 @@
+mod grid_test;
+mod script;

+ 75 - 0
frontend/rust-lib/flowy-grid/tests/grid/script.rs

@@ -0,0 +1,75 @@
+use flowy_grid::services::field::*;
+use flowy_grid::services::grid_editor::{ClientGridEditor, GridPadBuilder};
+use flowy_grid_data_model::entities::{AnyData, Field, FieldType};
+use flowy_test::event_builder::FolderEventBuilder;
+use flowy_test::helper::ViewTest;
+use flowy_test::FlowySDKTest;
+use std::sync::Arc;
+
+pub enum EditorScript {
+    CreateField { field: Field },
+    CreateRow,
+    AssertGridMetaPad,
+}
+
+pub struct GridEditorTest {
+    pub sdk: FlowySDKTest,
+    pub grid_id: String,
+    pub editor: Arc<ClientGridEditor>,
+}
+
+impl GridEditorTest {
+    pub async fn new() -> Self {
+        let sdk = FlowySDKTest::default();
+        let _ = sdk.init_user().await;
+        let test = ViewTest::new_grid_view(&sdk).await;
+        let editor = sdk.grid_manager.open_grid(&test.view.id).await.unwrap();
+        let grid_id = test.view.id;
+        Self { sdk, grid_id, editor }
+    }
+
+    pub async fn run_scripts(&mut self, scripts: Vec<EditorScript>) {
+        for script in scripts {
+            self.run_script(script).await;
+        }
+    }
+
+    pub async fn run_script(&mut self, script: EditorScript) {
+        let grid_manager = self.sdk.grid_manager.clone();
+        let pool = self.sdk.user_session.db_pool().unwrap();
+        let rev_manager = self.editor.rev_manager();
+        let cache = rev_manager.revision_cache().await;
+
+        match script {
+            EditorScript::CreateField { field } => {
+                self.editor.create_field(field).await.unwrap();
+            }
+            EditorScript::CreateRow => {}
+            EditorScript::AssertGridMetaPad => {
+                let mut grid_rev_manager = grid_manager.make_grid_rev_manager(&self.grid_id, pool.clone()).unwrap();
+                let grid_pad = grid_rev_manager.load::<GridPadBuilder>(None).await.unwrap();
+                println!("{}", grid_pad.delta_str());
+            }
+        }
+    }
+}
+
+pub fn create_text_field() -> Field {
+    FieldBuilder::new(RichTextTypeOptionsBuilder::new())
+        .name("Name")
+        .visibility(true)
+        .field_type(FieldType::RichText)
+        .build()
+}
+
+pub fn create_single_select_field() -> Field {
+    let single_select = SingleSelectTypeOptionsBuilder::new()
+        .option(SelectOption::new("Done"))
+        .option(SelectOption::new("Progress"));
+
+    FieldBuilder::new(single_select)
+        .name("Name")
+        .visibility(true)
+        .field_type(FieldType::SingleSelect)
+        .build()
+}

+ 0 - 0
frontend/rust-lib/flowy-grid/tests/grid_test.rs


+ 1 - 0
frontend/rust-lib/flowy-grid/tests/main.rs

@@ -0,0 +1 @@
+mod grid;

+ 1 - 1
frontend/rust-lib/flowy-sdk/src/deps_resolve/folder_deps.rs

@@ -1,7 +1,6 @@
 use bytes::Bytes;
 use flowy_block::TextBlockManager;
 use flowy_collaboration::client_document::default::initial_quill_delta_string;
-use flowy_collaboration::client_grid::make_default_grid;
 use flowy_collaboration::entities::revision::{RepeatedRevision, Revision};
 use flowy_collaboration::entities::ws_data::ClientRevisionWSData;
 use flowy_database::ConnectionPool;
@@ -13,6 +12,7 @@ use flowy_folder::{
     manager::FolderManager,
 };
 use flowy_grid::manager::GridManager;
+use flowy_grid::util::make_default_grid;
 use flowy_net::ClientServerConfiguration;
 use flowy_net::{
     http_server::folder::FolderHttpCloudService, local_server::LocalServer, ws::connection::FlowyWebSocketConnect,

+ 2 - 2
frontend/rust-lib/flowy-sync/src/rev_manager.rs

@@ -67,14 +67,14 @@ impl RevisionManager {
         }
     }
 
-    pub async fn load<B>(&mut self, cloud: Arc<dyn RevisionCloudService>) -> FlowyResult<B::Output>
+    pub async fn load<B>(&mut self, cloud: Option<Arc<dyn RevisionCloudService>>) -> FlowyResult<B::Output>
     where
         B: RevisionObjectBuilder,
     {
         let (revisions, rev_id) = RevisionLoader {
             object_id: self.object_id.clone(),
             user_id: self.user_id.clone(),
-            cloud: Some(cloud),
+            cloud,
             rev_persistence: self.rev_persistence.clone(),
         }
         .load()

+ 15 - 4
frontend/rust-lib/flowy-test/src/helper.rs

@@ -25,11 +25,12 @@ pub struct ViewTest {
 }
 
 impl ViewTest {
-    pub async fn new(sdk: &FlowySDKTest) -> Self {
+    #[allow(dead_code)]
+    pub async fn new(sdk: &FlowySDKTest, data_type: ViewDataType) -> Self {
         let workspace = create_workspace(sdk, "Workspace", "").await;
         open_workspace(sdk, &workspace.id).await;
         let app = create_app(sdk, "App", "AppFlowy GitHub Project", &workspace.id).await;
-        let view = create_view(sdk, &app.id).await;
+        let view = create_view(sdk, &app.id, data_type).await;
         Self {
             sdk: sdk.clone(),
             workspace,
@@ -37,6 +38,16 @@ impl ViewTest {
             view,
         }
     }
+
+    #[allow(dead_code)]
+    pub async fn new_grid_view(sdk: &FlowySDKTest) -> Self {
+        Self::new(sdk, ViewDataType::Grid).await
+    }
+
+    #[allow(dead_code)]
+    pub async fn new_text_block_view(sdk: &FlowySDKTest) -> Self {
+        Self::new(sdk, ViewDataType::TextBlock).await
+    }
 }
 
 async fn create_workspace(sdk: &FlowySDKTest, name: &str, desc: &str) -> Workspace {
@@ -82,13 +93,13 @@ async fn create_app(sdk: &FlowySDKTest, name: &str, desc: &str, workspace_id: &s
     app
 }
 
-async fn create_view(sdk: &FlowySDKTest, app_id: &str) -> View {
+async fn create_view(sdk: &FlowySDKTest, app_id: &str, data_type: ViewDataType) -> View {
     let request = CreateViewPayload {
         belong_to_id: app_id.to_string(),
         name: "View A".to_string(),
         desc: "".to_string(),
         thumbnail: Some("http://1.png".to_string()),
-        data_type: ViewDataType::TextBlock,
+        data_type,
         ext_data: "".to_string(),
         plugin_type: 0,
     };

+ 4 - 16
shared-lib/flowy-collaboration/src/client_grid/grid_builder.rs

@@ -25,8 +25,7 @@ impl GridBuilder {
         }
     }
 
-    pub fn add_field(mut self, name: &str, desc: &str, field_type: FieldType) -> Self {
-        let field = Field::new(name, desc, field_type);
+    pub fn add_field(mut self, field: Field) -> Self {
         self.fields.push(field);
         self
     }
@@ -74,27 +73,16 @@ fn check_rows(fields: &[Field], rows: &[RowMeta]) -> CollaborateResult<()> {
     Ok(())
 }
 
-pub fn make_default_grid(grid_id: &str) -> BuildGridInfo {
-    GridBuilder::new(grid_id)
-        .add_field("Name", "", FieldType::RichText)
-        .add_field("Tags", "", FieldType::SingleSelect)
-        .add_empty_row()
-        .add_empty_row()
-        .add_empty_row()
-        .build()
-        .unwrap()
-}
-
 #[cfg(test)]
 mod tests {
     use crate::client_grid::GridBuilder;
-    use flowy_grid_data_model::entities::{FieldType, GridBlockMeta, GridMeta};
+    use flowy_grid_data_model::entities::{Field, FieldType, GridBlockMeta, GridMeta};
 
     #[test]
     fn create_default_grid_test() {
         let info = GridBuilder::new("1")
-            .add_field("Name", "", FieldType::RichText)
-            .add_field("Tags", "", FieldType::SingleSelect)
+            .add_field(Field::new("Name", "", FieldType::RichText))
+            .add_field(Field::new("Tags", "", FieldType::SingleSelect))
             .add_empty_row()
             .add_empty_row()
             .add_empty_row()

+ 15 - 4
shared-lib/flowy-grid-data-model/src/entities/meta.rs

@@ -163,8 +163,19 @@ impl std::default::Default for FieldType {
     }
 }
 
+impl AsRef<FieldType> for FieldType {
+    fn as_ref(&self) -> &FieldType {
+        &self
+    }
+}
+
+impl Into<FieldType> for &FieldType {
+    fn into(self) -> FieldType {
+        self.clone()
+    }
+}
+
 impl FieldType {
-    #[allow(dead_code)]
     pub fn type_id(&self) -> String {
         let ty = self.clone();
         format!("{}", ty as u8)
@@ -193,13 +204,13 @@ pub struct AnyData {
 }
 
 impl AnyData {
-    pub fn from_str(field_type: &FieldType, s: &str) -> AnyData {
+    pub fn from_str<F: Into<FieldType>>(field_type: F, s: &str) -> AnyData {
         Self::from_bytes(field_type, s.as_bytes().to_vec())
     }
 
-    pub fn from_bytes<T: AsRef<[u8]>>(field_type: &FieldType, bytes: T) -> AnyData {
+    pub fn from_bytes<T: AsRef<[u8]>, F: Into<FieldType>>(field_type: F, bytes: T) -> AnyData {
         AnyData {
-            type_id: field_type.type_id(),
+            type_id: field_type.into().type_id(),
             value: bytes.as_ref().to_vec(),
         }
     }