Bladeren bron

feat: add grid widget

appflowy 3 jaren geleden
bovenliggende
commit
0bbf17f776
23 gewijzigde bestanden met toevoegingen van 1334 en 75 verwijderingen
  1. 135 0
      frontend/app_flowy/lib/workspace/application/grid/grid_bloc.dart
  2. 20 0
      frontend/app_flowy/lib/workspace/application/grid/grid_service.dart
  3. 73 0
      frontend/app_flowy/lib/workspace/presentation/plugins/grid/controller/flowy_table_selection.dart
  4. 22 0
      frontend/app_flowy/lib/workspace/presentation/plugins/grid/controller/grid_scroll.dart
  5. 17 0
      frontend/app_flowy/lib/workspace/presentation/plugins/grid/grid_layout.dart
  6. 150 0
      frontend/app_flowy/lib/workspace/presentation/plugins/grid/grid_page.dart
  7. 16 0
      frontend/app_flowy/lib/workspace/presentation/plugins/grid/grid_sizes.dart
  8. 17 0
      frontend/app_flowy/lib/workspace/presentation/plugins/grid/widgets/grid_content/cell_builder.dart
  9. 34 0
      frontend/app_flowy/lib/workspace/presentation/plugins/grid/widgets/grid_content/cell_container.dart
  10. 10 0
      frontend/app_flowy/lib/workspace/presentation/plugins/grid/widgets/grid_content/cell_decoration.dart
  11. 68 0
      frontend/app_flowy/lib/workspace/presentation/plugins/grid/widgets/grid_content/grid_cell.dart
  12. 64 0
      frontend/app_flowy/lib/workspace/presentation/plugins/grid/widgets/grid_content/grid_row.dart
  13. 14 0
      frontend/app_flowy/lib/workspace/presentation/plugins/grid/widgets/grid_error_page.dart
  14. 46 0
      frontend/app_flowy/lib/workspace/presentation/plugins/grid/widgets/grid_footer/grid_footer.dart
  15. 5 0
      frontend/app_flowy/lib/workspace/presentation/plugins/grid/widgets/grid_header/constants.dart
  16. 0 0
      frontend/app_flowy/lib/workspace/presentation/plugins/grid/widgets/grid_header/header.dart
  17. 52 0
      frontend/app_flowy/lib/workspace/presentation/plugins/grid/widgets/grid_header/header_cell.dart
  18. 3 0
      frontend/app_flowy/packages/flowy_sdk/lib/dispatch/dispatch.dart
  19. 123 15
      frontend/app_flowy/packages/flowy_sdk/lib/protobuf/flowy-grid-data-model/grid.pb.dart
  20. 23 2
      frontend/app_flowy/packages/flowy_sdk/lib/protobuf/flowy-grid-data-model/grid.pbjson.dart
  21. 16 1
      shared-lib/flowy-grid-data-model/src/entities/grid.rs
  22. 418 56
      shared-lib/flowy-grid-data-model/src/protobuf/model/grid.rs
  23. 8 1
      shared-lib/flowy-grid-data-model/src/protobuf/proto/grid.proto

+ 135 - 0
frontend/app_flowy/lib/workspace/application/grid/grid_bloc.dart

@@ -0,0 +1,135 @@
+import 'dart:async';
+
+import 'package:dartz/dartz.dart';
+import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
+import 'package:flowy_sdk/protobuf/flowy-folder-data-model/view.pb.dart';
+import 'package:flowy_sdk/protobuf/flowy-grid-data-model/protobuf.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:freezed_annotation/freezed_annotation.dart';
+
+import 'grid_service.dart';
+
+part 'grid_bloc.freezed.dart';
+
+class GridBloc extends Bloc<GridEvent, GridState> {
+  final GridService service;
+  final View view;
+  late Grid _grid;
+
+  GridBloc({required this.view, required this.service}) : super(GridState.initial()) {
+    on<GridEvent>(
+      (event, emit) async {
+        await event.map(
+          initial: (Initial value) async {
+            await _initial(value, emit);
+          },
+          createRow: (_CreateRow value) {
+            service.createRow(gridId: view.id);
+          },
+          delete: (_Delete value) {},
+          rename: (_Rename value) {},
+          updateDesc: (_Desc value) {},
+        );
+      },
+    );
+  }
+
+  @override
+  Future<void> close() async {
+    return super.close();
+  }
+
+  Future<void> _initial(Initial value, Emitter<GridState> emit) async {
+    final result = await service.openGrid(gridId: view.id);
+    result.fold(
+      (grid) {
+        _grid = grid;
+        _loadGridInfo(emit);
+      },
+      (err) {
+        emit(state.copyWith(loadingState: GridLoadingState.finish(right(err))));
+      },
+    );
+  }
+
+  Future<void> _loadGridInfo(Emitter<GridState> emit) async {
+    emit(
+      state.copyWith(loadingState: GridLoadingState.finish(left(unit))),
+    );
+  }
+}
+
+@freezed
+abstract class GridEvent with _$GridEvent {
+  const factory GridEvent.initial() = Initial;
+  const factory GridEvent.rename(String gridId, String name) = _Rename;
+  const factory GridEvent.updateDesc(String gridId, String desc) = _Desc;
+  const factory GridEvent.delete(String gridId) = _Delete;
+  const factory GridEvent.createRow() = _CreateRow;
+}
+
+@freezed
+abstract class GridState with _$GridState {
+  const factory GridState({
+    required GridLoadingState loadingState,
+    required Option<Either<GridInfo, FlowyError>> gridInfo,
+  }) = _GridState;
+
+  factory GridState.initial() => GridState(
+        loadingState: const _Loading(),
+        gridInfo: none(),
+      );
+}
+
+@freezed
+class GridLoadingState with _$GridLoadingState {
+  const factory GridLoadingState.loading() = _Loading;
+  const factory GridLoadingState.finish(Either<Unit, FlowyError> successOrFail) = _Finish;
+}
+
+typedef FieldById = Map<String, Field>;
+typedef RowById = Map<String, Row>;
+typedef CellById = Map<String, DisplayCell>;
+
+class GridInfo {
+  List<RowOrder> rowOrders;
+  List<FieldOrder> fieldOrders;
+  RowById rowMap;
+  FieldById fieldMap;
+
+  GridInfo({
+    required this.rowOrders,
+    required this.fieldOrders,
+    required this.fieldMap,
+    required this.rowMap,
+  });
+
+  RowInfo rowInfoAtIndex(int index) {
+    final rowOrder = rowOrders[index];
+    final Row row = rowMap[rowOrder.rowId]!;
+    final cellMap = row.cellByFieldId;
+
+    final displayCellMap = <String, DisplayCell>{};
+
+    return RowInfo(
+      fieldOrders: fieldOrders,
+      fieldMap: fieldMap,
+      displayCellMap: displayCellMap,
+    );
+  }
+
+  int numberOfRows() {
+    return rowOrders.length;
+  }
+}
+
+class RowInfo {
+  List<FieldOrder> fieldOrders;
+  FieldById fieldMap;
+  CellById displayCellMap;
+  RowInfo({
+    required this.fieldOrders,
+    required this.fieldMap,
+    required this.displayCellMap,
+  });
+}

+ 20 - 0
frontend/app_flowy/lib/workspace/application/grid/grid_service.dart

@@ -0,0 +1,20 @@
+import 'package:flowy_sdk/dispatch/dispatch.dart';
+import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
+import 'package:flowy_sdk/protobuf/flowy-grid-data-model/grid.pb.dart';
+import 'package:dartz/dartz.dart';
+
+class GridService {
+  Future<Either<Grid, FlowyError>> createGrid({required String name}) {
+    final payload = CreateGridPayload()..name = name;
+    return GridEventCreateGrid(payload).send();
+  }
+
+  Future<Either<Grid, FlowyError>> openGrid({required String gridId}) {
+    final payload = GridId(value: gridId);
+    return GridEventOpenGrid(payload).send();
+  }
+
+  Future<Either<void, FlowyError>> createRow({required String gridId}) {
+    throw UnimplementedError();
+  }
+}

+ 73 - 0
frontend/app_flowy/lib/workspace/presentation/plugins/grid/controller/flowy_table_selection.dart

@@ -0,0 +1,73 @@
+/// The data structor representing each selection of flowy table.
+enum FlowyTableSelectionType {
+  item,
+  row,
+  col,
+}
+
+class FlowyTableSelectionItem {
+  final FlowyTableSelectionType type;
+  final int? row;
+  final int? column;
+
+  const FlowyTableSelectionItem({
+    required this.type,
+    this.row,
+    this.column,
+  });
+
+  @override
+  String toString() {
+    return '$type($row, $column)';
+  }
+
+  @override
+  bool operator ==(Object other) {
+    if (identical(this, other)) {
+      return true;
+    }
+    return other is FlowyTableSelectionItem &&
+        type == other.type &&
+        row == other.row &&
+        column == other.column;
+  }
+
+  @override
+  int get hashCode => type.hashCode ^ row.hashCode ^ column.hashCode;
+}
+
+class FlowyTableSelection {
+  Set<FlowyTableSelectionItem> _items = {};
+
+  Set<FlowyTableSelectionItem> get items => _items;
+
+  FlowyTableSelection(
+    this._items,
+  );
+
+  FlowyTableSelection.combine(
+      FlowyTableSelection lhs, FlowyTableSelection rhs) {
+    this..combine(lhs)..combine(rhs);
+  }
+
+  FlowyTableSelection operator +(FlowyTableSelection other) {
+    return this..combine(other);
+  }
+
+  void combine(FlowyTableSelection other) {
+    var totalItems = items..union(other.items);
+    final rows = totalItems
+        .where((ele) => ele.type == FlowyTableSelectionType.row)
+        .map((e) => e.row)
+        .toSet();
+    final cols = totalItems
+        .where((ele) => ele.type == FlowyTableSelectionType.col)
+        .map((e) => e.column)
+        .toSet();
+    totalItems.removeWhere((ele) {
+      return ele.type == FlowyTableSelectionType.item &&
+          (rows.contains(ele.row) || cols.contains(ele.column));
+    });
+    _items = totalItems;
+  }
+}

+ 22 - 0
frontend/app_flowy/lib/workspace/presentation/plugins/grid/controller/grid_scroll.dart

@@ -0,0 +1,22 @@
+import 'package:flutter/material.dart';
+
+class GridScrollController {
+  final ScrollController _verticalController = ScrollController();
+  final ScrollController _horizontalController = ScrollController();
+
+  ScrollController get verticalController => _verticalController;
+  ScrollController get horizontalController => _horizontalController;
+
+  GridScrollController();
+
+  // final SelectionChangeCallback? onSelectionChanged;
+
+  // final ShouldApplySelection? shouldApplySelection;
+
+  // final ScrollCallback? onScroll;
+
+  void dispose() {
+    verticalController.dispose();
+    horizontalController.dispose();
+  }
+}

+ 17 - 0
frontend/app_flowy/lib/workspace/presentation/plugins/grid/grid_layout.dart

@@ -0,0 +1,17 @@
+import 'package:flowy_sdk/protobuf/flowy-grid-data-model/grid.pb.dart';
+
+import 'grid_sizes.dart';
+
+class GridLayout {
+  static double headerWidth(List<FieldOrder> fieldOrders) {
+    if (fieldOrders.isEmpty) return 0;
+
+    final fieldsWidth = fieldOrders
+        .map(
+          (fieldOrder) => fieldOrder.width.toDouble(),
+        )
+        .reduce((value, element) => value + element);
+
+    return fieldsWidth + GridSize.firstHeaderPadding;
+  }
+}

+ 150 - 0
frontend/app_flowy/lib/workspace/presentation/plugins/grid/grid_page.dart

@@ -0,0 +1,150 @@
+import 'package:app_flowy/startup/startup.dart';
+import 'package:app_flowy/workspace/application/grid/grid_bloc.dart';
+import 'package:flowy_infra_ui/style_widget/scrolling/styled_list.dart';
+import 'package:flowy_infra_ui/style_widget/scrolling/styled_scroll_bar.dart';
+import 'package:flowy_infra_ui/style_widget/scrolling/styled_scrollview.dart';
+import 'package:flowy_infra_ui/widget/error_page.dart';
+import 'package:flowy_sdk/protobuf/flowy-folder-data-model/view.pb.dart';
+import 'package:flowy_sdk/protobuf/flowy-grid-data-model/grid.pb.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:flutter/material.dart';
+import 'package:app_flowy/workspace/presentation/plugins/grid/controller/grid_scroll.dart';
+import 'package:styled_widget/styled_widget.dart';
+
+import 'grid_layout.dart';
+import 'grid_sizes.dart';
+import 'widgets/grid_content/grid_row.dart';
+import 'widgets/grid_footer/grid_footer.dart';
+import 'widgets/grid_header/header.dart';
+
+class GridPage extends StatefulWidget {
+  final View view;
+
+  GridPage({Key? key, required this.view}) : super(key: ValueKey(view.id));
+
+  @override
+  State<GridPage> createState() => _GridPageState();
+}
+
+class _GridPageState extends State<GridPage> {
+  @override
+  Widget build(BuildContext context) {
+    return MultiBlocProvider(
+      providers: [
+        BlocProvider<GridBloc>(create: (context) => getIt<GridBloc>()),
+      ],
+      child: BlocBuilder<GridBloc, GridState>(
+        builder: (context, state) {
+          return state.loadingState.map(
+            loading: (_) => const Center(child: CircularProgressIndicator.adaptive()),
+            finish: (result) => result.successOrFail.fold(
+              (_) => const GridBody(),
+              (err) => FlowyErrorPage(err.toString()),
+            ),
+          );
+        },
+      ),
+    );
+  }
+
+  @override
+  void dispose() {
+    super.dispose();
+  }
+
+  @override
+  void deactivate() {
+    super.deactivate();
+  }
+
+  @override
+  void didUpdateWidget(covariant GridPage oldWidget) {
+    super.didUpdateWidget(oldWidget);
+  }
+}
+
+class GridBody extends StatefulWidget {
+  const GridBody({Key? key}) : super(key: key);
+
+  @override
+  _GridBodyState createState() => _GridBodyState();
+}
+
+class _GridBodyState extends State<GridBody> {
+  final _scrollController = GridScrollController();
+
+  @override
+  void dispose() {
+    _scrollController.dispose();
+    super.dispose();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return BlocBuilder<GridBloc, GridState>(
+      builder: (context, state) {
+        return state.gridInfo.fold(
+          () => const Center(child: CircularProgressIndicator.adaptive()),
+          (some) => some.fold(
+            (gridInfo) => _renderGrid(context, gridInfo),
+            (err) => FlowyErrorPage(err.toString()),
+          ),
+        );
+      },
+    );
+  }
+
+  Widget _renderGrid(BuildContext context, GridInfo gridInfo) {
+    return Stack(
+      children: [
+        StyledSingleChildScrollView(
+          controller: _scrollController.horizontalController,
+          axis: Axis.horizontal,
+          child: SizedBox(
+            width: GridLayout.headerWidth(gridInfo.fieldOrders),
+            child: CustomScrollView(
+              physics: StyledScrollPhysics(),
+              controller: _scrollController.verticalController,
+              slivers: <Widget>[
+                _buildHeader(gridInfo.fieldOrders, gridInfo.fieldMap),
+                _buildRows(gridInfo),
+                _builderFooter(context),
+              ],
+            ),
+          ),
+        ),
+        ScrollbarListStack(
+          axis: Axis.vertical,
+          controller: _scrollController.verticalController,
+          barSize: GridSize.scrollBarSize,
+          child: Container(),
+        ).padding(right: 0, top: GridSize.headerHeight, bottom: GridSize.scrollBarSize),
+      ],
+    );
+  }
+
+  Widget _buildHeader(List<FieldOrder> fieldOrders, FieldById fieldById) {
+    return SliverPersistentHeader(
+      delegate: GridHeaderDelegate(fieldOrders, fieldById),
+      floating: true,
+      pinned: true,
+    );
+  }
+
+  Widget _buildRows(GridInfo gridInfo) {
+    return SliverList(
+      delegate: SliverChildBuilderDelegate((context, index) {
+        final rowInfo = gridInfo.rowInfoAtIndex(index);
+        return RepaintBoundary(child: GridRow(rowInfo));
+      }, childCount: gridInfo.numberOfRows()),
+    );
+  }
+
+  Widget _builderFooter(BuildContext context) {
+    return GridFooter(
+      onAddRow: () {
+        context.read<GridBloc>().add(const GridEvent.createRow());
+      },
+    );
+  }
+}

+ 16 - 0
frontend/app_flowy/lib/workspace/presentation/plugins/grid/grid_sizes.dart

@@ -0,0 +1,16 @@
+class GridInsets {
+  static double scale = 1;
+
+  static double get horizontal => 6 * scale;
+  static double get vertical => 6 * scale;
+}
+
+class GridSize {
+  static double scale = 1;
+
+  static double get scrollBarSize => 12 * scale;
+  static double get headerHeight => 50 * scale;
+  static double get rowHeight => 50 * scale;
+  static double get footerHeight => 40 * scale;
+  static double get firstHeaderPadding => 20 * scale;
+}

+ 17 - 0
frontend/app_flowy/lib/workspace/presentation/plugins/grid/widgets/grid_content/cell_builder.dart

@@ -0,0 +1,17 @@
+import 'package:flowy_sdk/protobuf/flowy-grid-data-model/grid.pb.dart';
+import 'grid_cell.dart';
+
+class GridCellBuilder {
+  static GridCell buildCell(Field? field, DisplayCell? cell) {
+    if (field == null || cell == null) {
+      return BlankCell();
+    }
+
+    switch (field.fieldType) {
+      case FieldType.RichText:
+        return GridTextCell(cell.content);
+      default:
+        return BlankCell();
+    }
+  }
+}

+ 34 - 0
frontend/app_flowy/lib/workspace/presentation/plugins/grid/widgets/grid_content/cell_container.dart

@@ -0,0 +1,34 @@
+import 'package:app_flowy/workspace/presentation/plugins/grid/grid_sizes.dart';
+import 'package:flowy_infra_ui/widget/mouse_hover_builder.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+import 'cell_decoration.dart';
+import 'grid_cell.dart';
+
+class CellContainer extends StatelessWidget {
+  final GridCell child;
+  final double width;
+  const CellContainer({Key? key, required this.child, required this.width}) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    return GestureDetector(
+      behavior: HitTestBehavior.translucent,
+      onTap: () {
+        // context
+        //     .read<HomeBloc>()
+        //     .add(HomeEvent.setEditPannel(CellEditPannelContext()));
+      },
+      child: MouseHoverBuilder(
+        builder: (_, isHovered) => Container(
+          width: width,
+          decoration: CellDecoration.box(
+            color: isHovered ? Colors.red.withOpacity(.1) : Colors.transparent,
+          ),
+          padding: EdgeInsets.symmetric(vertical: GridInsets.vertical, horizontal: GridInsets.horizontal),
+          child: child,
+        ),
+      ),
+    );
+  }
+}

+ 10 - 0
frontend/app_flowy/lib/workspace/presentation/plugins/grid/widgets/grid_content/cell_decoration.dart

@@ -0,0 +1,10 @@
+import 'package:flutter/material.dart';
+
+class CellDecoration {
+  static BoxDecoration box({required Color color}) {
+    return BoxDecoration(
+      border: Border.all(color: Colors.black26, width: 0.2),
+      color: color,
+    );
+  }
+}

+ 68 - 0
frontend/app_flowy/lib/workspace/presentation/plugins/grid/widgets/grid_content/grid_cell.dart

@@ -0,0 +1,68 @@
+import 'package:flutter/material.dart';
+// ignore: import_of_legacy_library_into_null_safe
+
+/// The interface of base cell.
+abstract class GridCell extends StatelessWidget {
+  final canSelect = true;
+
+  const GridCell({Key? key}) : super(key: key);
+}
+
+class GridTextCell extends GridCell {
+  final String content;
+  const GridTextCell(this.content, {Key? key}) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    return Text(content);
+  }
+}
+
+class DateCell extends GridCell {
+  final String content;
+  const DateCell(this.content, {Key? key}) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    return Text(content);
+  }
+}
+
+class NumberCell extends GridCell {
+  final String content;
+  const NumberCell(this.content, {Key? key}) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    return Text(content);
+  }
+}
+
+class SingleSelectCell extends GridCell {
+  final String content;
+  const SingleSelectCell(this.content, {Key? key}) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    return Text(content);
+  }
+}
+
+class MultiSelectCell extends GridCell {
+  final String content;
+  const MultiSelectCell(this.content, {Key? key}) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    return Text(content);
+  }
+}
+
+class BlankCell extends GridCell {
+  const BlankCell({Key? key}) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    return Container();
+  }
+}

+ 64 - 0
frontend/app_flowy/lib/workspace/presentation/plugins/grid/widgets/grid_content/grid_row.dart

@@ -0,0 +1,64 @@
+import 'package:app_flowy/workspace/application/grid/grid_bloc.dart';
+import 'package:app_flowy/workspace/presentation/plugins/grid/grid_sizes.dart';
+import 'package:flowy_sdk/protobuf/flowy-grid-data-model/grid.pb.dart' hide Row;
+import 'package:flutter/material.dart';
+import 'cell_builder.dart';
+import 'cell_container.dart';
+import 'grid_row_leading.dart';
+
+class GridRowContext {
+  final RepeatedFieldOrder fieldOrders;
+  final Map<String, Field> fieldById;
+  final Map<String, DisplayCell> cellByFieldId;
+  GridRowContext(this.fieldOrders, this.fieldById, this.cellByFieldId);
+}
+
+class GridRow extends StatelessWidget {
+  final RowInfo rowInfo;
+  final Function(bool)? onHoverChange;
+  const GridRow(this.rowInfo, {Key? key, this.onHoverChange}) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    return SizedBox(
+      height: GridSize.rowHeight,
+      child: _buildRowBody(),
+    );
+  }
+
+  Widget _buildRowBody() {
+    Widget rowWidget = Row(
+      crossAxisAlignment: CrossAxisAlignment.stretch,
+      children: _buildCells(),
+    );
+
+    if (onHoverChange != null) {
+      rowWidget = MouseRegion(
+        onEnter: (event) => onHoverChange!(true),
+        onExit: (event) => onHoverChange!(false),
+        cursor: MouseCursor.uncontrolled,
+        child: rowWidget,
+      );
+    }
+
+    return rowWidget;
+  }
+
+  List<Widget> _buildCells() {
+    var cells = List<Widget>.empty(growable: true);
+    cells.add(const RowLeading());
+
+    rowInfo.fieldOrders.where((element) => element.visibility).forEach((fieldOrder) {
+      final field = rowInfo.fieldMap[fieldOrder.fieldId];
+      final data = rowInfo.displayCellMap[fieldOrder.fieldId];
+
+      final cell = CellContainer(
+        width: fieldOrder.width.toDouble(),
+        child: GridCellBuilder.buildCell(field, data),
+      );
+
+      cells.add(cell);
+    });
+    return cells;
+  }
+}

+ 14 - 0
frontend/app_flowy/lib/workspace/presentation/plugins/grid/widgets/grid_error_page.dart

@@ -0,0 +1,14 @@
+import 'package:flutter/material.dart';
+
+class GridUnknownError extends StatelessWidget {
+  const GridUnknownError({Key? key}) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    return Container(
+      child: Center(
+        child: CircularProgressIndicator(),
+      ),
+    );
+  }
+}

+ 46 - 0
frontend/app_flowy/lib/workspace/presentation/plugins/grid/widgets/grid_footer/grid_footer.dart

@@ -0,0 +1,46 @@
+import 'package:app_flowy/workspace/presentation/plugins/grid/grid_sizes.dart';
+import 'package:app_flowy/workspace/presentation/plugins/grid/widgets/grid_content/cell_decoration.dart';
+import 'package:flowy_infra_ui/widget/mouse_hover_builder.dart';
+import 'package:flutter/material.dart';
+
+class GridFooter extends StatelessWidget {
+  final VoidCallback? onAddRow;
+  const GridFooter({Key? key, required this.onAddRow}) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    return SliverToBoxAdapter(
+      child: SizedBox(
+        height: GridSize.footerHeight,
+        child: Row(
+          children: [
+            AddRowButton(onTap: onAddRow),
+          ],
+        ),
+      ),
+    );
+  }
+}
+
+class AddRowButton extends StatelessWidget {
+  final VoidCallback? onTap;
+  const AddRowButton({Key? key, required this.onTap}) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    return GestureDetector(
+      behavior: HitTestBehavior.translucent,
+      onTap: onTap,
+      child: MouseHoverBuilder(
+        builder: (_, isHovered) => Container(
+          width: GridSize.firstHeaderPadding,
+          height: GridSize.footerHeight,
+          decoration: CellDecoration.box(
+            color: isHovered ? Colors.red.withOpacity(.1) : Colors.white,
+          ),
+          child: const Icon(Icons.add, size: 16),
+        ),
+      ),
+    );
+  }
+}

+ 5 - 0
frontend/app_flowy/lib/workspace/presentation/plugins/grid/widgets/grid_header/constants.dart

@@ -0,0 +1,5 @@
+import 'package:flutter/material.dart';
+
+class GridHeaderConstants {
+  static Color get backgroundColor => Colors.grey;
+}

+ 0 - 0
frontend/app_flowy/lib/workspace/presentation/plugins/grid/widgets/grid_header/header.dart


+ 52 - 0
frontend/app_flowy/lib/workspace/presentation/plugins/grid/widgets/grid_header/header_cell.dart

@@ -0,0 +1,52 @@
+import 'package:app_flowy/workspace/presentation/plugins/grid/grid_sizes.dart';
+import 'package:flowy_sdk/protobuf/flowy-grid-data-model/grid.pb.dart' hide Row;
+import 'package:flutter/material.dart';
+import 'constants.dart';
+
+class HeaderCell extends StatelessWidget {
+  final Field field;
+  const HeaderCell(this.field, {Key? key}) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    return Text(
+      field.name,
+      style: const TextStyle(fontSize: 15.0, color: Colors.black),
+    );
+  }
+}
+
+class HeaderCellContainer extends StatelessWidget {
+  final HeaderCell child;
+  final double width;
+  const HeaderCellContainer({Key? key, required this.child, required this.width}) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    return GestureDetector(
+      behavior: HitTestBehavior.translucent,
+      onTap: () {},
+      child: Container(
+        width: width,
+        decoration: BoxDecoration(
+          border: Border.all(color: Colors.black26, width: 0.5),
+          color: GridHeaderConstants.backgroundColor,
+        ),
+        padding: EdgeInsets.symmetric(vertical: GridInsets.vertical, horizontal: GridInsets.horizontal),
+        child: child,
+      ),
+    );
+  }
+}
+
+class HeaderCellLeading extends StatelessWidget {
+  const HeaderCellLeading({Key? key}) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    return Container(
+      width: GridSize.firstHeaderPadding,
+      color: GridHeaderConstants.backgroundColor,
+    );
+  }
+}

+ 3 - 0
frontend/app_flowy/packages/flowy_sdk/lib/dispatch/dispatch.dart

@@ -9,6 +9,7 @@ import 'package:flowy_sdk/protobuf/flowy-net/event.pb.dart';
 import 'package:flowy_sdk/protobuf/flowy-net/network_state.pb.dart';
 import 'package:flowy_sdk/protobuf/flowy-user/event_map.pb.dart';
 import 'package:flowy_sdk/protobuf/flowy-folder/event_map.pb.dart';
+import 'package:flowy_sdk/protobuf/flowy-grid/event_map.pb.dart';
 import 'package:isolates/isolates.dart';
 import 'package:isolates/ports.dart';
 import 'package:ffi/ffi.dart';
@@ -21,6 +22,7 @@ import 'package:flowy_sdk/protobuf/flowy-user-data-model/protobuf.dart';
 import 'package:flowy_sdk/protobuf/dart-ffi/protobuf.dart';
 import 'package:flowy_sdk/protobuf/flowy-folder-data-model/protobuf.dart';
 import 'package:flowy_sdk/protobuf/flowy-collaboration/protobuf.dart';
+import 'package:flowy_sdk/protobuf/flowy-grid-data-model/protobuf.dart';
 
 // ignore: unused_import
 import 'package:protobuf/protobuf.dart';
@@ -30,6 +32,7 @@ import 'error.dart';
 part 'dart_event/flowy-folder/dart_event.dart';
 part 'dart_event/flowy-net/dart_event.dart';
 part 'dart_event/flowy-user/dart_event.dart';
+part 'dart_event/flowy-grid/dart_event.dart';
 
 enum FFIException {
   RequestIsEmpty,

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

@@ -725,7 +725,6 @@ class Cell extends $pb.GeneratedMessage {
     ..aOS(1, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'id')
     ..aOS(2, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'rowId')
     ..aOS(3, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'fieldId')
-    ..aOM<AnyData>(4, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'data', subBuilder: AnyData.create)
     ..hasRequiredFields = false
   ;
 
@@ -734,7 +733,6 @@ class Cell extends $pb.GeneratedMessage {
     $core.String? id,
     $core.String? rowId,
     $core.String? fieldId,
-    AnyData? data,
   }) {
     final _result = create();
     if (id != null) {
@@ -746,9 +744,6 @@ class Cell extends $pb.GeneratedMessage {
     if (fieldId != null) {
       _result.fieldId = fieldId;
     }
-    if (data != null) {
-      _result.data = data;
-    }
     return _result;
   }
   factory Cell.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
@@ -798,17 +793,130 @@ class Cell extends $pb.GeneratedMessage {
   $core.bool hasFieldId() => $_has(2);
   @$pb.TagNumber(3)
   void clearFieldId() => clearField(3);
+}
 
-  @$pb.TagNumber(4)
-  AnyData get data => $_getN(3);
-  @$pb.TagNumber(4)
-  set data(AnyData v) { setField(4, v); }
-  @$pb.TagNumber(4)
-  $core.bool hasData() => $_has(3);
-  @$pb.TagNumber(4)
-  void clearData() => clearField(4);
-  @$pb.TagNumber(4)
-  AnyData ensureData() => $_ensure(3);
+class DisplayCell extends $pb.GeneratedMessage {
+  static final $pb.BuilderInfo _i = $pb.BuilderInfo(const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'DisplayCell', createEmptyInstance: create)
+    ..aOS(1, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'id')
+    ..aOS(2, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'content')
+    ..hasRequiredFields = false
+  ;
+
+  DisplayCell._() : super();
+  factory DisplayCell({
+    $core.String? id,
+    $core.String? content,
+  }) {
+    final _result = create();
+    if (id != null) {
+      _result.id = id;
+    }
+    if (content != null) {
+      _result.content = content;
+    }
+    return _result;
+  }
+  factory DisplayCell.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
+  factory DisplayCell.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')
+  DisplayCell clone() => DisplayCell()..mergeFromMessage(this);
+  @$core.Deprecated(
+  'Using this can add significant overhead to your binary. '
+  'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
+  'Will be removed in next major version')
+  DisplayCell copyWith(void Function(DisplayCell) updates) => super.copyWith((message) => updates(message as DisplayCell)) as DisplayCell; // ignore: deprecated_member_use
+  $pb.BuilderInfo get info_ => _i;
+  @$core.pragma('dart2js:noInline')
+  static DisplayCell create() => DisplayCell._();
+  DisplayCell createEmptyInstance() => create();
+  static $pb.PbList<DisplayCell> createRepeated() => $pb.PbList<DisplayCell>();
+  @$core.pragma('dart2js:noInline')
+  static DisplayCell getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<DisplayCell>(create);
+  static DisplayCell? _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 content => $_getSZ(1);
+  @$pb.TagNumber(2)
+  set content($core.String v) { $_setString(1, v); }
+  @$pb.TagNumber(2)
+  $core.bool hasContent() => $_has(1);
+  @$pb.TagNumber(2)
+  void clearContent() => clearField(2);
+}
+
+class RawCell extends $pb.GeneratedMessage {
+  static final $pb.BuilderInfo _i = $pb.BuilderInfo(const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'RawCell', createEmptyInstance: create)
+    ..aOS(1, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'id')
+    ..aOM<AnyData>(2, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'data', subBuilder: AnyData.create)
+    ..hasRequiredFields = false
+  ;
+
+  RawCell._() : super();
+  factory RawCell({
+    $core.String? id,
+    AnyData? data,
+  }) {
+    final _result = create();
+    if (id != null) {
+      _result.id = id;
+    }
+    if (data != null) {
+      _result.data = data;
+    }
+    return _result;
+  }
+  factory RawCell.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
+  factory RawCell.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')
+  RawCell clone() => RawCell()..mergeFromMessage(this);
+  @$core.Deprecated(
+  'Using this can add significant overhead to your binary. '
+  'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
+  'Will be removed in next major version')
+  RawCell copyWith(void Function(RawCell) updates) => super.copyWith((message) => updates(message as RawCell)) as RawCell; // ignore: deprecated_member_use
+  $pb.BuilderInfo get info_ => _i;
+  @$core.pragma('dart2js:noInline')
+  static RawCell create() => RawCell._();
+  RawCell createEmptyInstance() => create();
+  static $pb.PbList<RawCell> createRepeated() => $pb.PbList<RawCell>();
+  @$core.pragma('dart2js:noInline')
+  static RawCell getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<RawCell>(create);
+  static RawCell? _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)
+  AnyData get data => $_getN(1);
+  @$pb.TagNumber(2)
+  set data(AnyData v) { setField(2, v); }
+  @$pb.TagNumber(2)
+  $core.bool hasData() => $_has(1);
+  @$pb.TagNumber(2)
+  void clearData() => clearField(2);
+  @$pb.TagNumber(2)
+  AnyData ensureData() => $_ensure(1);
 }
 
 class CreateGridPayload extends $pb.GeneratedMessage {

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

@@ -159,12 +159,33 @@ const Cell$json = const {
     const {'1': 'id', '3': 1, '4': 1, '5': 9, '10': 'id'},
     const {'1': 'row_id', '3': 2, '4': 1, '5': 9, '10': 'rowId'},
     const {'1': 'field_id', '3': 3, '4': 1, '5': 9, '10': 'fieldId'},
-    const {'1': 'data', '3': 4, '4': 1, '5': 11, '6': '.AnyData', '10': 'data'},
   ],
 };
 
 /// Descriptor for `Cell`. Decode as a `google.protobuf.DescriptorProto`.
-final $typed_data.Uint8List cellDescriptor = $convert.base64Decode('CgRDZWxsEg4KAmlkGAEgASgJUgJpZBIVCgZyb3dfaWQYAiABKAlSBXJvd0lkEhkKCGZpZWxkX2lkGAMgASgJUgdmaWVsZElkEhwKBGRhdGEYBCABKAsyCC5BbnlEYXRhUgRkYXRh');
+final $typed_data.Uint8List cellDescriptor = $convert.base64Decode('CgRDZWxsEg4KAmlkGAEgASgJUgJpZBIVCgZyb3dfaWQYAiABKAlSBXJvd0lkEhkKCGZpZWxkX2lkGAMgASgJUgdmaWVsZElk');
+@$core.Deprecated('Use displayCellDescriptor instead')
+const DisplayCell$json = const {
+  '1': 'DisplayCell',
+  '2': const [
+    const {'1': 'id', '3': 1, '4': 1, '5': 9, '10': 'id'},
+    const {'1': 'content', '3': 2, '4': 1, '5': 9, '10': 'content'},
+  ],
+};
+
+/// Descriptor for `DisplayCell`. Decode as a `google.protobuf.DescriptorProto`.
+final $typed_data.Uint8List displayCellDescriptor = $convert.base64Decode('CgtEaXNwbGF5Q2VsbBIOCgJpZBgBIAEoCVICaWQSGAoHY29udGVudBgCIAEoCVIHY29udGVudA==');
+@$core.Deprecated('Use rawCellDescriptor instead')
+const RawCell$json = const {
+  '1': 'RawCell',
+  '2': const [
+    const {'1': 'id', '3': 1, '4': 1, '5': 9, '10': 'id'},
+    const {'1': 'data', '3': 2, '4': 1, '5': 11, '6': '.AnyData', '10': 'data'},
+  ],
+};
+
+/// Descriptor for `RawCell`. Decode as a `google.protobuf.DescriptorProto`.
+final $typed_data.Uint8List rawCellDescriptor = $convert.base64Decode('CgdSYXdDZWxsEg4KAmlkGAEgASgJUgJpZBIcCgRkYXRhGAIgASgLMgguQW55RGF0YVIEZGF0YQ==');
 @$core.Deprecated('Use createGridPayloadDescriptor instead')
 const CreateGridPayload$json = const {
   '1': 'CreateGridPayload',

+ 16 - 1
shared-lib/flowy-grid-data-model/src/entities/grid.rs

@@ -142,8 +142,23 @@ pub struct Cell {
 
     #[pb(index = 3)]
     pub field_id: String,
+}
 
-    #[pb(index = 4)]
+#[derive(Debug, Default, ProtoBuf)]
+pub struct DisplayCell {
+    #[pb(index = 1)]
+    pub id: String,
+
+    #[pb(index = 2)]
+    pub content: String,
+}
+
+#[derive(Debug, Default, ProtoBuf)]
+pub struct RawCell {
+    #[pb(index = 1)]
+    pub id: String,
+
+    #[pb(index = 2)]
     pub data: AnyData,
 }
 

+ 418 - 56
shared-lib/flowy-grid-data-model/src/protobuf/model/grid.rs

@@ -2405,7 +2405,6 @@ pub struct Cell {
     pub id: ::std::string::String,
     pub row_id: ::std::string::String,
     pub field_id: ::std::string::String,
-    pub data: ::protobuf::SingularPtrField<AnyData>,
     // special fields
     pub unknown_fields: ::protobuf::UnknownFields,
     pub cached_size: ::protobuf::CachedSize,
@@ -2499,8 +2498,399 @@ impl Cell {
     pub fn take_field_id(&mut self) -> ::std::string::String {
         ::std::mem::replace(&mut self.field_id, ::std::string::String::new())
     }
+}
+
+impl ::protobuf::Message for Cell {
+    fn is_initialized(&self) -> bool {
+        true
+    }
+
+    fn merge_from(&mut self, is: &mut ::protobuf::CodedInputStream<'_>) -> ::protobuf::ProtobufResult<()> {
+        while !is.eof()? {
+            let (field_number, wire_type) = is.read_tag_unpack()?;
+            match field_number {
+                1 => {
+                    ::protobuf::rt::read_singular_proto3_string_into(wire_type, is, &mut self.id)?;
+                },
+                2 => {
+                    ::protobuf::rt::read_singular_proto3_string_into(wire_type, is, &mut self.row_id)?;
+                },
+                3 => {
+                    ::protobuf::rt::read_singular_proto3_string_into(wire_type, is, &mut self.field_id)?;
+                },
+                _ => {
+                    ::protobuf::rt::read_unknown_or_skip_group(field_number, wire_type, is, self.mut_unknown_fields())?;
+                },
+            };
+        }
+        ::std::result::Result::Ok(())
+    }
+
+    // Compute sizes of nested messages
+    #[allow(unused_variables)]
+    fn compute_size(&self) -> u32 {
+        let mut my_size = 0;
+        if !self.id.is_empty() {
+            my_size += ::protobuf::rt::string_size(1, &self.id);
+        }
+        if !self.row_id.is_empty() {
+            my_size += ::protobuf::rt::string_size(2, &self.row_id);
+        }
+        if !self.field_id.is_empty() {
+            my_size += ::protobuf::rt::string_size(3, &self.field_id);
+        }
+        my_size += ::protobuf::rt::unknown_fields_size(self.get_unknown_fields());
+        self.cached_size.set(my_size);
+        my_size
+    }
+
+    fn write_to_with_cached_sizes(&self, os: &mut ::protobuf::CodedOutputStream<'_>) -> ::protobuf::ProtobufResult<()> {
+        if !self.id.is_empty() {
+            os.write_string(1, &self.id)?;
+        }
+        if !self.row_id.is_empty() {
+            os.write_string(2, &self.row_id)?;
+        }
+        if !self.field_id.is_empty() {
+            os.write_string(3, &self.field_id)?;
+        }
+        os.write_unknown_fields(self.get_unknown_fields())?;
+        ::std::result::Result::Ok(())
+    }
+
+    fn get_cached_size(&self) -> u32 {
+        self.cached_size.get()
+    }
+
+    fn get_unknown_fields(&self) -> &::protobuf::UnknownFields {
+        &self.unknown_fields
+    }
+
+    fn mut_unknown_fields(&mut self) -> &mut ::protobuf::UnknownFields {
+        &mut self.unknown_fields
+    }
+
+    fn as_any(&self) -> &dyn (::std::any::Any) {
+        self as &dyn (::std::any::Any)
+    }
+    fn as_any_mut(&mut self) -> &mut dyn (::std::any::Any) {
+        self as &mut dyn (::std::any::Any)
+    }
+    fn into_any(self: ::std::boxed::Box<Self>) -> ::std::boxed::Box<dyn (::std::any::Any)> {
+        self
+    }
+
+    fn descriptor(&self) -> &'static ::protobuf::reflect::MessageDescriptor {
+        Self::descriptor_static()
+    }
+
+    fn new() -> Cell {
+        Cell::new()
+    }
+
+    fn descriptor_static() -> &'static ::protobuf::reflect::MessageDescriptor {
+        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::ProtobufTypeString>(
+                "id",
+                |m: &Cell| { &m.id },
+                |m: &mut Cell| { &mut m.id },
+            ));
+            fields.push(::protobuf::reflect::accessor::make_simple_field_accessor::<_, ::protobuf::types::ProtobufTypeString>(
+                "row_id",
+                |m: &Cell| { &m.row_id },
+                |m: &mut Cell| { &mut m.row_id },
+            ));
+            fields.push(::protobuf::reflect::accessor::make_simple_field_accessor::<_, ::protobuf::types::ProtobufTypeString>(
+                "field_id",
+                |m: &Cell| { &m.field_id },
+                |m: &mut Cell| { &mut m.field_id },
+            ));
+            ::protobuf::reflect::MessageDescriptor::new_pb_name::<Cell>(
+                "Cell",
+                fields,
+                file_descriptor_proto()
+            )
+        })
+    }
+
+    fn default_instance() -> &'static Cell {
+        static instance: ::protobuf::rt::LazyV2<Cell> = ::protobuf::rt::LazyV2::INIT;
+        instance.get(Cell::new)
+    }
+}
+
+impl ::protobuf::Clear for Cell {
+    fn clear(&mut self) {
+        self.id.clear();
+        self.row_id.clear();
+        self.field_id.clear();
+        self.unknown_fields.clear();
+    }
+}
+
+impl ::std::fmt::Debug for Cell {
+    fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
+        ::protobuf::text_format::fmt(self, f)
+    }
+}
+
+impl ::protobuf::reflect::ProtobufValue for Cell {
+    fn as_ref(&self) -> ::protobuf::reflect::ReflectValueRef {
+        ::protobuf::reflect::ReflectValueRef::Message(self)
+    }
+}
+
+#[derive(PartialEq,Clone,Default)]
+pub struct DisplayCell {
+    // message fields
+    pub id: ::std::string::String,
+    pub content: ::std::string::String,
+    // special fields
+    pub unknown_fields: ::protobuf::UnknownFields,
+    pub cached_size: ::protobuf::CachedSize,
+}
+
+impl<'a> ::std::default::Default for &'a DisplayCell {
+    fn default() -> &'a DisplayCell {
+        <DisplayCell as ::protobuf::Message>::default_instance()
+    }
+}
+
+impl DisplayCell {
+    pub fn new() -> DisplayCell {
+        ::std::default::Default::default()
+    }
 
-    // .AnyData data = 4;
+    // string id = 1;
+
+
+    pub fn get_id(&self) -> &str {
+        &self.id
+    }
+    pub fn clear_id(&mut self) {
+        self.id.clear();
+    }
+
+    // Param is passed by value, moved
+    pub fn set_id(&mut self, v: ::std::string::String) {
+        self.id = v;
+    }
+
+    // Mutable pointer to the field.
+    // If field is not initialized, it is initialized with default value first.
+    pub fn mut_id(&mut self) -> &mut ::std::string::String {
+        &mut self.id
+    }
+
+    // Take field
+    pub fn take_id(&mut self) -> ::std::string::String {
+        ::std::mem::replace(&mut self.id, ::std::string::String::new())
+    }
+
+    // string content = 2;
+
+
+    pub fn get_content(&self) -> &str {
+        &self.content
+    }
+    pub fn clear_content(&mut self) {
+        self.content.clear();
+    }
+
+    // Param is passed by value, moved
+    pub fn set_content(&mut self, v: ::std::string::String) {
+        self.content = v;
+    }
+
+    // Mutable pointer to the field.
+    // If field is not initialized, it is initialized with default value first.
+    pub fn mut_content(&mut self) -> &mut ::std::string::String {
+        &mut self.content
+    }
+
+    // Take field
+    pub fn take_content(&mut self) -> ::std::string::String {
+        ::std::mem::replace(&mut self.content, ::std::string::String::new())
+    }
+}
+
+impl ::protobuf::Message for DisplayCell {
+    fn is_initialized(&self) -> bool {
+        true
+    }
+
+    fn merge_from(&mut self, is: &mut ::protobuf::CodedInputStream<'_>) -> ::protobuf::ProtobufResult<()> {
+        while !is.eof()? {
+            let (field_number, wire_type) = is.read_tag_unpack()?;
+            match field_number {
+                1 => {
+                    ::protobuf::rt::read_singular_proto3_string_into(wire_type, is, &mut self.id)?;
+                },
+                2 => {
+                    ::protobuf::rt::read_singular_proto3_string_into(wire_type, is, &mut self.content)?;
+                },
+                _ => {
+                    ::protobuf::rt::read_unknown_or_skip_group(field_number, wire_type, is, self.mut_unknown_fields())?;
+                },
+            };
+        }
+        ::std::result::Result::Ok(())
+    }
+
+    // Compute sizes of nested messages
+    #[allow(unused_variables)]
+    fn compute_size(&self) -> u32 {
+        let mut my_size = 0;
+        if !self.id.is_empty() {
+            my_size += ::protobuf::rt::string_size(1, &self.id);
+        }
+        if !self.content.is_empty() {
+            my_size += ::protobuf::rt::string_size(2, &self.content);
+        }
+        my_size += ::protobuf::rt::unknown_fields_size(self.get_unknown_fields());
+        self.cached_size.set(my_size);
+        my_size
+    }
+
+    fn write_to_with_cached_sizes(&self, os: &mut ::protobuf::CodedOutputStream<'_>) -> ::protobuf::ProtobufResult<()> {
+        if !self.id.is_empty() {
+            os.write_string(1, &self.id)?;
+        }
+        if !self.content.is_empty() {
+            os.write_string(2, &self.content)?;
+        }
+        os.write_unknown_fields(self.get_unknown_fields())?;
+        ::std::result::Result::Ok(())
+    }
+
+    fn get_cached_size(&self) -> u32 {
+        self.cached_size.get()
+    }
+
+    fn get_unknown_fields(&self) -> &::protobuf::UnknownFields {
+        &self.unknown_fields
+    }
+
+    fn mut_unknown_fields(&mut self) -> &mut ::protobuf::UnknownFields {
+        &mut self.unknown_fields
+    }
+
+    fn as_any(&self) -> &dyn (::std::any::Any) {
+        self as &dyn (::std::any::Any)
+    }
+    fn as_any_mut(&mut self) -> &mut dyn (::std::any::Any) {
+        self as &mut dyn (::std::any::Any)
+    }
+    fn into_any(self: ::std::boxed::Box<Self>) -> ::std::boxed::Box<dyn (::std::any::Any)> {
+        self
+    }
+
+    fn descriptor(&self) -> &'static ::protobuf::reflect::MessageDescriptor {
+        Self::descriptor_static()
+    }
+
+    fn new() -> DisplayCell {
+        DisplayCell::new()
+    }
+
+    fn descriptor_static() -> &'static ::protobuf::reflect::MessageDescriptor {
+        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::ProtobufTypeString>(
+                "id",
+                |m: &DisplayCell| { &m.id },
+                |m: &mut DisplayCell| { &mut m.id },
+            ));
+            fields.push(::protobuf::reflect::accessor::make_simple_field_accessor::<_, ::protobuf::types::ProtobufTypeString>(
+                "content",
+                |m: &DisplayCell| { &m.content },
+                |m: &mut DisplayCell| { &mut m.content },
+            ));
+            ::protobuf::reflect::MessageDescriptor::new_pb_name::<DisplayCell>(
+                "DisplayCell",
+                fields,
+                file_descriptor_proto()
+            )
+        })
+    }
+
+    fn default_instance() -> &'static DisplayCell {
+        static instance: ::protobuf::rt::LazyV2<DisplayCell> = ::protobuf::rt::LazyV2::INIT;
+        instance.get(DisplayCell::new)
+    }
+}
+
+impl ::protobuf::Clear for DisplayCell {
+    fn clear(&mut self) {
+        self.id.clear();
+        self.content.clear();
+        self.unknown_fields.clear();
+    }
+}
+
+impl ::std::fmt::Debug for DisplayCell {
+    fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
+        ::protobuf::text_format::fmt(self, f)
+    }
+}
+
+impl ::protobuf::reflect::ProtobufValue for DisplayCell {
+    fn as_ref(&self) -> ::protobuf::reflect::ReflectValueRef {
+        ::protobuf::reflect::ReflectValueRef::Message(self)
+    }
+}
+
+#[derive(PartialEq,Clone,Default)]
+pub struct RawCell {
+    // message fields
+    pub id: ::std::string::String,
+    pub data: ::protobuf::SingularPtrField<AnyData>,
+    // special fields
+    pub unknown_fields: ::protobuf::UnknownFields,
+    pub cached_size: ::protobuf::CachedSize,
+}
+
+impl<'a> ::std::default::Default for &'a RawCell {
+    fn default() -> &'a RawCell {
+        <RawCell as ::protobuf::Message>::default_instance()
+    }
+}
+
+impl RawCell {
+    pub fn new() -> RawCell {
+        ::std::default::Default::default()
+    }
+
+    // string id = 1;
+
+
+    pub fn get_id(&self) -> &str {
+        &self.id
+    }
+    pub fn clear_id(&mut self) {
+        self.id.clear();
+    }
+
+    // Param is passed by value, moved
+    pub fn set_id(&mut self, v: ::std::string::String) {
+        self.id = v;
+    }
+
+    // Mutable pointer to the field.
+    // If field is not initialized, it is initialized with default value first.
+    pub fn mut_id(&mut self) -> &mut ::std::string::String {
+        &mut self.id
+    }
+
+    // Take field
+    pub fn take_id(&mut self) -> ::std::string::String {
+        ::std::mem::replace(&mut self.id, ::std::string::String::new())
+    }
+
+    // .AnyData data = 2;
 
 
     pub fn get_data(&self) -> &AnyData {
@@ -2534,7 +2924,7 @@ impl Cell {
     }
 }
 
-impl ::protobuf::Message for Cell {
+impl ::protobuf::Message for RawCell {
     fn is_initialized(&self) -> bool {
         for v in &self.data {
             if !v.is_initialized() {
@@ -2552,12 +2942,6 @@ impl ::protobuf::Message for Cell {
                     ::protobuf::rt::read_singular_proto3_string_into(wire_type, is, &mut self.id)?;
                 },
                 2 => {
-                    ::protobuf::rt::read_singular_proto3_string_into(wire_type, is, &mut self.row_id)?;
-                },
-                3 => {
-                    ::protobuf::rt::read_singular_proto3_string_into(wire_type, is, &mut self.field_id)?;
-                },
-                4 => {
                     ::protobuf::rt::read_singular_message_into(wire_type, is, &mut self.data)?;
                 },
                 _ => {
@@ -2575,12 +2959,6 @@ impl ::protobuf::Message for Cell {
         if !self.id.is_empty() {
             my_size += ::protobuf::rt::string_size(1, &self.id);
         }
-        if !self.row_id.is_empty() {
-            my_size += ::protobuf::rt::string_size(2, &self.row_id);
-        }
-        if !self.field_id.is_empty() {
-            my_size += ::protobuf::rt::string_size(3, &self.field_id);
-        }
         if let Some(ref v) = self.data.as_ref() {
             let len = v.compute_size();
             my_size += 1 + ::protobuf::rt::compute_raw_varint32_size(len) + len;
@@ -2594,14 +2972,8 @@ impl ::protobuf::Message for Cell {
         if !self.id.is_empty() {
             os.write_string(1, &self.id)?;
         }
-        if !self.row_id.is_empty() {
-            os.write_string(2, &self.row_id)?;
-        }
-        if !self.field_id.is_empty() {
-            os.write_string(3, &self.field_id)?;
-        }
         if let Some(ref v) = self.data.as_ref() {
-            os.write_tag(4, ::protobuf::wire_format::WireTypeLengthDelimited)?;
+            os.write_tag(2, ::protobuf::wire_format::WireTypeLengthDelimited)?;
             os.write_raw_varint32(v.get_cached_size())?;
             v.write_to_with_cached_sizes(os)?;
         }
@@ -2635,8 +3007,8 @@ impl ::protobuf::Message for Cell {
         Self::descriptor_static()
     }
 
-    fn new() -> Cell {
-        Cell::new()
+    fn new() -> RawCell {
+        RawCell::new()
     }
 
     fn descriptor_static() -> &'static ::protobuf::reflect::MessageDescriptor {
@@ -2645,55 +3017,43 @@ impl ::protobuf::Message for Cell {
             let mut fields = ::std::vec::Vec::new();
             fields.push(::protobuf::reflect::accessor::make_simple_field_accessor::<_, ::protobuf::types::ProtobufTypeString>(
                 "id",
-                |m: &Cell| { &m.id },
-                |m: &mut Cell| { &mut m.id },
-            ));
-            fields.push(::protobuf::reflect::accessor::make_simple_field_accessor::<_, ::protobuf::types::ProtobufTypeString>(
-                "row_id",
-                |m: &Cell| { &m.row_id },
-                |m: &mut Cell| { &mut m.row_id },
-            ));
-            fields.push(::protobuf::reflect::accessor::make_simple_field_accessor::<_, ::protobuf::types::ProtobufTypeString>(
-                "field_id",
-                |m: &Cell| { &m.field_id },
-                |m: &mut Cell| { &mut m.field_id },
+                |m: &RawCell| { &m.id },
+                |m: &mut RawCell| { &mut m.id },
             ));
             fields.push(::protobuf::reflect::accessor::make_singular_ptr_field_accessor::<_, ::protobuf::types::ProtobufTypeMessage<AnyData>>(
                 "data",
-                |m: &Cell| { &m.data },
-                |m: &mut Cell| { &mut m.data },
+                |m: &RawCell| { &m.data },
+                |m: &mut RawCell| { &mut m.data },
             ));
-            ::protobuf::reflect::MessageDescriptor::new_pb_name::<Cell>(
-                "Cell",
+            ::protobuf::reflect::MessageDescriptor::new_pb_name::<RawCell>(
+                "RawCell",
                 fields,
                 file_descriptor_proto()
             )
         })
     }
 
-    fn default_instance() -> &'static Cell {
-        static instance: ::protobuf::rt::LazyV2<Cell> = ::protobuf::rt::LazyV2::INIT;
-        instance.get(Cell::new)
+    fn default_instance() -> &'static RawCell {
+        static instance: ::protobuf::rt::LazyV2<RawCell> = ::protobuf::rt::LazyV2::INIT;
+        instance.get(RawCell::new)
     }
 }
 
-impl ::protobuf::Clear for Cell {
+impl ::protobuf::Clear for RawCell {
     fn clear(&mut self) {
         self.id.clear();
-        self.row_id.clear();
-        self.field_id.clear();
         self.data.clear();
         self.unknown_fields.clear();
     }
 }
 
-impl ::std::fmt::Debug for Cell {
+impl ::std::fmt::Debug for RawCell {
     fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
         ::protobuf::text_format::fmt(self, f)
     }
 }
 
-impl ::protobuf::reflect::ProtobufValue for Cell {
+impl ::protobuf::reflect::ProtobufValue for RawCell {
     fn as_ref(&self) -> ::protobuf::reflect::ReflectValueRef {
         ::protobuf::reflect::ReflectValueRef::Message(self)
     }
@@ -3107,15 +3467,17 @@ static file_descriptor_proto_data: &'static [u8] = b"\
     difiedTime\x12@\n\x10cell_by_field_id\x18\x04\x20\x03(\x0b2\x17.Row.Cell\
     ByFieldIdEntryR\rcellByFieldId\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\"f\n\x04Cell\x12\x0e\n\x02id\x18\x01\
+    \x0b2\x05.CellR\x05value:\x028\x01\"H\n\x04Cell\x12\x0e\n\x02id\x18\x01\
     \x20\x01(\tR\x02id\x12\x15\n\x06row_id\x18\x02\x20\x01(\tR\x05rowId\x12\
-    \x19\n\x08field_id\x18\x03\x20\x01(\tR\x07fieldId\x12\x1c\n\x04data\x18\
-    \x04\x20\x01(\x0b2\x08.AnyDataR\x04data\"'\n\x11CreateGridPayload\x12\
-    \x12\n\x04name\x18\x01\x20\x01(\tR\x04name\"\x1e\n\x06GridId\x12\x14\n\
-    \x05value\x18\x01\x20\x01(\tR\x05value*d\n\tFieldType\x12\x0c\n\x08RichT\
-    ext\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\
+    \x19\n\x08field_id\x18\x03\x20\x01(\tR\x07fieldId\"7\n\x0bDisplayCell\
+    \x12\x0e\n\x02id\x18\x01\x20\x01(\tR\x02id\x12\x18\n\x07content\x18\x02\
+    \x20\x01(\tR\x07content\"7\n\x07RawCell\x12\x0e\n\x02id\x18\x01\x20\x01(\
+    \tR\x02id\x12\x1c\n\x04data\x18\x02\x20\x01(\x0b2\x08.AnyDataR\x04data\"\
+    '\n\x11CreateGridPayload\x12\x12\n\x04name\x18\x01\x20\x01(\tR\x04name\"\
+    \x1e\n\x06GridId\x12\x14\n\x05value\x18\x01\x20\x01(\tR\x05value*d\n\tFi\
+    eldType\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\x0bMult\
+    iSelect\x10\x04\x12\x0c\n\x08Checkbox\x10\x05b\x06proto3\
 ";
 
 static file_descriptor_proto_lazy: ::protobuf::rt::LazyV2<::protobuf::descriptor::FileDescriptorProto> = ::protobuf::rt::LazyV2::INIT;

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

@@ -52,7 +52,14 @@ message Cell {
     string id = 1;
     string row_id = 2;
     string field_id = 3;
-    AnyData data = 4;
+}
+message DisplayCell {
+    string id = 1;
+    string content = 2;
+}
+message RawCell {
+    string id = 1;
+    AnyData data = 2;
 }
 message CreateGridPayload {
     string name = 1;