소스 검색

chore: config row detail page

appflowy 3 년 전
부모
커밋
eeba6884ce
20개의 변경된 파일506개의 추가작업 그리고 184개의 파일을 삭제
  1. 6 0
      frontend/app_flowy/assets/images/grid/expander.svg
  2. 73 0
      frontend/app_flowy/lib/workspace/application/grid/row/row_detail_bloc.dart
  3. 18 26
      frontend/app_flowy/lib/workspace/application/grid/row/row_service.dart
  4. 2 4
      frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/grid_page.dart
  5. 54 6
      frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/cell_container.dart
  6. 30 14
      frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/header/field_cell_action_sheet.dart
  7. 109 41
      frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/row/grid_row.dart
  8. 48 0
      frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/row/row_detail.dart
  9. 1 1
      frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/toolbar/grid_property.dart
  10. 14 0
      frontend/app_flowy/packages/flowy_sdk/lib/protobuf/flowy-grid-data-model/grid.pb.dart
  11. 2 1
      frontend/app_flowy/packages/flowy_sdk/lib/protobuf/flowy-grid-data-model/grid.pbjson.dart
  12. 7 1
      frontend/rust-lib/flowy-grid/src/services/field/field_builder.rs
  13. 2 3
      frontend/rust-lib/flowy-grid/src/services/grid_editor.rs
  14. 1 0
      frontend/rust-lib/flowy-grid/src/util.rs
  15. 2 0
      frontend/rust-lib/flowy-grid/tests/grid/script.rs
  16. 4 0
      shared-lib/flowy-grid-data-model/src/entities/grid.rs
  17. 11 2
      shared-lib/flowy-grid-data-model/src/entities/meta.rs
  18. 119 83
      shared-lib/flowy-grid-data-model/src/protobuf/model/grid.rs
  19. 1 0
      shared-lib/flowy-grid-data-model/src/protobuf/proto/grid.proto
  20. 2 2
      shared-lib/flowy-sync/src/client_grid/grid_builder.rs

+ 6 - 0
frontend/app_flowy/assets/images/grid/expander.svg

@@ -0,0 +1,6 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M6 13H3V10" stroke="#00BCF0" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M10 3H13V6" stroke="#00BCF0" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M3 13L7 9" stroke="#00BCF0" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M13 3L9 7" stroke="#00BCF0" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

+ 73 - 0
frontend/app_flowy/lib/workspace/application/grid/row/row_detail_bloc.dart

@@ -0,0 +1,73 @@
+import 'dart:collection';
+import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:freezed_annotation/freezed_annotation.dart';
+import 'dart:async';
+import 'row_service.dart';
+import 'package:dartz/dartz.dart';
+
+part 'row_detail_bloc.freezed.dart';
+
+class RowDetailBloc extends Bloc<RowDetailEvent, RowDetailState> {
+  final GridRow rowData;
+  final GridRowCache _rowCache;
+  void Function()? _rowListenFn;
+
+  RowDetailBloc({
+    required this.rowData,
+    required GridRowCache rowCache,
+  })  : _rowCache = rowCache,
+        super(RowDetailState.initial()) {
+    on<RowDetailEvent>(
+      (event, emit) async {
+        await event.map(
+          initial: (_Initial value) async {
+            await _startListening();
+          },
+          didReceiveCellDatas: (_DidReceiveCellDatas value) {},
+        );
+      },
+    );
+  }
+
+  @override
+  Future<void> close() async {
+    if (_rowListenFn != null) {
+      _rowCache.removeRowListener(_rowListenFn!);
+    }
+    return super.close();
+  }
+
+  Future<void> _startListening() async {
+    _rowListenFn = _rowCache.addRowListener(
+      rowId: rowData.rowId,
+      onUpdated: (cellDatas) => add(RowDetailEvent.didReceiveCellDatas(cellDatas)),
+      listenWhen: () => !isClosed,
+    );
+  }
+
+  Future<void> _loadRow(Emitter<RowDetailState> emit) async {
+    final data = _rowCache.loadCellData(rowData.rowId);
+    data.foldRight(null, (cellDatas, _) {
+      if (!isClosed) {
+        add(RowDetailEvent.didReceiveCellDatas(cellDatas));
+      }
+    });
+  }
+}
+
+@freezed
+class RowDetailEvent with _$RowDetailEvent {
+  const factory RowDetailEvent.initial() = _Initial;
+  const factory RowDetailEvent.didReceiveCellDatas(CellDataMap cellData) = _DidReceiveCellDatas;
+}
+
+@freezed
+class RowDetailState with _$RowDetailState {
+  const factory RowDetailState({
+    required Option<CellDataMap> cellDataMap,
+  }) = _RowDetailState;
+
+  factory RowDetailState.initial() => RowDetailState(
+        cellDataMap: none(),
+      );
+}

+ 18 - 26
frontend/app_flowy/lib/workspace/application/grid/row/row_service.dart

@@ -42,9 +42,9 @@ class GridRowCache {
       result.fold(
         (changesets) {
           for (final changeset in changesets) {
-            _deleteRows(changeset.deletedRows);
-            _insertRows(changeset.insertedRows);
-            _updateRows(changeset.updatedRows);
+            _rowNotifier.deleteRows(changeset.deletedRows);
+            _rowNotifier.insertRows(changeset.insertedRows);
+            _rowNotifier.updateRows(changeset.updatedRows);
           }
         },
         (err) => Log.error(err),
@@ -63,13 +63,15 @@ class GridRowCache {
     bool Function()? listenWhen,
   }) {
     _rowNotifier.addListener(() {
-      if (listenWhen != null && listenWhen() == false) {
+      if (onChanged == null) {
         return;
       }
 
-      if (onChanged != null) {
-        onChanged(clonedRows, _rowNotifier._changeReason);
+      if (listenWhen != null && listenWhen() == false) {
+        return;
       }
+
+      onChanged(clonedRows, _rowNotifier._changeReason);
     });
   }
 
@@ -136,18 +138,6 @@ class GridRowCache {
     final rowOrders = blocks.expand((block) => block.rowOrders).toList();
     _rowNotifier.reset(rowOrders);
   }
-
-  void _deleteRows(List<RowOrder> deletedRows) {
-    _rowNotifier.deleteRows(deletedRows);
-  }
-
-  void _insertRows(List<IndexRowOrder> createdRows) {
-    _rowNotifier.insertRows(createdRows);
-  }
-
-  void _updateRows(List<RowOrder> rowOrders) {
-    _rowNotifier.updateRows(rowOrders);
-  }
 }
 
 class RowsNotifier extends ChangeNotifier {
@@ -173,7 +163,7 @@ class RowsNotifier extends ChangeNotifier {
 
     final List<GridRow> newRows = [];
     final DeletedIndexs deletedIndex = [];
-    final Map<String, RowOrder> deletedRowMap = {for (var rowOrder in deletedRows) rowOrder.rowId: rowOrder};
+    final Map<String, RowOrder> deletedRowMap = {for (var e in deletedRows) e.rowId: e};
 
     _rows.asMap().forEach((index, row) {
       if (deletedRowMap[row.rowId] == null) {
@@ -192,12 +182,14 @@ class RowsNotifier extends ChangeNotifier {
     }
 
     InsertedIndexs insertIndexs = [];
-    final List<GridRow> newRows = _rows;
+    final List<GridRow> newRows = clonedRows;
     for (final createdRow in createdRows) {
-      final rowOrder = createdRow.rowOrder;
-      final insertIndex = InsertedIndex(index: createdRow.index, rowId: rowOrder.rowId);
+      final insertIndex = InsertedIndex(
+        index: createdRow.index,
+        rowId: createdRow.rowOrder.rowId,
+      );
       insertIndexs.add(insertIndex);
-      newRows.insert(createdRow.index, (rowBuilder(rowOrder)));
+      newRows.insert(createdRow.index, (rowBuilder(createdRow.rowOrder)));
     }
     _update(newRows, GridRowChangeReason.insert(insertIndexs));
   }
@@ -208,15 +200,15 @@ class RowsNotifier extends ChangeNotifier {
     }
 
     final UpdatedIndexs updatedIndexs = UpdatedIndexs();
-    final List<GridRow> newRows = _rows;
+    final List<GridRow> newRows = clonedRows;
     for (final rowOrder in updatedRows) {
       final index = newRows.indexWhere((row) => row.rowId == rowOrder.rowId);
       if (index != -1) {
-        newRows.removeAt(index);
         // Remove the old row data, the data will be filled if the loadRow method gets called.
         _rowDataMap.remove(rowOrder.rowId);
-        newRows.insert(index, rowBuilder(rowOrder));
 
+        newRows.removeAt(index);
+        newRows.insert(index, rowBuilder(rowOrder));
         updatedIndexs[rowOrder.rowId] = UpdatedIndex(index: index, rowId: rowOrder.rowId);
       }
     }

+ 2 - 4
frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/grid_page.dart

@@ -231,10 +231,8 @@ class _GridRowsState extends State<_GridRows> {
     return SizeTransition(
       sizeFactor: animation,
       child: GridRowWidget(
-        blocBuilder: () => RowBloc(
-          rowData: rowData,
-          rowCache: rowCache,
-        ),
+        rowData: rowData,
+        rowCache: rowCache,
         key: ValueKey(rowData.rowId),
       ),
     );

+ 54 - 6
frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/cell_container.dart

@@ -6,6 +6,7 @@ import 'package:app_flowy/workspace/presentation/plugins/grid/src/layout/sizes.d
 
 class CellStateNotifier extends ChangeNotifier {
   bool _isFocus = false;
+  bool _onEnter = false;
 
   set isFocus(bool value) {
     if (_isFocus != value) {
@@ -14,38 +15,56 @@ class CellStateNotifier extends ChangeNotifier {
     }
   }
 
+  set onEnter(bool value) {
+    if (_onEnter != value) {
+      _onEnter = value;
+      notifyListeners();
+    }
+  }
+
   bool get isFocus => _isFocus;
+
+  bool get onEnter => _onEnter;
 }
 
 class CellContainer extends StatelessWidget {
   final Widget child;
+  final Widget? expander;
   final double width;
   const CellContainer({
     Key? key,
     required this.child,
     required this.width,
+    this.expander,
   }) : super(key: key);
 
   @override
   Widget build(BuildContext context) {
     return ChangeNotifierProvider(
       create: (_) => CellStateNotifier(),
-      child: Consumer<CellStateNotifier>(
-        builder: (context, state, _) {
+      child: Selector<CellStateNotifier, bool>(
+        selector: (context, notifier) => notifier.isFocus,
+        builder: (context, isFocus, _) {
+          Widget container = Center(child: child);
+
+          if (expander != null) {
+            container = _CellEnterRegion(child: container, expander: expander!);
+          }
+
           return Container(
             constraints: BoxConstraints(maxWidth: width),
-            decoration: _makeBoxDecoration(context, state),
+            decoration: _makeBoxDecoration(context, isFocus),
             padding: GridSize.cellContentInsets,
-            child: Center(child: child),
+            child: container,
           );
         },
       ),
     );
   }
 
-  BoxDecoration _makeBoxDecoration(BuildContext context, CellStateNotifier state) {
+  BoxDecoration _makeBoxDecoration(BuildContext context, bool isFocus) {
     final theme = context.watch<AppTheme>();
-    if (state.isFocus) {
+    if (isFocus) {
       final borderSide = BorderSide(color: theme.main1, width: 1.0);
       return BoxDecoration(border: Border.fromBorderSide(borderSide));
     } else {
@@ -55,6 +74,35 @@ class CellContainer extends StatelessWidget {
   }
 }
 
+class _CellEnterRegion extends StatelessWidget {
+  final Widget expander;
+  final Widget child;
+  const _CellEnterRegion({required this.expander, required this.child, Key? key}) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    return Selector<CellStateNotifier, bool>(
+      selector: (context, notifier) => notifier.onEnter,
+      builder: (context, onEnter, _) {
+        List<Widget> children = [child];
+        if (onEnter) {
+          children.add(expander);
+        }
+
+        return MouseRegion(
+          cursor: SystemMouseCursors.click,
+          onEnter: (p) => Provider.of<CellStateNotifier>(context, listen: false).onEnter = true,
+          onExit: (p) => Provider.of<CellStateNotifier>(context, listen: false).onEnter = false,
+          child: Stack(
+            alignment: AlignmentDirectional.centerEnd,
+            children: children,
+          ),
+        );
+      },
+    );
+  }
+}
+
 abstract class GridCellWidget extends StatefulWidget {
   const GridCellWidget({Key? key}) : super(key: key);
 

+ 30 - 14
frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/header/field_cell_action_sheet.dart

@@ -90,16 +90,6 @@ class _FieldOperationList extends StatelessWidget {
 
   @override
   Widget build(BuildContext context) {
-    final actions = FieldAction.values
-        .map(
-          (action) => FieldActionCell(
-            fieldId: fieldData.field.id,
-            action: action,
-            onTap: onDismissed,
-          ),
-        )
-        .toList();
-
     return GridView(
       // https://api.flutter.dev/flutter/widgets/AnimatedList/shrinkWrap.html
       shrinkWrap: true,
@@ -108,20 +98,44 @@ class _FieldOperationList extends StatelessWidget {
         childAspectRatio: 4.0,
         mainAxisSpacing: 8,
       ),
-      children: actions,
+      children: buildCells(),
     );
   }
+
+  List<Widget> buildCells() {
+    return FieldAction.values.map(
+      (action) {
+        bool enable = true;
+        switch (action) {
+          case FieldAction.delete:
+            enable = !fieldData.field.isPrimary;
+            break;
+          default:
+            break;
+        }
+
+        return FieldActionCell(
+          fieldId: fieldData.field.id,
+          action: action,
+          onTap: onDismissed,
+          enable: enable,
+        );
+      },
+    ).toList();
+  }
 }
 
 class FieldActionCell extends StatelessWidget {
   final String fieldId;
   final VoidCallback onTap;
   final FieldAction action;
+  final bool enable;
 
   const FieldActionCell({
     required this.fieldId,
     required this.action,
     required this.onTap,
+    required this.enable,
     Key? key,
   }) : super(key: key);
 
@@ -129,11 +143,13 @@ class FieldActionCell extends StatelessWidget {
   Widget build(BuildContext context) {
     final theme = context.watch<AppTheme>();
     return FlowyButton(
-      text: FlowyText.medium(action.title(), fontSize: 12),
+      text: FlowyText.medium(action.title(), fontSize: 12, color: enable ? null : theme.shader4),
       hoverColor: theme.hover,
       onTap: () {
-        action.run(context);
-        onTap();
+        if (enable) {
+          action.run(context);
+          onTap();
+        }
       },
       leftIcon: svgWidget(action.iconName(), color: theme.iconColor),
     );

+ 109 - 41
frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/row/grid_row.dart

@@ -8,12 +8,17 @@ import 'package:flutter/material.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 import 'package:provider/provider.dart';
 import 'row_action_sheet.dart';
+import 'package:dartz/dartz.dart' show Option;
+
+import 'row_detail.dart';
 
 class GridRowWidget extends StatefulWidget {
-  final RowBloc Function() blocBuilder;
+  final GridRow rowData;
+  final GridRowCache rowCache;
 
   const GridRowWidget({
-    required this.blocBuilder,
+    required this.rowData,
+    required this.rowCache,
     Key? key,
   }) : super(key: key);
 
@@ -23,13 +28,14 @@ class GridRowWidget extends StatefulWidget {
 
 class _GridRowWidgetState extends State<GridRowWidget> {
   late RowBloc _rowBloc;
-  late _RegionStateNotifier _rowStateNotifier;
 
   @override
   void initState() {
-    _rowBloc = widget.blocBuilder();
+    _rowBloc = RowBloc(
+      rowData: widget.rowData,
+      rowCache: widget.rowCache,
+    );
     _rowBloc.add(const RowEvent.initial());
-    _rowStateNotifier = _RegionStateNotifier();
     super.initState();
   }
 
@@ -37,29 +43,24 @@ class _GridRowWidgetState extends State<GridRowWidget> {
   Widget build(BuildContext context) {
     return BlocProvider.value(
       value: _rowBloc,
-      child: ChangeNotifierProvider.value(
-        value: _rowStateNotifier,
-        child: MouseRegion(
-          cursor: SystemMouseCursors.click,
-          onEnter: (p) => _rowStateNotifier.onEnter = true,
-          onExit: (p) => _rowStateNotifier.onEnter = false,
-          child: BlocBuilder<RowBloc, RowState>(
-            buildWhen: (p, c) => p.rowData.height != c.rowData.height,
-            builder: (context, state) {
-              return SizedBox(
-                height: 42,
-                child: Row(
-                  mainAxisSize: MainAxisSize.max,
-                  crossAxisAlignment: CrossAxisAlignment.center,
-                  children: const [
-                    _RowLeading(),
-                    _RowCells(),
-                    _RowTrailing(),
-                  ],
-                ),
-              );
-            },
-          ),
+      child: _RowEnterRegion(
+        child: BlocBuilder<RowBloc, RowState>(
+          buildWhen: (p, c) => p.rowData.height != c.rowData.height,
+          builder: (context, state) {
+            final children = [
+              const _RowLeading(),
+              _RowCells(onExpand: () => onExpandCell(context)),
+              const _RowTrailing(),
+            ];
+
+            final child = Row(
+              mainAxisSize: MainAxisSize.max,
+              crossAxisAlignment: CrossAxisAlignment.center,
+              children: children,
+            );
+
+            return SizedBox(height: 42, child: child);
+          },
         ),
       ),
     );
@@ -68,9 +69,13 @@ class _GridRowWidgetState extends State<GridRowWidget> {
   @override
   Future<void> dispose() async {
     _rowBloc.close();
-    _rowStateNotifier.dispose();
     super.dispose();
   }
+
+  void onExpandCell(BuildContext context) {
+    final page = RowDetailPage(rowData: widget.rowData, rowCache: widget.rowCache);
+    page.show(context);
+  }
 }
 
 class _RowLeading extends StatelessWidget {
@@ -142,32 +147,41 @@ class _DeleteRowButton extends StatelessWidget {
 }
 
 class _RowCells extends StatelessWidget {
-  const _RowCells({Key? key}) : super(key: key);
+  final VoidCallback onExpand;
+  const _RowCells({required this.onExpand, Key? key}) : super(key: key);
 
   @override
   Widget build(BuildContext context) {
     return BlocBuilder<RowBloc, RowState>(
       buildWhen: (previous, current) => previous.cellDataMap != current.cellDataMap,
       builder: (context, state) {
-        final List<Widget> children = state.cellDataMap.fold(() => [], _toCells);
         return Row(
           mainAxisSize: MainAxisSize.min,
           mainAxisAlignment: MainAxisAlignment.center,
-          children: children,
+          children: _makeCells(state.cellDataMap),
         );
       },
     );
   }
 
-  List<Widget> _toCells(CellDataMap dataMap) {
-    return dataMap.values.map(
-      (cellData) {
-        return CellContainer(
-          width: cellData.field.width.toDouble(),
-          child: buildGridCell(cellData),
-        );
-      },
-    ).toList();
+  List<Widget> _makeCells(Option<CellDataMap> data) {
+    return data.fold(
+      () => [],
+      (cellDataMap) => cellDataMap.values.map(
+        (cellData) {
+          Widget? expander;
+          if (cellData.field.isPrimary) {
+            expander = _CellExpander(onExpand: onExpand);
+          }
+
+          return CellContainer(
+            width: cellData.field.width.toDouble(),
+            child: buildGridCell(cellData),
+            expander: expander,
+          );
+        },
+      ).toList(),
+    );
   }
 }
 
@@ -183,3 +197,57 @@ class _RegionStateNotifier extends ChangeNotifier {
 
   bool get onEnter => _onEnter;
 }
+
+class _CellExpander extends StatelessWidget {
+  final VoidCallback onExpand;
+  const _CellExpander({required this.onExpand, Key? key}) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    final theme = context.watch<AppTheme>();
+    return FlowyIconButton(
+      width: 20,
+      onPressed: onExpand,
+      iconPadding: const EdgeInsets.fromLTRB(2, 2, 2, 2),
+      icon: svgWidget("grid/expander", color: theme.main1),
+    );
+  }
+}
+
+class _RowEnterRegion extends StatefulWidget {
+  final Widget child;
+  const _RowEnterRegion({required this.child, Key? key}) : super(key: key);
+
+  @override
+  State<_RowEnterRegion> createState() => _RowEnterRegionState();
+}
+
+class _RowEnterRegionState extends State<_RowEnterRegion> {
+  late _RegionStateNotifier _rowStateNotifier;
+
+  @override
+  void initState() {
+    _rowStateNotifier = _RegionStateNotifier();
+    super.initState();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return ChangeNotifierProvider.value(
+      value: _rowStateNotifier,
+      child: MouseRegion(
+        cursor: SystemMouseCursors.click,
+        onEnter: (p) => _rowStateNotifier.onEnter = true,
+        onExit: (p) => _rowStateNotifier.onEnter = false,
+        child: widget.child,
+      ),
+    );
+    ;
+  }
+
+  @override
+  Future<void> dispose() async {
+    _rowStateNotifier.dispose();
+    super.dispose();
+  }
+}

+ 48 - 0
frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/row/row_detail.dart

@@ -0,0 +1,48 @@
+import 'package:app_flowy/workspace/application/grid/row/row_detail_bloc.dart';
+import 'package:app_flowy/workspace/application/grid/row/row_service.dart';
+import 'package:flowy_infra_ui/flowy_infra_ui.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:window_size/window_size.dart';
+
+class RowDetailPage extends StatelessWidget with FlowyOverlayDelegate {
+  final GridRow rowData;
+  final GridRowCache rowCache;
+
+  const RowDetailPage({
+    required this.rowData,
+    required this.rowCache,
+    Key? key,
+  }) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    return BlocProvider(
+      create: (context) => RowDetailBloc(rowData: rowData, rowCache: rowCache),
+      child: Container(),
+    );
+  }
+
+  void show(BuildContext context) async {
+    FlowyOverlay.of(context).remove(identifier());
+
+    const size = Size(460, 400);
+    final window = await getWindowInfo();
+    FlowyOverlay.of(context).insertWithRect(
+      widget: OverlayContainer(
+        child: this,
+        constraints: BoxConstraints.tight(const Size(460, 400)),
+      ),
+      identifier: identifier(),
+      anchorPosition: Offset(-size.width / 2.0, -size.height / 2.0),
+      anchorSize: window.frame.size,
+      anchorDirection: AnchorDirection.center,
+      style: FlowyOverlayStyle(blur: false),
+      delegate: this,
+    );
+  }
+
+  static String identifier() {
+    return (RowDetailPage).toString();
+  }
+}

+ 1 - 1
frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/toolbar/grid_property.dart

@@ -67,7 +67,7 @@ class GridPropertyList extends StatelessWidget with FlowyOverlayDelegate {
   }
 
   String identifier() {
-    return toString();
+    return (GridPropertyList).toString();
   }
 
   @override

+ 14 - 0
frontend/app_flowy/packages/flowy_sdk/lib/protobuf/flowy-grid-data-model/grid.pb.dart

@@ -85,6 +85,7 @@ class Field extends $pb.GeneratedMessage {
     ..aOB(5, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'frozen')
     ..aOB(6, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'visibility')
     ..a<$core.int>(7, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'width', $pb.PbFieldType.O3)
+    ..aOB(8, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'isPrimary')
     ..hasRequiredFields = false
   ;
 
@@ -97,6 +98,7 @@ class Field extends $pb.GeneratedMessage {
     $core.bool? frozen,
     $core.bool? visibility,
     $core.int? width,
+    $core.bool? isPrimary,
   }) {
     final _result = create();
     if (id != null) {
@@ -120,6 +122,9 @@ class Field extends $pb.GeneratedMessage {
     if (width != null) {
       _result.width = width;
     }
+    if (isPrimary != null) {
+      _result.isPrimary = isPrimary;
+    }
     return _result;
   }
   factory Field.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
@@ -205,6 +210,15 @@ class Field extends $pb.GeneratedMessage {
   $core.bool hasWidth() => $_has(6);
   @$pb.TagNumber(7)
   void clearWidth() => clearField(7);
+
+  @$pb.TagNumber(8)
+  $core.bool get isPrimary => $_getBF(7);
+  @$pb.TagNumber(8)
+  set isPrimary($core.bool v) { $_setBool(7, v); }
+  @$pb.TagNumber(8)
+  $core.bool hasIsPrimary() => $_has(7);
+  @$pb.TagNumber(8)
+  void clearIsPrimary() => clearField(8);
 }
 
 class FieldOrder extends $pb.GeneratedMessage {

+ 2 - 1
frontend/app_flowy/packages/flowy_sdk/lib/protobuf/flowy-grid-data-model/grid.pbjson.dart

@@ -57,11 +57,12 @@ const Field$json = const {
     const {'1': 'frozen', '3': 5, '4': 1, '5': 8, '10': 'frozen'},
     const {'1': 'visibility', '3': 6, '4': 1, '5': 8, '10': 'visibility'},
     const {'1': 'width', '3': 7, '4': 1, '5': 5, '10': 'width'},
+    const {'1': 'is_primary', '3': 8, '4': 1, '5': 8, '10': 'isPrimary'},
   ],
 };
 
 /// Descriptor for `Field`. Decode as a `google.protobuf.DescriptorProto`.
-final $typed_data.Uint8List fieldDescriptor = $convert.base64Decode('CgVGaWVsZBIOCgJpZBgBIAEoCVICaWQSEgoEbmFtZRgCIAEoCVIEbmFtZRISCgRkZXNjGAMgASgJUgRkZXNjEikKCmZpZWxkX3R5cGUYBCABKA4yCi5GaWVsZFR5cGVSCWZpZWxkVHlwZRIWCgZmcm96ZW4YBSABKAhSBmZyb3plbhIeCgp2aXNpYmlsaXR5GAYgASgIUgp2aXNpYmlsaXR5EhQKBXdpZHRoGAcgASgFUgV3aWR0aA==');
+final $typed_data.Uint8List fieldDescriptor = $convert.base64Decode('CgVGaWVsZBIOCgJpZBgBIAEoCVICaWQSEgoEbmFtZRgCIAEoCVIEbmFtZRISCgRkZXNjGAMgASgJUgRkZXNjEikKCmZpZWxkX3R5cGUYBCABKA4yCi5GaWVsZFR5cGVSCWZpZWxkVHlwZRIWCgZmcm96ZW4YBSABKAhSBmZyb3plbhIeCgp2aXNpYmlsaXR5GAYgASgIUgp2aXNpYmlsaXR5EhQKBXdpZHRoGAcgASgFUgV3aWR0aBIdCgppc19wcmltYXJ5GAggASgIUglpc1ByaW1hcnk=');
 @$core.Deprecated('Use fieldOrderDescriptor instead')
 const FieldOrder$json = const {
   '1': 'FieldOrder',

+ 7 - 1
frontend/rust-lib/flowy-grid/src/services/field/field_builder.rs

@@ -13,7 +13,7 @@ pub type BoxTypeOptionBuilder = Box<dyn TypeOptionBuilder + 'static>;
 impl FieldBuilder {
     pub fn new<T: Into<BoxTypeOptionBuilder>>(type_option_builder: T) -> Self {
         let type_option_builder = type_option_builder.into();
-        let field_meta = FieldMeta::new("", "", type_option_builder.field_type());
+        let field_meta = FieldMeta::new("", "", type_option_builder.field_type(), false);
         Self {
             field_meta,
             type_option_builder,
@@ -35,6 +35,7 @@ impl FieldBuilder {
             visibility: field.visibility,
             width: field.width,
             type_options: IndexMap::default(),
+            is_primary: field.is_primary,
         };
         Self {
             field_meta,
@@ -52,6 +53,11 @@ impl FieldBuilder {
         self
     }
 
+    pub fn primary(mut self, is_primary: bool) -> Self {
+        self.field_meta.is_primary = is_primary;
+        self
+    }
+
     pub fn visibility(mut self, visibility: bool) -> Self {
         self.field_meta.visibility = visibility;
         self

+ 2 - 3
frontend/rust-lib/flowy-grid/src/services/grid_editor.rs

@@ -1,10 +1,9 @@
 use crate::dart_notification::{send_dart_notification, GridNotification};
 use crate::manager::GridUser;
 use crate::services::block_meta_manager::GridBlockMetaEditorManager;
-use crate::services::entities::{CellIdentifier, CreateSelectOptionParams};
+use crate::services::entities::CellIdentifier;
 use crate::services::field::{
-    default_type_option_builder_from_type, select_option_operation, type_option_builder_from_bytes, FieldBuilder,
-    SelectOption,
+    default_type_option_builder_from_type, type_option_builder_from_bytes, FieldBuilder, SelectOption,
 };
 use crate::services::persistence::block_index::BlockIndexPersistence;
 use crate::services::row::*;

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

@@ -7,6 +7,7 @@ pub fn make_default_grid() -> BuildGridContext {
     let text_field = FieldBuilder::new(RichTextTypeOptionBuilder::default())
         .name("Name")
         .visibility(true)
+        .primary(true)
         .build();
 
     // single select

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

@@ -271,6 +271,7 @@ pub fn create_text_field(grid_id: &str) -> (InsertFieldParams, FieldMeta) {
         frozen: field_meta.frozen,
         visibility: field_meta.visibility,
         width: field_meta.width,
+        is_primary: false,
     };
 
     let params = InsertFieldParams {
@@ -303,6 +304,7 @@ pub fn create_single_select_field(grid_id: &str) -> (InsertFieldParams, FieldMet
         frozen: field_meta.frozen,
         visibility: field_meta.visibility,
         width: field_meta.width,
+        is_primary: false,
     };
 
     let params = InsertFieldParams {

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

@@ -42,6 +42,9 @@ pub struct Field {
 
     #[pb(index = 7)]
     pub width: i32,
+
+    #[pb(index = 8)]
+    pub is_primary: bool,
 }
 
 impl std::convert::From<FieldMeta> for Field {
@@ -54,6 +57,7 @@ impl std::convert::From<FieldMeta> for Field {
             frozen: field_meta.frozen,
             visibility: field_meta.visibility,
             width: field_meta.width,
+            is_primary: field_meta.is_primary,
         }
     }
 }

+ 11 - 2
shared-lib/flowy-grid-data-model/src/entities/meta.rs

@@ -98,13 +98,21 @@ pub struct FieldMeta {
     // #[pb(index = 8)]
     /// type_options contains key/value pairs
     /// key: id of the FieldType
-    /// value: type option data string
+    /// value: type option data that can be parsed into specified TypeOptionStruct.
+    /// For example, CheckboxTypeOption, MultiSelectTypeOption etc.
     #[serde(with = "indexmap::serde_seq")]
     pub type_options: IndexMap<String, String>,
+
+    #[serde(default = "default_is_primary")]
+    pub is_primary: bool,
+}
+
+fn default_is_primary() -> bool {
+    false
 }
 
 impl FieldMeta {
-    pub fn new(name: &str, desc: &str, field_type: FieldType) -> Self {
+    pub fn new(name: &str, desc: &str, field_type: FieldType, is_primary: bool) -> Self {
         let width = field_type.default_cell_width();
         Self {
             id: gen_field_id(),
@@ -115,6 +123,7 @@ impl FieldMeta {
             visibility: true,
             width,
             type_options: Default::default(),
+            is_primary,
         }
     }
 

+ 119 - 83
shared-lib/flowy-grid-data-model/src/protobuf/model/grid.rs

@@ -290,6 +290,7 @@ pub struct Field {
     pub frozen: bool,
     pub visibility: bool,
     pub width: i32,
+    pub is_primary: bool,
     // special fields
     pub unknown_fields: ::protobuf::UnknownFields,
     pub cached_size: ::protobuf::CachedSize,
@@ -443,6 +444,21 @@ impl Field {
     pub fn set_width(&mut self, v: i32) {
         self.width = v;
     }
+
+    // bool is_primary = 8;
+
+
+    pub fn get_is_primary(&self) -> bool {
+        self.is_primary
+    }
+    pub fn clear_is_primary(&mut self) {
+        self.is_primary = false;
+    }
+
+    // Param is passed by value, moved
+    pub fn set_is_primary(&mut self, v: bool) {
+        self.is_primary = v;
+    }
 }
 
 impl ::protobuf::Message for Field {
@@ -487,6 +503,13 @@ impl ::protobuf::Message for Field {
                     let tmp = is.read_int32()?;
                     self.width = tmp;
                 },
+                8 => {
+                    if wire_type != ::protobuf::wire_format::WireTypeVarint {
+                        return ::std::result::Result::Err(::protobuf::rt::unexpected_wire_type(wire_type));
+                    }
+                    let tmp = is.read_bool()?;
+                    self.is_primary = tmp;
+                },
                 _ => {
                     ::protobuf::rt::read_unknown_or_skip_group(field_number, wire_type, is, self.mut_unknown_fields())?;
                 },
@@ -520,6 +543,9 @@ impl ::protobuf::Message for Field {
         if self.width != 0 {
             my_size += ::protobuf::rt::value_size(7, self.width, ::protobuf::wire_format::WireTypeVarint);
         }
+        if self.is_primary != false {
+            my_size += 2;
+        }
         my_size += ::protobuf::rt::unknown_fields_size(self.get_unknown_fields());
         self.cached_size.set(my_size);
         my_size
@@ -547,6 +573,9 @@ impl ::protobuf::Message for Field {
         if self.width != 0 {
             os.write_int32(7, self.width)?;
         }
+        if self.is_primary != false {
+            os.write_bool(8, self.is_primary)?;
+        }
         os.write_unknown_fields(self.get_unknown_fields())?;
         ::std::result::Result::Ok(())
     }
@@ -620,6 +649,11 @@ impl ::protobuf::Message for Field {
                 |m: &Field| { &m.width },
                 |m: &mut Field| { &mut m.width },
             ));
+            fields.push(::protobuf::reflect::accessor::make_simple_field_accessor::<_, ::protobuf::types::ProtobufTypeBool>(
+                "is_primary",
+                |m: &Field| { &m.is_primary },
+                |m: &mut Field| { &mut m.is_primary },
+            ));
             ::protobuf::reflect::MessageDescriptor::new_pb_name::<Field>(
                 "Field",
                 fields,
@@ -643,6 +677,7 @@ impl ::protobuf::Clear for Field {
         self.frozen = false;
         self.visibility = false;
         self.width = 0;
+        self.is_primary = false;
         self.unknown_fields.clear();
     }
 }
@@ -7770,93 +7805,94 @@ static file_descriptor_proto_data: &'static [u8] = b"\
     \n\ngrid.proto\"z\n\x04Grid\x12\x0e\n\x02id\x18\x01\x20\x01(\tR\x02id\
     \x12.\n\x0cfield_orders\x18\x02\x20\x03(\x0b2\x0b.FieldOrderR\x0bfieldOr\
     ders\x122\n\x0cblock_orders\x18\x03\x20\x03(\x0b2\x0f.GridBlockOrderR\
-    \x0bblockOrders\"\xb8\x01\n\x05Field\x12\x0e\n\x02id\x18\x01\x20\x01(\tR\
+    \x0bblockOrders\"\xd7\x01\n\x05Field\x12\x0e\n\x02id\x18\x01\x20\x01(\tR\
     \x02id\x12\x12\n\x04name\x18\x02\x20\x01(\tR\x04name\x12\x12\n\x04desc\
     \x18\x03\x20\x01(\tR\x04desc\x12)\n\nfield_type\x18\x04\x20\x01(\x0e2\n.\
     FieldTypeR\tfieldType\x12\x16\n\x06frozen\x18\x05\x20\x01(\x08R\x06froze\
     n\x12\x1e\n\nvisibility\x18\x06\x20\x01(\x08R\nvisibility\x12\x14\n\x05w\
-    idth\x18\x07\x20\x01(\x05R\x05width\"'\n\nFieldOrder\x12\x19\n\x08field_\
-    id\x18\x01\x20\x01(\tR\x07fieldId\"\xc6\x01\n\x12GridFieldChangeset\x12\
-    \x17\n\x07grid_id\x18\x01\x20\x01(\tR\x06gridId\x124\n\x0finserted_field\
-    s\x18\x02\x20\x03(\x0b2\x0b.IndexFieldR\x0einsertedFields\x122\n\x0edele\
-    ted_fields\x18\x03\x20\x03(\x0b2\x0b.FieldOrderR\rdeletedFields\x12-\n\
-    \x0eupdated_fields\x18\x04\x20\x03(\x0b2\x06.FieldR\rupdatedFields\"@\n\
-    \nIndexField\x12\x1c\n\x05field\x18\x01\x20\x01(\x0b2\x06.FieldR\x05fiel\
-    d\x12\x14\n\x05index\x18\x02\x20\x01(\x05R\x05index\"\x90\x01\n\x1aGetEd\
-    itFieldContextPayload\x12\x17\n\x07grid_id\x18\x01\x20\x01(\tR\x06gridId\
-    \x12\x1b\n\x08field_id\x18\x02\x20\x01(\tH\0R\x07fieldId\x12)\n\nfield_t\
-    ype\x18\x03\x20\x01(\x0e2\n.FieldTypeR\tfieldTypeB\x11\n\x0fone_of_field\
-    _id\"q\n\x10EditFieldPayload\x12\x17\n\x07grid_id\x18\x01\x20\x01(\tR\
-    \x06gridId\x12\x19\n\x08field_id\x18\x02\x20\x01(\tR\x07fieldId\x12)\n\n\
-    field_type\x18\x03\x20\x01(\x0e2\n.FieldTypeR\tfieldType\"|\n\x10EditFie\
-    ldContext\x12\x17\n\x07grid_id\x18\x01\x20\x01(\tR\x06gridId\x12%\n\ngri\
-    d_field\x18\x02\x20\x01(\x0b2\x06.FieldR\tgridField\x12(\n\x10type_optio\
-    n_data\x18\x03\x20\x01(\x0cR\x0etypeOptionData\"-\n\rRepeatedField\x12\
-    \x1c\n\x05items\x18\x01\x20\x03(\x0b2\x06.FieldR\x05items\"7\n\x12Repeat\
-    edFieldOrder\x12!\n\x05items\x18\x01\x20\x03(\x0b2\x0b.FieldOrderR\x05it\
-    ems\"T\n\x08RowOrder\x12\x15\n\x06row_id\x18\x01\x20\x01(\tR\x05rowId\
-    \x12\x19\n\x08block_id\x18\x02\x20\x01(\tR\x07blockId\x12\x16\n\x06heigh\
-    t\x18\x03\x20\x01(\x05R\x06height\"\xb8\x01\n\x03Row\x12\x0e\n\x02id\x18\
-    \x01\x20\x01(\tR\x02id\x12@\n\x10cell_by_field_id\x18\x02\x20\x03(\x0b2\
-    \x17.Row.CellByFieldIdEntryR\rcellByFieldId\x12\x16\n\x06height\x18\x03\
-    \x20\x01(\x05R\x06height\x1aG\n\x12CellByFieldIdEntry\x12\x10\n\x03key\
-    \x18\x01\x20\x01(\tR\x03key\x12\x1b\n\x05value\x18\x02\x20\x01(\x0b2\x05\
-    .CellR\x05value:\x028\x01\")\n\x0bRepeatedRow\x12\x1a\n\x05items\x18\x01\
-    \x20\x03(\x0b2\x04.RowR\x05items\"5\n\x11RepeatedGridBlock\x12\x20\n\x05\
-    items\x18\x01\x20\x03(\x0b2\n.GridBlockR\x05items\"U\n\x0eGridBlockOrder\
-    \x12\x19\n\x08block_id\x18\x01\x20\x01(\tR\x07blockId\x12(\n\nrow_orders\
-    \x18\x02\x20\x03(\x0b2\t.RowOrderR\trowOrders\"_\n\rIndexRowOrder\x12&\n\
-    \trow_order\x18\x01\x20\x01(\x0b2\t.RowOrderR\x08rowOrder\x12\x16\n\x05i\
-    ndex\x18\x02\x20\x01(\x05H\0R\x05indexB\x0e\n\x0cone_of_index\"\xbf\x01\
-    \n\x11GridRowsChangeset\x12\x19\n\x08block_id\x18\x01\x20\x01(\tR\x07blo\
-    ckId\x123\n\rinserted_rows\x18\x02\x20\x03(\x0b2\x0e.IndexRowOrderR\x0ci\
-    nsertedRows\x12,\n\x0cdeleted_rows\x18\x03\x20\x03(\x0b2\t.RowOrderR\x0b\
-    deletedRows\x12,\n\x0cupdated_rows\x18\x04\x20\x03(\x0b2\t.RowOrderR\x0b\
-    updatedRows\"E\n\tGridBlock\x12\x0e\n\x02id\x18\x01\x20\x01(\tR\x02id\
-    \x12(\n\nrow_orders\x18\x02\x20\x03(\x0b2\t.RowOrderR\trowOrders\";\n\
-    \x04Cell\x12\x19\n\x08field_id\x18\x01\x20\x01(\tR\x07fieldId\x12\x18\n\
-    \x07content\x18\x02\x20\x01(\tR\x07content\"\x8f\x01\n\x14CellNotificati\
-    onData\x12\x17\n\x07grid_id\x18\x01\x20\x01(\tR\x06gridId\x12\x19\n\x08f\
-    ield_id\x18\x02\x20\x01(\tR\x07fieldId\x12\x15\n\x06row_id\x18\x03\x20\
-    \x01(\tR\x05rowId\x12\x1a\n\x07content\x18\x04\x20\x01(\tH\0R\x07content\
-    B\x10\n\x0eone_of_content\"+\n\x0cRepeatedCell\x12\x1b\n\x05items\x18\
-    \x01\x20\x03(\x0b2\x05.CellR\x05items\"'\n\x11CreateGridPayload\x12\x12\
-    \n\x04name\x18\x01\x20\x01(\tR\x04name\"\x1e\n\x06GridId\x12\x14\n\x05va\
-    lue\x18\x01\x20\x01(\tR\x05value\"#\n\x0bGridBlockId\x12\x14\n\x05value\
-    \x18\x01\x20\x01(\tR\x05value\"f\n\x10CreateRowPayload\x12\x17\n\x07grid\
-    _id\x18\x01\x20\x01(\tR\x06gridId\x12\"\n\x0cstart_row_id\x18\x02\x20\
-    \x01(\tH\0R\nstartRowIdB\x15\n\x13one_of_start_row_id\"\xb6\x01\n\x12Ins\
-    ertFieldPayload\x12\x17\n\x07grid_id\x18\x01\x20\x01(\tR\x06gridId\x12\
-    \x1c\n\x05field\x18\x02\x20\x01(\x0b2\x06.FieldR\x05field\x12(\n\x10type\
-    _option_data\x18\x03\x20\x01(\x0cR\x0etypeOptionData\x12&\n\x0estart_fie\
-    ld_id\x18\x04\x20\x01(\tH\0R\x0cstartFieldIdB\x17\n\x15one_of_start_fiel\
-    d_id\"d\n\x11QueryFieldPayload\x12\x17\n\x07grid_id\x18\x01\x20\x01(\tR\
-    \x06gridId\x126\n\x0cfield_orders\x18\x02\x20\x01(\x0b2\x13.RepeatedFiel\
-    dOrderR\x0bfieldOrders\"e\n\x16QueryGridBlocksPayload\x12\x17\n\x07grid_\
-    id\x18\x01\x20\x01(\tR\x06gridId\x122\n\x0cblock_orders\x18\x02\x20\x03(\
-    \x0b2\x0f.GridBlockOrderR\x0bblockOrders\"\xa8\x03\n\x15FieldChangesetPa\
-    yload\x12\x19\n\x08field_id\x18\x01\x20\x01(\tR\x07fieldId\x12\x17\n\x07\
-    grid_id\x18\x02\x20\x01(\tR\x06gridId\x12\x14\n\x04name\x18\x03\x20\x01(\
-    \tH\0R\x04name\x12\x14\n\x04desc\x18\x04\x20\x01(\tH\x01R\x04desc\x12+\n\
-    \nfield_type\x18\x05\x20\x01(\x0e2\n.FieldTypeH\x02R\tfieldType\x12\x18\
-    \n\x06frozen\x18\x06\x20\x01(\x08H\x03R\x06frozen\x12\x20\n\nvisibility\
-    \x18\x07\x20\x01(\x08H\x04R\nvisibility\x12\x16\n\x05width\x18\x08\x20\
-    \x01(\x05H\x05R\x05width\x12*\n\x10type_option_data\x18\t\x20\x01(\x0cH\
-    \x06R\x0etypeOptionDataB\r\n\x0bone_of_nameB\r\n\x0bone_of_descB\x13\n\
-    \x11one_of_field_typeB\x0f\n\rone_of_frozenB\x13\n\x11one_of_visibilityB\
-    \x0e\n\x0cone_of_widthB\x19\n\x17one_of_type_option_data\"\x9c\x01\n\x0f\
-    MoveItemPayload\x12\x17\n\x07grid_id\x18\x01\x20\x01(\tR\x06gridId\x12\
-    \x17\n\x07item_id\x18\x02\x20\x01(\tR\x06itemId\x12\x1d\n\nfrom_index\
-    \x18\x03\x20\x01(\x05R\tfromIndex\x12\x19\n\x08to_index\x18\x04\x20\x01(\
-    \x05R\x07toIndex\x12\x1d\n\x02ty\x18\x05\x20\x01(\x0e2\r.MoveItemTypeR\
-    \x02ty\"\x7f\n\rCellChangeset\x12\x17\n\x07grid_id\x18\x01\x20\x01(\tR\
-    \x06gridId\x12\x15\n\x06row_id\x18\x02\x20\x01(\tR\x05rowId\x12\x19\n\
-    \x08field_id\x18\x03\x20\x01(\tR\x07fieldId\x12\x14\n\x04data\x18\x04\
-    \x20\x01(\tH\0R\x04dataB\r\n\x0bone_of_data**\n\x0cMoveItemType\x12\r\n\
-    \tMoveField\x10\0\x12\x0b\n\x07MoveRow\x10\x01*d\n\tFieldType\x12\x0c\n\
-    \x08RichText\x10\0\x12\n\n\x06Number\x10\x01\x12\x0c\n\x08DateTime\x10\
-    \x02\x12\x10\n\x0cSingleSelect\x10\x03\x12\x0f\n\x0bMultiSelect\x10\x04\
-    \x12\x0c\n\x08Checkbox\x10\x05b\x06proto3\
+    idth\x18\x07\x20\x01(\x05R\x05width\x12\x1d\n\nis_primary\x18\x08\x20\
+    \x01(\x08R\tisPrimary\"'\n\nFieldOrder\x12\x19\n\x08field_id\x18\x01\x20\
+    \x01(\tR\x07fieldId\"\xc6\x01\n\x12GridFieldChangeset\x12\x17\n\x07grid_\
+    id\x18\x01\x20\x01(\tR\x06gridId\x124\n\x0finserted_fields\x18\x02\x20\
+    \x03(\x0b2\x0b.IndexFieldR\x0einsertedFields\x122\n\x0edeleted_fields\
+    \x18\x03\x20\x03(\x0b2\x0b.FieldOrderR\rdeletedFields\x12-\n\x0eupdated_\
+    fields\x18\x04\x20\x03(\x0b2\x06.FieldR\rupdatedFields\"@\n\nIndexField\
+    \x12\x1c\n\x05field\x18\x01\x20\x01(\x0b2\x06.FieldR\x05field\x12\x14\n\
+    \x05index\x18\x02\x20\x01(\x05R\x05index\"\x90\x01\n\x1aGetEditFieldCont\
+    extPayload\x12\x17\n\x07grid_id\x18\x01\x20\x01(\tR\x06gridId\x12\x1b\n\
+    \x08field_id\x18\x02\x20\x01(\tH\0R\x07fieldId\x12)\n\nfield_type\x18\
+    \x03\x20\x01(\x0e2\n.FieldTypeR\tfieldTypeB\x11\n\x0fone_of_field_id\"q\
+    \n\x10EditFieldPayload\x12\x17\n\x07grid_id\x18\x01\x20\x01(\tR\x06gridI\
+    d\x12\x19\n\x08field_id\x18\x02\x20\x01(\tR\x07fieldId\x12)\n\nfield_typ\
+    e\x18\x03\x20\x01(\x0e2\n.FieldTypeR\tfieldType\"|\n\x10EditFieldContext\
+    \x12\x17\n\x07grid_id\x18\x01\x20\x01(\tR\x06gridId\x12%\n\ngrid_field\
+    \x18\x02\x20\x01(\x0b2\x06.FieldR\tgridField\x12(\n\x10type_option_data\
+    \x18\x03\x20\x01(\x0cR\x0etypeOptionData\"-\n\rRepeatedField\x12\x1c\n\
+    \x05items\x18\x01\x20\x03(\x0b2\x06.FieldR\x05items\"7\n\x12RepeatedFiel\
+    dOrder\x12!\n\x05items\x18\x01\x20\x03(\x0b2\x0b.FieldOrderR\x05items\"T\
+    \n\x08RowOrder\x12\x15\n\x06row_id\x18\x01\x20\x01(\tR\x05rowId\x12\x19\
+    \n\x08block_id\x18\x02\x20\x01(\tR\x07blockId\x12\x16\n\x06height\x18\
+    \x03\x20\x01(\x05R\x06height\"\xb8\x01\n\x03Row\x12\x0e\n\x02id\x18\x01\
+    \x20\x01(\tR\x02id\x12@\n\x10cell_by_field_id\x18\x02\x20\x03(\x0b2\x17.\
+    Row.CellByFieldIdEntryR\rcellByFieldId\x12\x16\n\x06height\x18\x03\x20\
+    \x01(\x05R\x06height\x1aG\n\x12CellByFieldIdEntry\x12\x10\n\x03key\x18\
+    \x01\x20\x01(\tR\x03key\x12\x1b\n\x05value\x18\x02\x20\x01(\x0b2\x05.Cel\
+    lR\x05value:\x028\x01\")\n\x0bRepeatedRow\x12\x1a\n\x05items\x18\x01\x20\
+    \x03(\x0b2\x04.RowR\x05items\"5\n\x11RepeatedGridBlock\x12\x20\n\x05item\
+    s\x18\x01\x20\x03(\x0b2\n.GridBlockR\x05items\"U\n\x0eGridBlockOrder\x12\
+    \x19\n\x08block_id\x18\x01\x20\x01(\tR\x07blockId\x12(\n\nrow_orders\x18\
+    \x02\x20\x03(\x0b2\t.RowOrderR\trowOrders\"_\n\rIndexRowOrder\x12&\n\tro\
+    w_order\x18\x01\x20\x01(\x0b2\t.RowOrderR\x08rowOrder\x12\x16\n\x05index\
+    \x18\x02\x20\x01(\x05H\0R\x05indexB\x0e\n\x0cone_of_index\"\xbf\x01\n\
+    \x11GridRowsChangeset\x12\x19\n\x08block_id\x18\x01\x20\x01(\tR\x07block\
+    Id\x123\n\rinserted_rows\x18\x02\x20\x03(\x0b2\x0e.IndexRowOrderR\x0cins\
+    ertedRows\x12,\n\x0cdeleted_rows\x18\x03\x20\x03(\x0b2\t.RowOrderR\x0bde\
+    letedRows\x12,\n\x0cupdated_rows\x18\x04\x20\x03(\x0b2\t.RowOrderR\x0bup\
+    datedRows\"E\n\tGridBlock\x12\x0e\n\x02id\x18\x01\x20\x01(\tR\x02id\x12(\
+    \n\nrow_orders\x18\x02\x20\x03(\x0b2\t.RowOrderR\trowOrders\";\n\x04Cell\
+    \x12\x19\n\x08field_id\x18\x01\x20\x01(\tR\x07fieldId\x12\x18\n\x07conte\
+    nt\x18\x02\x20\x01(\tR\x07content\"\x8f\x01\n\x14CellNotificationData\
+    \x12\x17\n\x07grid_id\x18\x01\x20\x01(\tR\x06gridId\x12\x19\n\x08field_i\
+    d\x18\x02\x20\x01(\tR\x07fieldId\x12\x15\n\x06row_id\x18\x03\x20\x01(\tR\
+    \x05rowId\x12\x1a\n\x07content\x18\x04\x20\x01(\tH\0R\x07contentB\x10\n\
+    \x0eone_of_content\"+\n\x0cRepeatedCell\x12\x1b\n\x05items\x18\x01\x20\
+    \x03(\x0b2\x05.CellR\x05items\"'\n\x11CreateGridPayload\x12\x12\n\x04nam\
+    e\x18\x01\x20\x01(\tR\x04name\"\x1e\n\x06GridId\x12\x14\n\x05value\x18\
+    \x01\x20\x01(\tR\x05value\"#\n\x0bGridBlockId\x12\x14\n\x05value\x18\x01\
+    \x20\x01(\tR\x05value\"f\n\x10CreateRowPayload\x12\x17\n\x07grid_id\x18\
+    \x01\x20\x01(\tR\x06gridId\x12\"\n\x0cstart_row_id\x18\x02\x20\x01(\tH\0\
+    R\nstartRowIdB\x15\n\x13one_of_start_row_id\"\xb6\x01\n\x12InsertFieldPa\
+    yload\x12\x17\n\x07grid_id\x18\x01\x20\x01(\tR\x06gridId\x12\x1c\n\x05fi\
+    eld\x18\x02\x20\x01(\x0b2\x06.FieldR\x05field\x12(\n\x10type_option_data\
+    \x18\x03\x20\x01(\x0cR\x0etypeOptionData\x12&\n\x0estart_field_id\x18\
+    \x04\x20\x01(\tH\0R\x0cstartFieldIdB\x17\n\x15one_of_start_field_id\"d\n\
+    \x11QueryFieldPayload\x12\x17\n\x07grid_id\x18\x01\x20\x01(\tR\x06gridId\
+    \x126\n\x0cfield_orders\x18\x02\x20\x01(\x0b2\x13.RepeatedFieldOrderR\
+    \x0bfieldOrders\"e\n\x16QueryGridBlocksPayload\x12\x17\n\x07grid_id\x18\
+    \x01\x20\x01(\tR\x06gridId\x122\n\x0cblock_orders\x18\x02\x20\x03(\x0b2\
+    \x0f.GridBlockOrderR\x0bblockOrders\"\xa8\x03\n\x15FieldChangesetPayload\
+    \x12\x19\n\x08field_id\x18\x01\x20\x01(\tR\x07fieldId\x12\x17\n\x07grid_\
+    id\x18\x02\x20\x01(\tR\x06gridId\x12\x14\n\x04name\x18\x03\x20\x01(\tH\0\
+    R\x04name\x12\x14\n\x04desc\x18\x04\x20\x01(\tH\x01R\x04desc\x12+\n\nfie\
+    ld_type\x18\x05\x20\x01(\x0e2\n.FieldTypeH\x02R\tfieldType\x12\x18\n\x06\
+    frozen\x18\x06\x20\x01(\x08H\x03R\x06frozen\x12\x20\n\nvisibility\x18\
+    \x07\x20\x01(\x08H\x04R\nvisibility\x12\x16\n\x05width\x18\x08\x20\x01(\
+    \x05H\x05R\x05width\x12*\n\x10type_option_data\x18\t\x20\x01(\x0cH\x06R\
+    \x0etypeOptionDataB\r\n\x0bone_of_nameB\r\n\x0bone_of_descB\x13\n\x11one\
+    _of_field_typeB\x0f\n\rone_of_frozenB\x13\n\x11one_of_visibilityB\x0e\n\
+    \x0cone_of_widthB\x19\n\x17one_of_type_option_data\"\x9c\x01\n\x0fMoveIt\
+    emPayload\x12\x17\n\x07grid_id\x18\x01\x20\x01(\tR\x06gridId\x12\x17\n\
+    \x07item_id\x18\x02\x20\x01(\tR\x06itemId\x12\x1d\n\nfrom_index\x18\x03\
+    \x20\x01(\x05R\tfromIndex\x12\x19\n\x08to_index\x18\x04\x20\x01(\x05R\
+    \x07toIndex\x12\x1d\n\x02ty\x18\x05\x20\x01(\x0e2\r.MoveItemTypeR\x02ty\
+    \"\x7f\n\rCellChangeset\x12\x17\n\x07grid_id\x18\x01\x20\x01(\tR\x06grid\
+    Id\x12\x15\n\x06row_id\x18\x02\x20\x01(\tR\x05rowId\x12\x19\n\x08field_i\
+    d\x18\x03\x20\x01(\tR\x07fieldId\x12\x14\n\x04data\x18\x04\x20\x01(\tH\0\
+    R\x04dataB\r\n\x0bone_of_data**\n\x0cMoveItemType\x12\r\n\tMoveField\x10\
+    \0\x12\x0b\n\x07MoveRow\x10\x01*d\n\tFieldType\x12\x0c\n\x08RichText\x10\
+    \0\x12\n\n\x06Number\x10\x01\x12\x0c\n\x08DateTime\x10\x02\x12\x10\n\x0c\
+    SingleSelect\x10\x03\x12\x0f\n\x0bMultiSelect\x10\x04\x12\x0c\n\x08Check\
+    box\x10\x05b\x06proto3\
 ";
 
 static file_descriptor_proto_lazy: ::protobuf::rt::LazyV2<::protobuf::descriptor::FileDescriptorProto> = ::protobuf::rt::LazyV2::INIT;

+ 1 - 0
shared-lib/flowy-grid-data-model/src/protobuf/proto/grid.proto

@@ -13,6 +13,7 @@ message Field {
     bool frozen = 5;
     bool visibility = 6;
     int32 width = 7;
+    bool is_primary = 8;
 }
 message FieldOrder {
     string field_id = 1;

+ 2 - 2
shared-lib/flowy-sync/src/client_grid/grid_builder.rs

@@ -47,8 +47,8 @@ mod tests {
     fn create_default_grid_test() {
         let grid_id = "1".to_owned();
         let build_context = GridBuilder::default()
-            .add_field(FieldMeta::new("Name", "", FieldType::RichText))
-            .add_field(FieldMeta::new("Tags", "", FieldType::SingleSelect))
+            .add_field(FieldMeta::new("Name", "", FieldType::RichText, true))
+            .add_field(FieldMeta::new("Tags", "", FieldType::SingleSelect, false))
             .add_empty_row()
             .add_empty_row()
             .add_empty_row()