瀏覽代碼

feat: move kanban blocks (#2022)

* chore: add edit / create field test

* chore: add delete field test

* chore: change log class arguments

* chore: delete/create row

* chore: set tracing log to debug level

* fix: filter notification with id

* chore: add get single select type option data

* fix: high cpu usage

* chore: format code

* chore: update tokio version

* chore: config tokio runtime subscriber

* chore: add profiling feature

* chore: setup auto login

* chore: fix tauri build

* chore: (unstable) using controllers

* fix: initially authenticated and serializable fix

* fix: ci warning

* ci: compile error

* fix: new folder trash overflow

* fix: min width for nav panel

* fix: nav panel and main panel animation on hide menu

* fix: highlight active page

* fix: post merge fixes

* fix: post merge fix

* fix: remove warnings

* fix: change IDatabaseField fix eslint errors

* chore: create cell component for each field type

* chore: move cell hook into custom cell component

* chore: refactor row hook

* chore: add tauri clean

* chore: add tauri clean

* chore: save offset top of nav items

* chore: move constants

* fix: nav item popup overflow

* fix: page rename position

* chore: remove offset top

* chore: remove floating menu functions

* chore: scroll down to new page

* chore: smooth scroll and scroll to new folder

* fix: breadcrumbs

* chore: back and forward buttons nav scroll fix

* chore: get board groups and rows

* chore: set log level & remove empty line

* fix: create kanban board row

* fix: appflowy session name

* chore: import beautiful dnd

* bug: kanban new row

* chore: update refs

* fix: dispose group controller

* fix: dispose cell controller

* chore: move rows in group

* chore: move row into other block

* fix: groups observer dispose

* chore: dnd reordering

* chore: fix import references

* chore: initial edit board modal

* fix: kanban board rendering

* chore: add column and edit text cell

* chore: column rename

* chore: edit row components reorganize

* chore: don't show group by field

* wip: edit cell type

* chore: fade in, out

* chore: change field type

* chore: update editing cell

* chore: fade in change

* chore: cell options layout

* fix: padding fixes for cell wrapper

* fix: cell options positions

* chore: cell options write to backend

* fix: select options for new row

* chore: edit url cell

* chore: language button

* fix: close popup on lang select

* fix: save url cell

* chore: date picker

* chore: small code cleanups

* chore: options in board

* chore: move fields dnd

---------

Co-authored-by: nathan <[email protected]>
Co-authored-by: Nathan.fooo <[email protected]>
Co-authored-by: appflowy <[email protected]>
Askarbek Zadauly 2 年之前
父節點
當前提交
fe524dbc78
共有 88 個文件被更改,包括 1660 次插入196 次删除
  1. 6 2
      frontend/appflowy_tauri/package.json
  2. 34 0
      frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/CellOptions.tsx
  3. 152 0
      frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/CellOptionsPopup.tsx
  4. 71 0
      frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/ChangeFieldTypePopup.tsx
  5. 97 0
      frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/DatePickerPopup.tsx
  6. 24 0
      frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/EditCellDate.tsx
  7. 29 0
      frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/EditCellNumber.tsx
  8. 41 0
      frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/EditCellText.tsx
  9. 31 0
      frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/EditCellUrl.tsx
  10. 102 0
      frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/EditCellWrapper.tsx
  11. 23 0
      frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/EditCheckboxCell.tsx
  12. 130 0
      frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/EditFieldPopup.tsx
  13. 210 0
      frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/EditRow.tsx
  14. 24 0
      frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/FieldTypeIcon.tsx
  15. 18 0
      frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/FieldTypeName.tsx
  16. 4 1
      frontend/appflowy_tauri/src/appflowy_app/components/_shared/LanguageSelectPopup.tsx
  17. 1 1
      frontend/appflowy_tauri/src/appflowy_app/components/_shared/database-hooks/loadField.ts
  18. 25 14
      frontend/appflowy_tauri/src/appflowy_app/components/_shared/database-hooks/useCell.ts
  19. 36 8
      frontend/appflowy_tauri/src/appflowy_app/components/_shared/database-hooks/useDatabase.ts
  20. 19 7
      frontend/appflowy_tauri/src/appflowy_app/components/_shared/database-hooks/useRow.ts
  21. 7 0
      frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/ArrowLeftSvg.tsx
  22. 7 0
      frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/ArrowRightSvg.tsx
  23. 13 0
      frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/CheckboxSvg.tsx
  24. 7 0
      frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/CheckmarkSvg.tsx
  25. 15 0
      frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/ClockSvg.tsx
  26. 8 0
      frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/EditorCheckSvg.tsx
  27. 7 0
      frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/EditorUncheckSvg.tsx
  28. 10 0
      frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/MoreSvg.tsx
  29. 9 0
      frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/SkipLeftSvg.tsx
  30. 9 0
      frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/SkipRightSvg.tsx
  31. 43 18
      frontend/appflowy_tauri/src/appflowy_app/components/board/Board.tsx
  32. 41 17
      frontend/appflowy_tauri/src/appflowy_app/components/board/BoardBlock.tsx
  33. 34 18
      frontend/appflowy_tauri/src/appflowy_app/components/board/BoardCard.tsx
  34. 7 0
      frontend/appflowy_tauri/src/appflowy_app/components/board/BoardCell.tsx
  35. 7 3
      frontend/appflowy_tauri/src/appflowy_app/components/board/BoardOptionsCell.tsx
  36. 10 4
      frontend/appflowy_tauri/src/appflowy_app/components/board/BoardTextCell.tsx
  37. 29 0
      frontend/appflowy_tauri/src/appflowy_app/components/board/BoardUrlCell.tsx
  38. 2 2
      frontend/appflowy_tauri/src/appflowy_app/components/error/ErrorModal.tsx
  39. 1 1
      frontend/appflowy_tauri/src/appflowy_app/components/grid/GridTableHeader/GridTableHeader.hooks.tsx
  40. 1 1
      frontend/appflowy_tauri/src/appflowy_app/components/grid/GridTableHeader/GridTableHeader.tsx
  41. 15 0
      frontend/appflowy_tauri/src/appflowy_app/components/layout/HeaderPanel/LanguageButton.tsx
  42. 4 1
      frontend/appflowy_tauri/src/appflowy_app/components/layout/HeaderPanel/PageOptions.tsx
  43. 1 1
      frontend/appflowy_tauri/src/appflowy_app/components/layout/NavigationPanel/FolderItem.hooks.ts
  44. 1 1
      frontend/appflowy_tauri/src/appflowy_app/components/layout/NavigationPanel/NavigationPanel.hooks.ts
  45. 1 1
      frontend/appflowy_tauri/src/appflowy_app/components/layout/NavigationPanel/PageItem.tsx
  46. 2 2
      frontend/appflowy_tauri/src/appflowy_app/components/user/application/notifications/parser.ts
  47. 2 2
      frontend/appflowy_tauri/src/appflowy_app/components/user/application/notifications/user_listener.ts
  48. 2 3
      frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/cell/cell_bd_svc.ts
  49. 1 0
      frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/cell/cell_controller.ts
  50. 2 2
      frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/cell/cell_observer.ts
  51. 1 1
      frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/cell/controller_builder.ts
  52. 2 4
      frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/cell/data_parser.ts
  53. 2 4
      frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/cell/data_persistence.ts
  54. 2 2
      frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/cell/select_option_bd_svc.ts
  55. 37 3
      frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/database_bd_svc.ts
  56. 30 3
      frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/database_controller.ts
  57. 2 2
      frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/field/field_bd_svc.ts
  58. 3 3
      frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/field/field_controller.ts
  59. 2 2
      frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/field/field_observer.ts
  60. 2 2
      frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/field/type_option/type_option_bd_svc.ts
  61. 1 1
      frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/field/type_option/type_option_context.ts
  62. 3 3
      frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/field/type_option/type_option_controller.ts
  63. 3 9
      frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/group/group_controller.ts
  64. 2 2
      frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/group/group_observer.ts
  65. 2 2
      frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/notifications/observer.ts
  66. 2 2
      frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/notifications/parser.ts
  67. 2 2
      frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/row/row_bd_svc.ts
  68. 4 4
      frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/row/row_cache.ts
  69. 1 2
      frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/view/database_view_cache.ts
  70. 2 2
      frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/view/view_row_observer.ts
  71. 3 3
      frontend/appflowy_tauri/src/appflowy_app/stores/effects/document/document_bd_svc.ts
  72. 2 2
      frontend/appflowy_tauri/src/appflowy_app/stores/effects/folder/app/app_bd_svc.ts
  73. 2 2
      frontend/appflowy_tauri/src/appflowy_app/stores/effects/folder/app/app_observer.ts
  74. 2 2
      frontend/appflowy_tauri/src/appflowy_app/stores/effects/folder/notifications/observer.ts
  75. 2 2
      frontend/appflowy_tauri/src/appflowy_app/stores/effects/folder/notifications/parser.ts
  76. 2 2
      frontend/appflowy_tauri/src/appflowy_app/stores/effects/folder/view/view_bd_svc.ts
  77. 2 3
      frontend/appflowy_tauri/src/appflowy_app/stores/effects/folder/view/view_observer.ts
  78. 2 2
      frontend/appflowy_tauri/src/appflowy_app/stores/effects/folder/workspace/workspace_bd_svc.ts
  79. 2 2
      frontend/appflowy_tauri/src/appflowy_app/stores/effects/folder/workspace/workspace_observer.ts
  80. 3 3
      frontend/appflowy_tauri/src/appflowy_app/stores/effects/user/user_bd_svc.ts
  81. 1 1
      frontend/appflowy_tauri/src/appflowy_app/stores/reducers/current-user/slice.ts
  82. 2 2
      frontend/appflowy_tauri/src/appflowy_app/stores/reducers/database/slice.ts
  83. 2 2
      frontend/appflowy_tauri/src/appflowy_app/stores/reducers/folders/notifications/parser.ts
  84. 1 1
      frontend/appflowy_tauri/src/appflowy_app/stores/reducers/grid/slice.ts
  85. 1 1
      frontend/appflowy_tauri/src/appflowy_app/stores/reducers/pages/slice.ts
  86. 1 0
      frontend/appflowy_tauri/src/main.tsx
  87. 141 0
      frontend/appflowy_tauri/src/styles/Calendar.css
  88. 9 1
      frontend/appflowy_tauri/tailwind.config.cjs

+ 6 - 2
frontend/appflowy_tauri/package.json

@@ -23,6 +23,7 @@
     "@slate-yjs/core": "^0.3.1",
     "@tanstack/react-virtual": "3.0.0-beta.54",
     "@tauri-apps/api": "^1.2.0",
+    "dayjs": "^1.11.7",
     "events": "^3.3.0",
     "google-protobuf": "^3.21.2",
     "i18next": "^22.4.10",
@@ -32,6 +33,8 @@
     "nanoid": "^4.0.0",
     "protoc-gen-ts": "^0.8.5",
     "react": "^18.2.0",
+    "react-beautiful-dnd": "^13.1.1",
+    "react-calendar": "^4.1.0",
     "react-dom": "^18.2.0",
     "react-error-boundary": "^3.1.4",
     "react-i18next": "^12.2.0",
@@ -44,8 +47,8 @@
     "slate-react": "^0.91.9",
     "ts-results": "^3.3.0",
     "utf8": "^3.0.0",
-    "yjs": "^13.5.51",
-		"y-indexeddb": "^9.0.9"
+    "y-indexeddb": "^9.0.9",
+    "yjs": "^13.5.51"
   },
   "devDependencies": {
     "@tauri-apps/cli": "^1.2.2",
@@ -53,6 +56,7 @@
     "@types/is-hotkey": "^0.1.7",
     "@types/node": "^18.7.10",
     "@types/react": "^18.0.15",
+    "@types/react-beautiful-dnd": "^13.1.3",
     "@types/react-dom": "^18.0.6",
     "@types/utf8": "^3.0.1",
     "@types/uuid": "^9.0.1",

+ 34 - 0
frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/CellOptions.tsx

@@ -0,0 +1,34 @@
+import { SelectOptionCellDataPB } from '@/services/backend';
+import { getBgColor } from '$app/components/_shared/getColor';
+import { useRef } from 'react';
+
+export const CellOptions = ({
+  data,
+  onEditClick,
+}: {
+  data: SelectOptionCellDataPB | undefined;
+  onEditClick: (left: number, top: number) => void;
+}) => {
+  const ref = useRef<HTMLDivElement>(null);
+
+  const onClick = () => {
+    if (!ref.current) return;
+    const { left, top } = ref.current.getBoundingClientRect();
+    onEditClick(left, top);
+  };
+
+  return (
+    <div
+      ref={ref}
+      onClick={() => onClick()}
+      className={'flex flex-wrap items-center gap-2 px-4 py-2 text-xs text-black'}
+    >
+      {data?.select_options?.map((option, index) => (
+        <div className={`${getBgColor(option.color)} rounded px-2 py-0.5`} key={index}>
+          {option?.name || ''}
+        </div>
+      )) || ''}
+      &nbsp;
+    </div>
+  );
+};

+ 152 - 0
frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/CellOptionsPopup.tsx

@@ -0,0 +1,152 @@
+import { KeyboardEventHandler, useEffect, useRef, useState } from 'react';
+import { CellIdentifier } from '$app/stores/effects/database/cell/cell_bd_svc';
+import { useCell } from '$app/components/_shared/database-hooks/useCell';
+import { CellCache } from '$app/stores/effects/database/cell/cell_cache';
+import { FieldController } from '$app/stores/effects/database/field/field_controller';
+import { SelectOptionCellDataPB, SelectOptionColorPB, SelectOptionPB } from '@/services/backend';
+import { getBgColor } from '$app/components/_shared/getColor';
+import { useTranslation } from 'react-i18next';
+import { Details2Svg } from '$app/components/_shared/svg/Details2Svg';
+import { CheckmarkSvg } from '$app/components/_shared/svg/CheckmarkSvg';
+import { CloseSvg } from '$app/components/_shared/svg/CloseSvg';
+import useOutsideClick from '$app/components/_shared/useOutsideClick';
+import { SelectOptionCellBackendService } from '$app/stores/effects/database/cell/select_option_bd_svc';
+import { useAppSelector } from '$app/stores/store';
+import { ISelectOptionType } from '$app/stores/reducers/database/slice';
+
+export const CellOptionsPopup = ({
+  top,
+  left,
+  cellIdentifier,
+  cellCache,
+  fieldController,
+  onOutsideClick,
+}: {
+  top: number;
+  left: number;
+  cellIdentifier: CellIdentifier;
+  cellCache: CellCache;
+  fieldController: FieldController;
+  onOutsideClick: () => void;
+}) => {
+  const ref = useRef<HTMLDivElement>(null);
+  const { t } = useTranslation('');
+  const [adjustedTop, setAdjustedTop] = useState(-100);
+  const [value, setValue] = useState('');
+  const { data, cellController } = useCell(cellIdentifier, cellCache, fieldController);
+  const databaseStore = useAppSelector((state) => state.database);
+
+  useEffect(() => {
+    if (!ref.current) return;
+    const { height } = ref.current.getBoundingClientRect();
+    if (top + height + 40 > window.innerHeight) {
+      setAdjustedTop(window.innerHeight - height - 40);
+    } else {
+      setAdjustedTop(top);
+    }
+  }, [ref, window, top, left]);
+
+  useOutsideClick(ref, async () => {
+    onOutsideClick();
+  });
+
+  const onKeyDown: KeyboardEventHandler = async (e) => {
+    if (e.key === 'Enter' && value.length > 0) {
+      await new SelectOptionCellBackendService(cellIdentifier).createOption({ name: value });
+      setValue('');
+    }
+  };
+
+  const onUnselectOptionClick = async (option: SelectOptionPB) => {
+    await new SelectOptionCellBackendService(cellIdentifier).unselectOption([option.id]);
+    setValue('');
+  };
+
+  const onToggleOptionClick = async (option: SelectOptionPB) => {
+    if (
+      (data as SelectOptionCellDataPB | undefined)?.select_options?.find(
+        (selectedOption) => selectedOption.id === option.id
+      )
+    ) {
+      await new SelectOptionCellBackendService(cellIdentifier).unselectOption([option.id]);
+    } else {
+      await new SelectOptionCellBackendService(cellIdentifier).selectOption([option.id]);
+    }
+    setValue('');
+  };
+
+  useEffect(() => {
+    console.log('loaded data: ', data);
+    console.log('have stored ', databaseStore.fields[cellIdentifier.fieldId]);
+  }, [data]);
+
+  return (
+    <div
+      ref={ref}
+      className={`fixed z-10 rounded-lg bg-white px-2 py-2 text-xs shadow-md transition-opacity duration-300 ${
+        adjustedTop === -100 ? 'opacity-0' : 'opacity-100'
+      }`}
+      style={{ top: `${adjustedTop + 40}px`, left: `${left}px` }}
+    >
+      <div className={'flex flex-col gap-2 p-2'}>
+        <div className={'border-shades-3 flex flex-1 items-center gap-2 rounded border bg-main-selector px-2 '}>
+          <div className={'flex flex-wrap items-center gap-2 text-black'}>
+            {(data as SelectOptionCellDataPB | undefined)?.select_options?.map((option, index) => (
+              <div className={`${getBgColor(option.color)} flex items-center gap-0.5 rounded px-1 py-0.5`} key={index}>
+                <span>{option?.name || ''}</span>
+                <button onClick={() => onUnselectOptionClick(option)} className={'h-5 w-5 cursor-pointer'}>
+                  <CloseSvg></CloseSvg>{' '}
+                </button>
+              </div>
+            )) || ''}
+          </div>
+          <input
+            className={'py-2'}
+            value={value}
+            onChange={(e) => setValue(e.target.value)}
+            placeholder={t('grid.selectOption.searchOption') || ''}
+            onKeyDown={onKeyDown}
+          />
+          <div className={'font-mono text-shade-3'}>{value.length}/30</div>
+        </div>
+        <div className={'-mx-4 h-[1px] bg-shade-6'}></div>
+        <div className={'font-semibold text-shade-3'}>{t('grid.selectOption.panelTitle') || ''}</div>
+        <div className={'flex flex-col gap-1'}>
+          {(databaseStore.fields[cellIdentifier.fieldId]?.fieldOptions as ISelectOptionType).selectOptions.map(
+            (option, index) => (
+              <div
+                key={index}
+                onClick={() =>
+                  onToggleOptionClick(
+                    new SelectOptionPB({
+                      id: option.selectOptionId,
+                      name: option.title,
+                      color: option.color || SelectOptionColorPB.Purple,
+                    })
+                  )
+                }
+                className={
+                  'flex cursor-pointer items-center justify-between rounded-lg px-2 py-1.5 hover:bg-main-secondary'
+                }
+              >
+                <div className={`${getBgColor(option.color)} rounded px-2 py-0.5`}>{option.title}</div>
+                <div className={'flex items-center'}>
+                  {(data as SelectOptionCellDataPB | undefined)?.select_options?.find(
+                    (selectedOption) => selectedOption.id === option.selectOptionId
+                  ) && (
+                    <button className={'h-5 w-5 p-1'}>
+                      <CheckmarkSvg></CheckmarkSvg>
+                    </button>
+                  )}
+                  <button className={'h-6 w-6 p-1'}>
+                    <Details2Svg></Details2Svg>
+                  </button>
+                </div>
+              </div>
+            )
+          )}
+        </div>
+      </div>
+    </div>
+  );
+};

+ 71 - 0
frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/ChangeFieldTypePopup.tsx

@@ -0,0 +1,71 @@
+import { FieldType } from '@/services/backend';
+import { FieldTypeIcon } from '$app/components/_shared/EditRow/FieldTypeIcon';
+import { FieldTypeName } from '$app/components/_shared/EditRow/FieldTypeName';
+import { useEffect, useMemo, useRef, useState } from 'react';
+import useOutsideClick from '$app/components/_shared/useOutsideClick';
+
+const typesOrder: FieldType[] = [
+  FieldType.RichText,
+  FieldType.Number,
+  FieldType.DateTime,
+  FieldType.SingleSelect,
+  FieldType.MultiSelect,
+  FieldType.Checkbox,
+  FieldType.URL,
+  FieldType.Checklist,
+];
+
+export const ChangeFieldTypePopup = ({
+  top,
+  right,
+  onClick,
+  onOutsideClick,
+}: {
+  top: number;
+  right: number;
+  onClick: (newType: FieldType) => void;
+  onOutsideClick: () => void;
+}) => {
+  const ref = useRef<HTMLDivElement>(null);
+  const [adjustedTop, setAdjustedTop] = useState(-100);
+  useOutsideClick(ref, async () => {
+    onOutsideClick();
+  });
+
+  useEffect(() => {
+    if (!ref.current) return;
+    const { height } = ref.current.getBoundingClientRect();
+    if (top + height > window.innerHeight) {
+      setAdjustedTop(window.innerHeight - height);
+    } else {
+      setAdjustedTop(top);
+    }
+  }, [ref, window, top, right]);
+
+  return (
+    <div
+      ref={ref}
+      className={`fixed z-10 rounded-lg bg-white p-2 text-xs shadow-md transition-opacity duration-300 ${
+        adjustedTop === -100 ? 'opacity-0' : 'opacity-100'
+      }`}
+      style={{ top: `${adjustedTop}px`, left: `${right + 30}px` }}
+    >
+      <div className={'flex flex-col'}>
+        {typesOrder.map((t, i) => (
+          <button
+            onClick={() => onClick(t)}
+            key={i}
+            className={'flex cursor-pointer items-center gap-2 rounded-lg px-2 py-2 pr-8 hover:bg-main-secondary'}
+          >
+            <i className={'h-5 w-5'}>
+              <FieldTypeIcon fieldType={t}></FieldTypeIcon>
+            </i>
+            <span>
+              <FieldTypeName fieldType={t}></FieldTypeName>
+            </span>
+          </button>
+        ))}
+      </div>
+    </div>
+  );
+};

+ 97 - 0
frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/DatePickerPopup.tsx

@@ -0,0 +1,97 @@
+import { useEffect, useRef, useState } from 'react';
+import { useTranslation } from 'react-i18next';
+import { CellIdentifier } from '$app/stores/effects/database/cell/cell_bd_svc';
+import { CellCache } from '$app/stores/effects/database/cell/cell_cache';
+import { FieldController } from '$app/stores/effects/database/field/field_controller';
+import useOutsideClick from '$app/components/_shared/useOutsideClick';
+import Calendar from 'react-calendar';
+import dayjs from 'dayjs';
+import { ClockSvg } from '$app/components/_shared/svg/ClockSvg';
+import { MoreSvg } from '$app/components/_shared/svg/MoreSvg';
+import { EditorUncheckSvg } from '$app/components/_shared/svg/EditorUncheckSvg';
+import { useCell } from '$app/components/_shared/database-hooks/useCell';
+
+export const DatePickerPopup = ({
+  left,
+  top,
+  cellIdentifier,
+  cellCache,
+  fieldController,
+  onOutsideClick,
+}: {
+  left: number;
+  top: number;
+  cellIdentifier: CellIdentifier;
+  cellCache: CellCache;
+  fieldController: FieldController;
+  onOutsideClick: () => void;
+}) => {
+  const { data, cellController } = useCell(cellIdentifier, cellCache, fieldController);
+  const ref = useRef<HTMLDivElement>(null);
+  const [adjustedTop, setAdjustedTop] = useState(-100);
+  // const [value, setValue] = useState();
+  const { t } = useTranslation('');
+  const [selectedDate, setSelectedDate] = useState<Date>(new Date());
+
+  useEffect(() => {
+    if (!ref.current) return;
+    const { height } = ref.current.getBoundingClientRect();
+    if (top + height + 40 > window.innerHeight) {
+      setAdjustedTop(top - height - 40);
+    } else {
+      setAdjustedTop(top);
+    }
+  }, [ref, window, top, left]);
+
+  useOutsideClick(ref, async () => {
+    onOutsideClick();
+  });
+
+  useEffect(() => {
+    // console.log((data as DateCellDataPB).date);
+    // setSelectedDate(new Date((data as DateCellDataPB).date));
+  }, [data]);
+
+  const onChange = (v: Date | null | (Date | null)[]) => {
+    if (v instanceof Date) {
+      console.log(dayjs(v).format('YYYY-MM-DD'));
+      setSelectedDate(v);
+      // void cellController?.saveCellData(new DateCellDataPB({ date: dayjs(v).format('YYYY-MM-DD') }));
+    }
+  };
+
+  return (
+    <div
+      ref={ref}
+      className={`fixed z-10 rounded-lg bg-white px-2 py-2 text-xs shadow-md transition-opacity duration-300 ${
+        adjustedTop === -100 ? 'opacity-0' : 'opacity-100'
+      }`}
+      style={{ top: `${adjustedTop + 40}px`, left: `${left}px` }}
+    >
+      <div className={'px-2'}>
+        <Calendar onChange={(d) => onChange(d)} value={selectedDate} />
+      </div>
+      <hr className={'-mx-2 my-4 border-shade-6'} />
+      <div className={'flex items-center justify-between px-4'}>
+        <div className={'flex items-center gap-2'}>
+          <i className={'h-4 w-4'}>
+            <ClockSvg></ClockSvg>
+          </i>
+          <span>{t('grid.field.includeTime')}</span>
+        </div>
+        <i className={'h-5 w-5'}>
+          <EditorUncheckSvg></EditorUncheckSvg>
+        </i>
+      </div>
+      <hr className={'-mx-2 my-4 border-shade-6'} />
+      <div className={'flex items-center justify-between px-4 pb-2'}>
+        <span>
+          {t('grid.field.dateFormat')} & {t('grid.field.timeFormat')}
+        </span>
+        <i className={'h-5 w-5'}>
+          <MoreSvg></MoreSvg>
+        </i>
+      </div>
+    </div>
+  );
+};

+ 24 - 0
frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/EditCellDate.tsx

@@ -0,0 +1,24 @@
+import { useRef } from 'react';
+import { DateCellDataPB } from '@/services/backend';
+
+export const EditCellDate = ({
+  data,
+  onEditClick,
+}: {
+  data?: DateCellDataPB;
+  onEditClick: (left: number, top: number) => void;
+}) => {
+  const ref = useRef<HTMLDivElement>(null);
+
+  const onClick = () => {
+    if (!ref.current) return;
+    const { left, top } = ref.current.getBoundingClientRect();
+    onEditClick(left, top);
+  };
+
+  return (
+    <div ref={ref} onClick={() => onClick()} className={'px-4 py-2'}>
+      {data?.date || <>&nbsp;</>}
+    </div>
+  );
+};

+ 29 - 0
frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/EditCellNumber.tsx

@@ -0,0 +1,29 @@
+import { CellController } from '$app/stores/effects/database/cell/cell_controller';
+import { useEffect, useState } from 'react';
+
+export const EditCellNumber = ({
+  data,
+  cellController,
+}: {
+  data: string | undefined;
+  cellController: CellController<any, any>;
+}) => {
+  const [value, setValue] = useState('');
+
+  useEffect(() => {
+    setValue(data || '');
+  }, [data]);
+
+  const save = async () => {
+    await cellController?.saveCellData(value);
+  };
+
+  return (
+    <input
+      value={value}
+      onChange={(e) => setValue(e.target.value)}
+      onBlur={() => save()}
+      className={'w-full px-4 py-2'}
+    ></input>
+  );
+};

+ 41 - 0
frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/EditCellText.tsx

@@ -0,0 +1,41 @@
+import { CellController } from '$app/stores/effects/database/cell/cell_controller';
+import { useEffect, useState, KeyboardEvent, useMemo } from 'react';
+
+export const EditCellText = ({
+  data,
+  cellController,
+}: {
+  data: string | undefined;
+  cellController: CellController<any, any>;
+}) => {
+  const [value, setValue] = useState('');
+  const [contentRows, setContentRows] = useState(1);
+
+  useEffect(() => {
+    setValue(data || '');
+  }, [data]);
+
+  useEffect(() => {
+    setContentRows(Math.max(1, (value || '').split('\n').length));
+  }, [value]);
+
+  const onTextFieldChange = async (v: string) => {
+    setValue(v);
+  };
+
+  const save = async () => {
+    await cellController?.saveCellData(value);
+  };
+
+  return (
+    <div className={''}>
+      <textarea
+        className={'mt-0.5 h-full w-full resize-none px-4 py-2'}
+        rows={contentRows}
+        value={value}
+        onChange={(e) => onTextFieldChange(e.target.value)}
+        onBlur={() => save()}
+      />
+    </div>
+  );
+};

+ 31 - 0
frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/EditCellUrl.tsx

@@ -0,0 +1,31 @@
+import { URLCellDataPB } from '@/services/backend';
+import { CellController } from '$app/stores/effects/database/cell/cell_controller';
+import { useEffect, useState } from 'react';
+import { URLCellController } from '$app/stores/effects/database/cell/controller_builder';
+
+export const EditCellUrl = ({
+  data,
+  cellController,
+}: {
+  data: URLCellDataPB | undefined;
+  cellController: CellController<any, any>;
+}) => {
+  const [value, setValue] = useState('');
+
+  useEffect(() => {
+    setValue((data as URLCellDataPB)?.url || '');
+  }, [data]);
+
+  const save = async () => {
+    await (cellController as URLCellController)?.saveCellData(value);
+  };
+
+  return (
+    <input
+      value={value}
+      onChange={(e) => setValue(e.target.value)}
+      onBlur={() => save()}
+      className={'w-full px-4 py-2'}
+    ></input>
+  );
+};

+ 102 - 0
frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/EditCellWrapper.tsx

@@ -0,0 +1,102 @@
+import { CellIdentifier } from '$app/stores/effects/database/cell/cell_bd_svc';
+import { useCell } from '$app/components/_shared/database-hooks/useCell';
+import { CellCache } from '$app/stores/effects/database/cell/cell_cache';
+import { FieldController } from '$app/stores/effects/database/field/field_controller';
+import { DateCellDataPB, FieldType, SelectOptionCellDataPB, URLCellDataPB } from '@/services/backend';
+import { useAppSelector } from '$app/stores/store';
+import { EditCellText } from '$app/components/_shared/EditRow/EditCellText';
+import { FieldTypeIcon } from '$app/components/_shared/EditRow/FieldTypeIcon';
+import { EditCellDate } from '$app/components/_shared/EditRow/EditCellDate';
+import { useRef } from 'react';
+import { CellOptions } from '$app/components/_shared/EditRow/CellOptions';
+import { EditCellNumber } from '$app/components/_shared/EditRow/EditCellNumber';
+import { EditCheckboxCell } from '$app/components/_shared/EditRow/EditCheckboxCell';
+import { EditCellUrl } from '$app/components/_shared/EditRow/EditCellUrl';
+import { Draggable } from 'react-beautiful-dnd';
+
+export const EditCellWrapper = ({
+  index,
+  cellIdentifier,
+  cellCache,
+  fieldController,
+  onEditFieldClick,
+  onEditOptionsClick,
+  onEditDateClick,
+}: {
+  index: number;
+  cellIdentifier: CellIdentifier;
+  cellCache: CellCache;
+  fieldController: FieldController;
+  onEditFieldClick: (top: number, right: number) => void;
+  onEditOptionsClick: (left: number, top: number) => void;
+  onEditDateClick: (left: number, top: number) => void;
+}) => {
+  const { data, cellController } = useCell(cellIdentifier, cellCache, fieldController);
+  const databaseStore = useAppSelector((state) => state.database);
+  const el = useRef<HTMLDivElement>(null);
+
+  const onClick = () => {
+    if (!el.current) return;
+    const { top, right } = el.current.getBoundingClientRect();
+    onEditFieldClick(top, right);
+  };
+
+  return (
+    <Draggable draggableId={cellIdentifier.fieldId} index={index}>
+      {(provided) => (
+        <div
+          ref={provided.innerRef}
+          {...provided.draggableProps}
+          {...provided.dragHandleProps}
+          className={'flex w-full items-center text-xs'}
+        >
+          <div
+            ref={el}
+            onClick={() => onClick()}
+            className={
+              'relative flex w-[180px] cursor-pointer items-center gap-2 rounded-lg px-3 py-1.5 hover:bg-shade-6'
+            }
+          >
+            <div className={'flex h-5 w-5 flex-shrink-0 items-center justify-center'}>
+              <FieldTypeIcon fieldType={cellIdentifier.fieldType}></FieldTypeIcon>
+            </div>
+            <span className={'overflow-hidden text-ellipsis whitespace-nowrap'}>
+              {databaseStore.fields[cellIdentifier.fieldId].title}
+            </span>
+          </div>
+          <div className={'flex-1 cursor-pointer rounded-lg hover:bg-shade-6'}>
+            {(cellIdentifier.fieldType === FieldType.SingleSelect ||
+              cellIdentifier.fieldType === FieldType.MultiSelect ||
+              cellIdentifier.fieldType === FieldType.Checklist) &&
+              cellController && (
+                <CellOptions
+                  data={data as SelectOptionCellDataPB | undefined}
+                  onEditClick={onEditOptionsClick}
+                ></CellOptions>
+              )}
+
+            {cellIdentifier.fieldType === FieldType.Checkbox && cellController && (
+              <EditCheckboxCell data={data as boolean | undefined} cellController={cellController}></EditCheckboxCell>
+            )}
+
+            {cellIdentifier.fieldType === FieldType.DateTime && (
+              <EditCellDate data={data as DateCellDataPB | undefined} onEditClick={onEditDateClick}></EditCellDate>
+            )}
+
+            {cellIdentifier.fieldType === FieldType.Number && cellController && (
+              <EditCellNumber data={data as string | undefined} cellController={cellController}></EditCellNumber>
+            )}
+
+            {cellIdentifier.fieldType === FieldType.URL && cellController && (
+              <EditCellUrl data={data as URLCellDataPB | undefined} cellController={cellController}></EditCellUrl>
+            )}
+
+            {cellIdentifier.fieldType === FieldType.RichText && cellController && (
+              <EditCellText data={data as string | undefined} cellController={cellController}></EditCellText>
+            )}
+          </div>
+        </div>
+      )}
+    </Draggable>
+  );
+};

+ 23 - 0
frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/EditCheckboxCell.tsx

@@ -0,0 +1,23 @@
+import { EditorCheckSvg } from '$app/components/_shared/svg/EditorCheckSvg';
+import { EditorUncheckSvg } from '$app/components/_shared/svg/EditorUncheckSvg';
+import { CellController } from '$app/stores/effects/database/cell/cell_controller';
+
+export const EditCheckboxCell = ({
+  data,
+  cellController,
+}: {
+  data: boolean | undefined;
+  cellController: CellController<any, any>;
+}) => {
+  const toggleValue = async () => {
+    await cellController?.saveCellData(!data);
+  };
+
+  return (
+    <div onClick={() => toggleValue()} className={'block px-4 py-2'}>
+      <button className={'h-5 w-5'}>
+        {data ? <EditorCheckSvg></EditorCheckSvg> : <EditorUncheckSvg></EditorUncheckSvg>}
+      </button>
+    </div>
+  );
+};

+ 130 - 0
frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/EditFieldPopup.tsx

@@ -0,0 +1,130 @@
+import { useEffect, useRef, useState } from 'react';
+import useOutsideClick from '$app/components/_shared/useOutsideClick';
+import { TrashSvg } from '$app/components/_shared/svg/TrashSvg';
+import { FieldTypeIcon } from '$app/components/_shared/EditRow/FieldTypeIcon';
+import { FieldTypeName } from '$app/components/_shared/EditRow/FieldTypeName';
+import { useTranslation } from 'react-i18next';
+import { TypeOptionController } from '$app/stores/effects/database/field/type_option/type_option_controller';
+import { Some } from 'ts-results';
+import { FieldInfo } from '$app/stores/effects/database/field/field_controller';
+import { MoreSvg } from '$app/components/_shared/svg/MoreSvg';
+import { useAppSelector } from '$app/stores/store';
+import { CellIdentifier } from '$app/stores/effects/database/cell/cell_bd_svc';
+
+export const EditFieldPopup = ({
+  top,
+  right,
+  cellIdentifier,
+  viewId,
+  onOutsideClick,
+  fieldInfo,
+  changeFieldTypeClick,
+}: {
+  top: number;
+  right: number;
+  cellIdentifier: CellIdentifier;
+  viewId: string;
+  onOutsideClick: () => void;
+  fieldInfo: FieldInfo | undefined;
+  changeFieldTypeClick: (buttonTop: number, buttonRight: number) => void;
+}) => {
+  const databaseStore = useAppSelector((state) => state.database);
+  const { t } = useTranslation('');
+  const ref = useRef<HTMLDivElement>(null);
+  const changeTypeButtonRef = useRef<HTMLDivElement>(null);
+  const [name, setName] = useState('');
+
+  const [adjustedTop, setAdjustedTop] = useState(-100);
+
+  useOutsideClick(ref, async () => {
+    await save();
+    onOutsideClick();
+  });
+
+  useEffect(() => {
+    setName(databaseStore.fields[cellIdentifier.fieldId].title);
+  }, [databaseStore, cellIdentifier]);
+
+  useEffect(() => {
+    if (!ref.current) return;
+    const { height } = ref.current.getBoundingClientRect();
+    if (top + height > window.innerHeight) {
+      setAdjustedTop(window.innerHeight - height);
+    } else {
+      setAdjustedTop(top);
+    }
+  }, [ref, window, top, right]);
+
+  const save = async () => {
+    if (!fieldInfo) return;
+    const controller = new TypeOptionController(viewId, Some(fieldInfo));
+    await controller.initialize();
+    await controller.setFieldName(name);
+  };
+
+  const onChangeFieldTypeClick = () => {
+    if (!changeTypeButtonRef.current) return;
+    const { top: buttonTop, right: buttonRight } = changeTypeButtonRef.current.getBoundingClientRect();
+    changeFieldTypeClick(buttonTop, buttonRight);
+  };
+
+  // this is causing an error right now
+  const onDeleteFieldClick = async () => {
+    if (!fieldInfo) return;
+    const controller = new TypeOptionController(viewId, Some(fieldInfo));
+    await controller.initialize();
+    await controller.deleteField();
+    onOutsideClick();
+  };
+
+  return (
+    <div
+      ref={ref}
+      className={`fixed z-10 rounded-lg bg-white px-2 py-2 text-xs shadow-md transition-opacity duration-300 ${
+        adjustedTop === -100 ? 'opacity-0' : 'opacity-100'
+      }`}
+      style={{ top: `${adjustedTop}px`, left: `${right + 10}px` }}
+    >
+      <div className={'flex flex-col gap-2 p-2'}>
+        <input
+          value={name}
+          onChange={(e) => setName(e.target.value)}
+          onBlur={() => save()}
+          className={'border-shades-3 flex-1 rounded border bg-main-selector px-2 py-2'}
+        />
+
+        <button
+          onClick={() => onDeleteFieldClick()}
+          className={
+            'flex cursor-pointer items-center gap-2 rounded-lg px-2 py-2 text-main-alert hover:bg-main-secondary'
+          }
+        >
+          <i className={'h-5 w-5'}>
+            <TrashSvg></TrashSvg>
+          </i>
+          <span>{t('grid.field.delete')}</span>
+        </button>
+
+        <div
+          ref={changeTypeButtonRef}
+          onClick={() => onChangeFieldTypeClick()}
+          className={
+            'relative flex cursor-pointer items-center justify-between rounded-lg text-black hover:bg-main-secondary'
+          }
+        >
+          <button className={'flex cursor-pointer items-center gap-2 rounded-lg px-2 py-2'}>
+            <i className={'h-5 w-5'}>
+              <FieldTypeIcon fieldType={cellIdentifier.fieldType}></FieldTypeIcon>
+            </i>
+            <span>
+              <FieldTypeName fieldType={cellIdentifier.fieldType}></FieldTypeName>
+            </span>
+          </button>
+          <i className={'h-5 w-5'}>
+            <MoreSvg></MoreSvg>
+          </i>
+        </div>
+      </div>
+    </div>
+  );
+};

+ 210 - 0
frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/EditRow.tsx

@@ -0,0 +1,210 @@
+import { CloseSvg } from '$app/components/_shared/svg/CloseSvg';
+import { useRow } from '$app/components/_shared/database-hooks/useRow';
+import { DatabaseController } from '$app/stores/effects/database/database_controller';
+import { RowInfo } from '$app/stores/effects/database/row/row_cache';
+import { EditCellWrapper } from '$app/components/_shared/EditRow/EditCellWrapper';
+import AddSvg from '$app/components/_shared/svg/AddSvg';
+import { useTranslation } from 'react-i18next';
+import { EditFieldPopup } from '$app/components/_shared/EditRow/EditFieldPopup';
+import { useEffect, useState } from 'react';
+import { CellIdentifier } from '$app/stores/effects/database/cell/cell_bd_svc';
+import { ChangeFieldTypePopup } from '$app/components/_shared/EditRow/ChangeFieldTypePopup';
+import { TypeOptionController } from '$app/stores/effects/database/field/type_option/type_option_controller';
+import { Some } from 'ts-results';
+import { FieldType } from '@/services/backend';
+import { CellOptionsPopup } from '$app/components/_shared/EditRow/CellOptionsPopup';
+import { DatePickerPopup } from '$app/components/_shared/EditRow/DatePickerPopup';
+import { DragDropContext, Droppable, OnDragEndResponder } from 'react-beautiful-dnd';
+
+export const EditRow = ({
+  onClose,
+  viewId,
+  controller,
+  rowInfo,
+}: {
+  onClose: () => void;
+  viewId: string;
+  controller: DatabaseController;
+  rowInfo: RowInfo;
+}) => {
+  const { cells, onNewColumnClick } = useRow(viewId, controller, rowInfo);
+  const { t } = useTranslation('');
+  const [unveil, setUnveil] = useState(false);
+
+  const [editingCell, setEditingCell] = useState<CellIdentifier | null>(null);
+  const [showFieldEditor, setShowFieldEditor] = useState(false);
+  const [editFieldTop, setEditFieldTop] = useState(0);
+  const [editFieldRight, setEditFieldRight] = useState(0);
+
+  const [showChangeFieldTypePopup, setShowChangeFieldTypePopup] = useState(false);
+  const [changeFieldTypeTop, setChangeFieldTypeTop] = useState(0);
+  const [changeFieldTypeRight, setChangeFieldTypeRight] = useState(0);
+
+  const [showChangeOptionsPopup, setShowChangeOptionsPopup] = useState(false);
+  const [changeOptionsTop, setChangeOptionsTop] = useState(0);
+  const [changeOptionsLeft, setChangeOptionsLeft] = useState(0);
+
+  const [showDatePicker, setShowDatePicker] = useState(false);
+  const [datePickerTop, setDatePickerTop] = useState(0);
+  const [datePickerLeft, setDatePickerLeft] = useState(0);
+
+  useEffect(() => {
+    setUnveil(true);
+  }, []);
+
+  const onCloseClick = () => {
+    setUnveil(false);
+    setTimeout(() => {
+      onClose();
+    }, 300);
+  };
+
+  const onEditFieldClick = (cellIdentifier: CellIdentifier, top: number, right: number) => {
+    setEditingCell(cellIdentifier);
+    setEditFieldTop(top);
+    setEditFieldRight(right);
+    setShowFieldEditor(true);
+  };
+
+  const onOutsideEditFieldClick = () => {
+    if (!showChangeFieldTypePopup) {
+      setShowFieldEditor(false);
+    }
+  };
+
+  const onChangeFieldTypeClick = (buttonTop: number, buttonRight: number) => {
+    setChangeFieldTypeTop(buttonTop);
+    setChangeFieldTypeRight(buttonRight);
+    setShowChangeFieldTypePopup(true);
+  };
+
+  const changeFieldType = async (newType: FieldType) => {
+    if (!editingCell) return;
+
+    const currentField = controller.fieldController.getField(editingCell.fieldId);
+    if (!currentField) return;
+
+    const typeOptionController = new TypeOptionController(viewId, Some(currentField));
+    await typeOptionController.switchToField(newType);
+
+    setEditingCell(new CellIdentifier(viewId, rowInfo.row.id, editingCell.fieldId, newType));
+
+    setShowChangeFieldTypePopup(false);
+  };
+
+  const onEditOptionsClick = async (cellIdentifier: CellIdentifier, left: number, top: number) => {
+    setEditingCell(cellIdentifier);
+    setChangeOptionsLeft(left);
+    setChangeOptionsTop(top);
+    setShowChangeOptionsPopup(true);
+  };
+
+  const onEditDateClick = async (cellIdentifier: CellIdentifier, left: number, top: number) => {
+    setEditingCell(cellIdentifier);
+    setDatePickerLeft(left);
+    setDatePickerTop(top);
+    setShowDatePicker(true);
+  };
+
+  const onDragEnd: OnDragEndResponder = (result) => {
+    if (!result.destination?.index) return;
+    void controller.moveField(result.source.droppableId, result.source.index, result.destination.index);
+  };
+
+  return (
+    <div
+      className={`fixed inset-0 z-10 flex items-center justify-center bg-black/30 backdrop-blur-sm transition-opacity duration-300 ${
+        unveil ? 'opacity-100' : 'opacity-0'
+      }`}
+    >
+      <div className={`relative flex h-[90%] w-[70%] flex-col gap-8 rounded-xl bg-white px-8 pb-4 pt-12`}>
+        <div onClick={() => onCloseClick()} className={'absolute top-4 right-4'}>
+          <button className={'block h-8 w-8 rounded-lg text-shade-2 hover:bg-main-secondary'}>
+            <CloseSvg></CloseSvg>
+          </button>
+        </div>
+
+        <DragDropContext onDragEnd={onDragEnd}>
+          <Droppable droppableId={'field-list'}>
+            {(provided) => (
+              <div
+                {...provided.droppableProps}
+                ref={provided.innerRef}
+                className={`flex flex-1 flex-col gap-2 ${
+                  showFieldEditor || showChangeOptionsPopup || showDatePicker ? 'overflow-hidden' : 'overflow-auto'
+                }`}
+              >
+                {cells.map((cell, cellIndex) => (
+                  <EditCellWrapper
+                    index={cellIndex}
+                    key={cellIndex}
+                    cellIdentifier={cell.cellIdentifier}
+                    cellCache={controller.databaseViewCache.getRowCache().getCellCache()}
+                    fieldController={controller.fieldController}
+                    onEditFieldClick={(top: number, right: number) => onEditFieldClick(cell.cellIdentifier, top, right)}
+                    onEditOptionsClick={(left: number, top: number) =>
+                      onEditOptionsClick(cell.cellIdentifier, left, top)
+                    }
+                    onEditDateClick={(left: number, top: number) => onEditDateClick(cell.cellIdentifier, left, top)}
+                  ></EditCellWrapper>
+                ))}
+              </div>
+            )}
+          </Droppable>
+        </DragDropContext>
+
+        <div className={'border-t border-shade-6 pt-2'}>
+          <button
+            onClick={() => onNewColumnClick()}
+            className={'flex w-full items-center gap-2 rounded-lg px-4 py-2 hover:bg-shade-6'}
+          >
+            <i className={'h-5 w-5'}>
+              <AddSvg></AddSvg>
+            </i>
+            <span>{t('grid.field.newColumn')}</span>
+          </button>
+        </div>
+
+        {showFieldEditor && editingCell && (
+          <EditFieldPopup
+            top={editFieldTop}
+            right={editFieldRight}
+            cellIdentifier={editingCell}
+            viewId={viewId}
+            onOutsideClick={onOutsideEditFieldClick}
+            fieldInfo={controller.fieldController.getField(editingCell.fieldId)}
+            changeFieldTypeClick={onChangeFieldTypeClick}
+          ></EditFieldPopup>
+        )}
+        {showChangeFieldTypePopup && (
+          <ChangeFieldTypePopup
+            top={changeFieldTypeTop}
+            right={changeFieldTypeRight}
+            onClick={(newType) => changeFieldType(newType)}
+            onOutsideClick={() => setShowChangeFieldTypePopup(false)}
+          ></ChangeFieldTypePopup>
+        )}
+        {showChangeOptionsPopup && editingCell && (
+          <CellOptionsPopup
+            top={changeOptionsTop}
+            left={changeOptionsLeft}
+            cellIdentifier={editingCell}
+            cellCache={controller.databaseViewCache.getRowCache().getCellCache()}
+            fieldController={controller.fieldController}
+            onOutsideClick={() => setShowChangeOptionsPopup(false)}
+          ></CellOptionsPopup>
+        )}
+        {showDatePicker && editingCell && (
+          <DatePickerPopup
+            top={datePickerTop}
+            left={datePickerLeft}
+            cellIdentifier={editingCell}
+            cellCache={controller.databaseViewCache.getRowCache().getCellCache()}
+            fieldController={controller.fieldController}
+            onOutsideClick={() => setShowDatePicker(false)}
+          ></DatePickerPopup>
+        )}
+      </div>
+    </div>
+  );
+};

+ 24 - 0
frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/FieldTypeIcon.tsx

@@ -0,0 +1,24 @@
+import { FieldType } from '@/services/backend';
+import { TextTypeSvg } from '$app/components/_shared/svg/TextTypeSvg';
+import { NumberTypeSvg } from '$app/components/_shared/svg/NumberTypeSvg';
+import { DateTypeSvg } from '$app/components/_shared/svg/DateTypeSvg';
+import { SingleSelectTypeSvg } from '$app/components/_shared/svg/SingleSelectTypeSvg';
+import { MultiSelectTypeSvg } from '$app/components/_shared/svg/MultiSelectTypeSvg';
+import { ChecklistTypeSvg } from '$app/components/_shared/svg/ChecklistTypeSvg';
+import { UrlTypeSvg } from '$app/components/_shared/svg/UrlTypeSvg';
+import { CheckboxSvg } from '$app/components/_shared/svg/CheckboxSvg';
+
+export const FieldTypeIcon = ({ fieldType }: { fieldType: FieldType }) => {
+  return (
+    <>
+      {fieldType === FieldType.RichText && <TextTypeSvg></TextTypeSvg>}
+      {fieldType === FieldType.Number && <NumberTypeSvg></NumberTypeSvg>}
+      {fieldType === FieldType.DateTime && <DateTypeSvg></DateTypeSvg>}
+      {fieldType === FieldType.SingleSelect && <SingleSelectTypeSvg></SingleSelectTypeSvg>}
+      {fieldType === FieldType.MultiSelect && <MultiSelectTypeSvg></MultiSelectTypeSvg>}
+      {fieldType === FieldType.Checklist && <ChecklistTypeSvg></ChecklistTypeSvg>}
+      {fieldType === FieldType.URL && <UrlTypeSvg></UrlTypeSvg>}
+      {fieldType === FieldType.Checkbox && <CheckboxSvg></CheckboxSvg>}
+    </>
+  );
+};

+ 18 - 0
frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/FieldTypeName.tsx

@@ -0,0 +1,18 @@
+import { FieldType } from '@/services/backend';
+import { useTranslation } from 'react-i18next';
+
+export const FieldTypeName = ({ fieldType }: { fieldType: FieldType }) => {
+  const { t } = useTranslation('');
+  return (
+    <>
+      {fieldType === FieldType.RichText && t('grid.field.textFieldName')}
+      {fieldType === FieldType.Number && t('grid.field.numberFieldName')}
+      {fieldType === FieldType.DateTime && t('grid.field.dateFieldName')}
+      {fieldType === FieldType.SingleSelect && t('grid.field.singleSelectFieldName')}
+      {fieldType === FieldType.MultiSelect && t('grid.field.multiSelectFieldName')}
+      {fieldType === FieldType.Checklist && t('grid.field.checklistFieldName')}
+      {fieldType === FieldType.URL && t('grid.field.urlFieldName')}
+      {fieldType === FieldType.Checkbox && t('grid.field.checkboxFieldName')}
+    </>
+  );
+};

+ 4 - 1
frontend/appflowy_tauri/src/appflowy_app/components/_shared/LanguageSelectPopup.tsx

@@ -29,7 +29,10 @@ const supportedLanguages: { key: string; title: string }[] = [
 
 export const LanguageSelectPopup = ({ onClose }: { onClose: () => void }) => {
   const items: IPopupItem[] = supportedLanguages.map<IPopupItem>((item) => ({
-    onClick: () => void i18n.changeLanguage(item.key),
+    onClick: () => {
+      void i18n.changeLanguage(item.key);
+      onClose();
+    },
     title: item.title,
     icon: <></>,
   }));

+ 1 - 1
frontend/appflowy_tauri/src/appflowy_app/components/_shared/database-hooks/loadField.ts

@@ -9,7 +9,7 @@ import {
   NumberFormat,
   SingleSelectTypeOptionPB,
   TimeFormat,
-} from '../../../../services/backend';
+} from '@/services/backend';
 import {
   makeChecklistTypeOptionContext,
   makeDateTypeOptionContext,

+ 25 - 14
frontend/appflowy_tauri/src/appflowy_app/components/_shared/database-hooks/useCell.ts

@@ -1,32 +1,43 @@
-import { CellIdentifier } from '../../../stores/effects/database/cell/cell_bd_svc';
-import { CellCache } from '../../../stores/effects/database/cell/cell_cache';
-import { FieldController } from '../../../stores/effects/database/field/field_controller';
-import { CellControllerBuilder } from '../../../stores/effects/database/cell/controller_builder';
-import { DateCellDataPB, SelectOptionCellDataPB, URLCellDataPB } from '../../../../services/backend';
+import { CellIdentifier } from '$app/stores/effects/database/cell/cell_bd_svc';
+import { CellCache } from '$app/stores/effects/database/cell/cell_cache';
+import { FieldController } from '$app/stores/effects/database/field/field_controller';
+import { CellControllerBuilder } from '$app/stores/effects/database/cell/controller_builder';
+import { DateCellDataPB, SelectOptionCellDataPB, URLCellDataPB } from '$app/../services/backend';
 import { useEffect, useState } from 'react';
+import { CellController } from '$app/stores/effects/database/cell/cell_controller';
 
 export const useCell = (cellIdentifier: CellIdentifier, cellCache: CellCache, fieldController: FieldController) => {
   const [data, setData] = useState<DateCellDataPB | URLCellDataPB | SelectOptionCellDataPB | string | undefined>();
+  const [cellController, setCellController] = useState<CellController<any, any>>();
 
   useEffect(() => {
+    if (!cellIdentifier || !cellCache || !fieldController) return;
     const builder = new CellControllerBuilder(cellIdentifier, cellCache, fieldController);
-    const cellController = builder.build();
-    cellController.subscribeChanged({
-      onCellChanged: (value) => {
-        setData(value.unwrap());
+    const c = builder.build();
+    setCellController(c);
+
+    c.subscribeChanged({
+      onCellChanged: (cellData) => {
+        if (cellData.some) {
+          setData(cellData.val);
+        }
       },
     });
 
-    // ignore the return value, because we are using the subscription
-    void cellController.getCellData();
+    void (async () => {
+      const cellData = await c.getCellData();
+      if (cellData.some) {
+        setData(cellData.unwrap());
+      }
+    })();
 
     return () => {
-      // dispose is causing an error
-      // void cellController.dispose();
+      void c.dispose();
     };
-  }, []);
+  }, [cellIdentifier, cellCache, fieldController]);
 
   return {
+    cellController,
     data,
   };
 };

+ 36 - 8
frontend/appflowy_tauri/src/appflowy_app/components/_shared/database-hooks/useDatabase.ts

@@ -1,26 +1,27 @@
 import { useEffect, useState } from 'react';
-import { DatabaseController } from '../../../stores/effects/database/database_controller';
-import { databaseActions, DatabaseFieldMap, IDatabaseColumn } from '../../../stores/reducers/database/slice';
-import { useAppDispatch } from '../../../stores/store';
+import { DatabaseController } from '$app/stores/effects/database/database_controller';
+import { databaseActions, DatabaseFieldMap, IDatabaseColumn } from '$app/stores/reducers/database/slice';
+import { useAppDispatch } from '$app/stores/store';
 import loadField from './loadField';
-import { FieldInfo } from '../../../stores/effects/database/field/field_controller';
-import { RowInfo } from '../../../stores/effects/database/row/row_cache';
+import { FieldInfo } from '$app/stores/effects/database/field/field_controller';
+import { RowInfo } from '$app/stores/effects/database/row/row_cache';
 import { ViewLayoutTypePB } from '@/services/backend';
 import { DatabaseGroupController } from '$app/stores/effects/database/group/group_controller';
+import { OnDragEndResponder } from 'react-beautiful-dnd';
 
 export const useDatabase = (viewId: string, type?: ViewLayoutTypePB) => {
   const dispatch = useAppDispatch();
   const [controller, setController] = useState<DatabaseController>();
   const [rows, setRows] = useState<readonly RowInfo[]>([]);
   const [groups, setGroups] = useState<readonly DatabaseGroupController[]>([]);
+  const [groupByFieldId, setGroupByFieldId] = useState('');
 
   useEffect(() => {
     if (!viewId.length) return;
     const c = new DatabaseController(viewId);
     setController(c);
 
-    // dispose is causing an error
-    // return () => void c.dispose();
+    return () => void c.dispose();
   }, [viewId]);
 
   const loadFields = async (fieldInfos: readonly FieldInfo[]) => {
@@ -58,10 +59,37 @@ export const useDatabase = (viewId: string, type?: ViewLayoutTypePB) => {
       await controller.open();
 
       if (type === ViewLayoutTypePB.Board) {
+        const fieldId = await controller.getGroupByFieldId();
+        setGroupByFieldId(fieldId.unwrap());
         setGroups(controller.groups.value);
       }
     })();
   }, [controller]);
 
-  return { loadFields, controller, rows, groups };
+  const onNewRowClick = async (index: number) => {
+    if (!groups) return;
+    if (!controller?.groups) return;
+    const group = groups[index];
+    await group.createRow();
+
+    setGroups([...controller.groups.value]);
+  };
+
+  const onDragEnd: OnDragEndResponder = async (result) => {
+    if (!controller) return;
+    const { source, destination } = result;
+    const group = groups.find((g) => g.groupId === source.droppableId);
+    if (!group) return;
+
+    if (source.droppableId === destination?.droppableId) {
+      // move inside the block (group)
+      await controller.exchangeRow(group.rows[source.index].id, group.rows[destination.index].id);
+    } else {
+      // move to different block (group)
+      if (!destination?.droppableId) return;
+      await controller.moveRow(group.rows[source.index].id, destination.droppableId);
+    }
+  };
+
+  return { loadFields, controller, rows, groups, groupByFieldId, onNewRowClick, onDragEnd };
 };

+ 19 - 7
frontend/appflowy_tauri/src/appflowy_app/components/_shared/database-hooks/useRow.ts

@@ -1,14 +1,19 @@
-import { DatabaseController } from '../../../stores/effects/database/database_controller';
-import { RowController } from '../../../stores/effects/database/row/row_controller';
-import { RowInfo } from '../../../stores/effects/database/row/row_cache';
-import { CellIdentifier } from '../../../stores/effects/database/cell/cell_bd_svc';
+import { DatabaseController } from '$app/stores/effects/database/database_controller';
+import { RowController } from '$app/stores/effects/database/row/row_controller';
+import { RowInfo } from '$app/stores/effects/database/row/row_cache';
+import { CellIdentifier } from '$app/stores/effects/database/cell/cell_bd_svc';
 import { useEffect, useState } from 'react';
+import { TypeOptionController } from '$app/stores/effects/database/field/type_option/type_option_controller';
+import { None } from 'ts-results';
+import { useAppSelector } from '$app/stores/store';
 
 export const useRow = (viewId: string, databaseController: DatabaseController, rowInfo: RowInfo) => {
   const [cells, setCells] = useState<{ fieldId: string; cellIdentifier: CellIdentifier }[]>([]);
   const [rowController, setRowController] = useState<RowController>();
+  const databaseStore = useAppSelector((state) => state.database);
 
   useEffect(() => {
+    if (!databaseController || !rowInfo) return;
     const rowCache = databaseController.databaseViewCache.getRowCache();
     const fieldController = databaseController.fieldController;
     const c = new RowController(rowInfo, fieldController, rowCache);
@@ -17,7 +22,7 @@ export const useRow = (viewId: string, databaseController: DatabaseController, r
     return () => {
       // dispose row controller in future
     };
-  }, []);
+  }, [databaseController, rowInfo]);
 
   useEffect(() => {
     if (!rowController) return;
@@ -35,9 +40,16 @@ export const useRow = (viewId: string, databaseController: DatabaseController, r
 
       setCells(loadingCells);
     })();
-  }, [rowController]);
+  }, [rowController, databaseStore.columns]);
+
+  const onNewColumnClick = async () => {
+    if (!databaseController) return;
+    const controller = new TypeOptionController(viewId, None);
+    await controller.initialize();
+  };
 
   return {
-    cells: cells,
+    cells,
+    onNewColumnClick,
   };
 };

+ 7 - 0
frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/ArrowLeftSvg.tsx

@@ -0,0 +1,7 @@
+export const ArrowLeftSvg = () => {
+  return (
+    <svg width='100%' height='100%' viewBox='0 0 16 16' fill='none' xmlns='http://www.w3.org/2000/svg'>
+      <path d='M10 4L6 8L10 12' stroke='currentColor' strokeLinecap='round' strokeLinejoin='round' />
+    </svg>
+  );
+};

+ 7 - 0
frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/ArrowRightSvg.tsx

@@ -0,0 +1,7 @@
+export const ArrowRightSvg = () => {
+  return (
+    <svg width='100%' height='100%' viewBox='0 0 16 16' fill='none' xmlns='http://www.w3.org/2000/svg'>
+      <path d='M6 4L10 8L6 12' stroke='currentColor' strokeLinecap='round' strokeLinejoin='round' />
+    </svg>
+  );
+};

+ 13 - 0
frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/CheckboxSvg.tsx

@@ -0,0 +1,13 @@
+export const CheckboxSvg = () => {
+  return (
+    <svg width='100%' height='100%' viewBox='0 0 16 16' fill='none' xmlns='http://www.w3.org/2000/svg'>
+      <path d='M6.5 8L8.11538 9.5L13.5 4.5' stroke='currentColor' strokeLinecap='round' strokeLinejoin='round' />
+      <path
+        d='M13 8.5V11.8889C13 12.1836 12.8829 12.4662 12.6746 12.6746C12.4662 12.8829 12.1836 13 11.8889 13H4.11111C3.81643 13 3.53381 12.8829 3.32544 12.6746C3.11706 12.4662 3 12.1836 3 11.8889V4.11111C3 3.81643 3.11706 3.53381 3.32544 3.32544C3.53381 3.11706 3.81643 3 4.11111 3H10.2222'
+        stroke='currentColor'
+        strokeLinecap='round'
+        strokeLinejoin='round'
+      />
+    </svg>
+  );
+};

+ 7 - 0
frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/CheckmarkSvg.tsx

@@ -0,0 +1,7 @@
+export const CheckmarkSvg = () => {
+  return (
+    <svg width='100%' height='100%' viewBox='0 0 10 8' fill='none' xmlns='http://www.w3.org/2000/svg'>
+      <path d='M1 5.2L2.84615 7L9 1' stroke='#00BCF0' strokeLinecap='round' strokeLinejoin='round' />
+    </svg>
+  );
+};

+ 15 - 0
frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/ClockSvg.tsx

@@ -0,0 +1,15 @@
+export const ClockSvg = () => {
+  return (
+    <svg width='100%' height='100%' viewBox='0 0 16 16' fill='none' xmlns='http://www.w3.org/2000/svg'>
+      <path
+        d='M8 13C10.7614 13 13 10.7614 13 8C13 5.23858 10.7614 3 8 3C5.23858 3 3 5.23858 3 8C3 10.7614 5.23858 13 8 13Z'
+        stroke='currentColor'
+        strokeLinecap='round'
+        strokeLinejoin='round'
+      />
+      <path d='M8 5V8L10 9' stroke='currentColor' strokeLinecap='round' strokeLinejoin='round' />
+      <path d='M11.5 2.5L13.5 4.5' stroke='currentColor' strokeLinecap='round' />
+      <path d='M4.5 2.5L2.5 4.5' stroke='currentColor' strokeLinecap='round' />
+    </svg>
+  );
+};

+ 8 - 0
frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/EditorCheckSvg.tsx

@@ -0,0 +1,8 @@
+export const EditorCheckSvg = () => {
+  return (
+    <svg width='100%' height='100%' viewBox='0 0 16 16' fill='none' xmlns='http://www.w3.org/2000/svg'>
+      <rect x='2' y='2' width='12' height='12' rx='4' fill='#00BCF0' />
+      <path d='M6 8L7.61538 9.5L10.5 6.5' stroke='white' strokeLinecap='round' strokeLinejoin='round' />
+    </svg>
+  );
+};

+ 7 - 0
frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/EditorUncheckSvg.tsx

@@ -0,0 +1,7 @@
+export const EditorUncheckSvg = () => {
+  return (
+    <svg width='100%' height='100%' viewBox='0 0 16 16' fill='none' xmlns='http://www.w3.org/2000/svg'>
+      <rect x='2.5' y='2.5' width='11' height='11' rx='3.5' stroke='#BDBDBD' />
+    </svg>
+  );
+};

+ 10 - 0
frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/MoreSvg.tsx

@@ -0,0 +1,10 @@
+export const MoreSvg = () => {
+  return (
+    <svg width='100%' height='100%' viewBox='0 0 16 16' fill='none' xmlns='http://www.w3.org/2000/svg'>
+      <path
+        d='M9.39568 7.6963L6.91032 5.56599C6.65085 5.34358 6.25 5.52795 6.25 5.86969L6.25 10.1303C6.25 10.4721 6.65085 10.6564 6.91032 10.434L9.39568 8.3037C9.58192 8.14406 9.58192 7.85594 9.39568 7.6963Z'
+        fill='currentColor'
+      />
+    </svg>
+  );
+};

+ 9 - 0
frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/SkipLeftSvg.tsx

@@ -0,0 +1,9 @@
+export const SkipLeftSvg = () => {
+  return (
+    <svg width='100%' height='100%' viewBox='0 0 16 16' fill='none' xmlns='http://www.w3.org/2000/svg'>
+      <path d='M3 11.7778L3 4' stroke='currentColor' strokeLinecap='round' strokeLinejoin='round' />
+      <path d='M9.5 4.5L6 8L9.5 11.5' stroke='currentColor' strokeLinecap='round' strokeLinejoin='round' />
+      <path d='M6 8L13 8' stroke='currentColor' strokeLinecap='round' strokeLinejoin='round' />
+    </svg>
+  );
+};

+ 9 - 0
frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/SkipRightSvg.tsx

@@ -0,0 +1,9 @@
+export const SkipRightSvg = () => {
+  return (
+    <svg width='100%' height='100%' viewBox='0 0 16 16' fill='none' xmlns='http://www.w3.org/2000/svg'>
+      <path d='M13 11.7778L13 4' stroke='currentColor' strokeLinecap='round' strokeLinejoin='round' />
+      <path d='M6.5 4.5L10 8L6.5 11.5' stroke='currentColor' strokeLinecap='round' strokeLinejoin='round' />
+      <path d='M10 8L3 8' stroke='currentColor' strokeLinecap='round' strokeLinejoin='round' />
+    </svg>
+  );
+};

+ 43 - 18
frontend/appflowy_tauri/src/appflowy_app/components/board/Board.tsx

@@ -4,9 +4,23 @@ import { BoardBlock } from './BoardBlock';
 import { NewBoardBlock } from './NewBoardBlock';
 import { useDatabase } from '../_shared/database-hooks/useDatabase';
 import { ViewLayoutTypePB } from '@/services/backend';
+import { DragDropContext } from 'react-beautiful-dnd';
+import { useState } from 'react';
+import { RowInfo } from '$app/stores/effects/database/row/row_cache';
+import { EditRow } from '$app/components/_shared/EditRow/EditRow';
 
 export const Board = ({ viewId }: { viewId: string }) => {
-  const { controller, rows, groups } = useDatabase(viewId, ViewLayoutTypePB.Board);
+  const { controller, rows, groups, groupByFieldId, onNewRowClick, onDragEnd } = useDatabase(
+    viewId,
+    ViewLayoutTypePB.Board
+  );
+  const [showBoardRow, setShowBoardRow] = useState(false);
+  const [boardRowInfo, setBoardRowInfo] = useState<RowInfo>();
+
+  const onOpenRow = (rowInfo: RowInfo) => {
+    setBoardRowInfo(rowInfo);
+    setShowBoardRow(true);
+  };
 
   return (
     <>
@@ -22,24 +36,35 @@ export const Board = ({ viewId }: { viewId: string }) => {
           <SearchInput />
         </div>
       </div>
-      <div className={'relative w-full flex-1 overflow-auto'}>
-        <div className={'absolute flex h-full flex-shrink-0 items-start justify-start gap-4'}>
-          {controller &&
-            groups &&
-            groups.map((group, index) => (
-              <BoardBlock
-                key={index}
-                viewId={viewId}
-                controller={controller}
-                rows={group.rows}
-                title={group.name}
-                allRows={rows}
-              />
-            ))}
-
-          <NewBoardBlock onClick={() => console.log('new block')}></NewBoardBlock>
+      <DragDropContext onDragEnd={onDragEnd}>
+        <div className={'relative w-full flex-1 overflow-auto'}>
+          <div className={'absolute flex h-full flex-shrink-0 items-start justify-start gap-4'}>
+            {controller &&
+              groups &&
+              groups.map((group, index) => (
+                <BoardBlock
+                  key={group.groupId}
+                  viewId={viewId}
+                  controller={controller}
+                  group={group}
+                  allRows={rows}
+                  groupByFieldId={groupByFieldId}
+                  onNewRowClick={() => onNewRowClick(index)}
+                  onOpenRow={onOpenRow}
+                />
+              ))}
+            <NewBoardBlock onClick={() => console.log('new block')}></NewBoardBlock>
+          </div>
         </div>
-      </div>
+      </DragDropContext>
+      {controller && showBoardRow && boardRowInfo && (
+        <EditRow
+          onClose={() => setShowBoardRow(false)}
+          viewId={viewId}
+          controller={controller}
+          rowInfo={boardRowInfo}
+        ></EditRow>
+      )}
     </>
   );
 };

+ 41 - 17
frontend/appflowy_tauri/src/appflowy_app/components/board/BoardBlock.tsx

@@ -3,26 +3,31 @@ import AddSvg from '../_shared/svg/AddSvg';
 import { BoardCard } from './BoardCard';
 import { RowInfo } from '../../stores/effects/database/row/row_cache';
 import { DatabaseController } from '../../stores/effects/database/database_controller';
-import { RowPB } from '@/services/backend';
+import { Droppable } from 'react-beautiful-dnd';
+import { DatabaseGroupController } from '$app/stores/effects/database/group/group_controller';
 
 export const BoardBlock = ({
   viewId,
   controller,
-  title,
-  rows,
   allRows,
+  groupByFieldId,
+  onNewRowClick,
+  onOpenRow,
+  group,
 }: {
   viewId: string;
   controller: DatabaseController;
-  title: string;
-  rows: RowPB[];
   allRows: readonly RowInfo[];
+  groupByFieldId: string;
+  onNewRowClick: () => void;
+  onOpenRow: (rowId: RowInfo) => void;
+  group: DatabaseGroupController;
 }) => {
   return (
     <div className={'flex h-full w-[250px] flex-col rounded-lg bg-surface-1'}>
       <div className={'flex items-center justify-between p-4'}>
         <div className={'flex items-center gap-2'}>
-          <span>{title}</span>
+          <span>{group.name}</span>
           <span className={'text-shade-4'}>()</span>
         </div>
         <div className={'flex items-center gap-2'}>
@@ -34,18 +39,37 @@ export const BoardBlock = ({
           </button>
         </div>
       </div>
-      <div className={'flex flex-1 flex-col gap-1 overflow-auto px-2'}>
-        {rows.map((row_pb, index) => {
-          const row = allRows.find((r) => r.row.id === row_pb.id);
-          return row ? (
-            <BoardCard viewId={viewId} controller={controller} key={index} rowInfo={row}></BoardCard>
-          ) : (
-            <span key={index}></span>
-          );
-        })}
-      </div>
+      <Droppable droppableId={group.groupId}>
+        {(provided) => (
+          <div
+            className={'flex flex-1 flex-col gap-1 overflow-auto px-2'}
+            {...provided.droppableProps}
+            ref={provided.innerRef}
+          >
+            {group.rows.map((row_pb, index) => {
+              const row = allRows.find((r) => r.row.id === row_pb.id);
+              return row ? (
+                <BoardCard
+                  viewId={viewId}
+                  controller={controller}
+                  index={index}
+                  key={row.row.id}
+                  rowInfo={row}
+                  groupByFieldId={groupByFieldId}
+                  onOpenRow={onOpenRow}
+                ></BoardCard>
+              ) : (
+                <span key={index}></span>
+              );
+            })}
+          </div>
+        )}
+      </Droppable>
       <div className={'p-2'}>
-        <button className={'flex w-full items-center gap-2 rounded-lg px-2 py-2 hover:bg-surface-2'}>
+        <button
+          onClick={onNewRowClick}
+          className={'flex w-full items-center gap-2 rounded-lg px-2 py-2 hover:bg-surface-2'}
+        >
           <span className={'h-5 w-5'}>
             <AddSvg></AddSvg>
           </span>

+ 34 - 18
frontend/appflowy_tauri/src/appflowy_app/components/board/BoardCard.tsx

@@ -3,36 +3,52 @@ import { RowInfo } from '../../stores/effects/database/row/row_cache';
 import { useRow } from '../_shared/database-hooks/useRow';
 import { DatabaseController } from '../../stores/effects/database/database_controller';
 import { BoardCell } from './BoardCell';
+import { Draggable } from 'react-beautiful-dnd';
 
 export const BoardCard = ({
+  index,
   viewId,
   controller,
   rowInfo,
+  groupByFieldId,
+  onOpenRow,
 }: {
+  index: number;
   viewId: string;
   controller: DatabaseController;
   rowInfo: RowInfo;
+  groupByFieldId: string;
+  onOpenRow: (rowId: RowInfo) => void;
 }) => {
   const { cells } = useRow(viewId, controller, rowInfo);
 
   return (
-    <div
-      onClick={() => console.log('on click')}
-      className={`relative cursor-pointer select-none rounded-lg border border-shade-6 bg-white px-3 py-2 transition-transform duration-100 hover:bg-main-selector `}
-    >
-      <button className={'absolute right-4 top-2.5 h-5 w-5 rounded hover:bg-surface-2'}>
-        <Details2Svg></Details2Svg>
-      </button>
-      <div className={'flex flex-col gap-3'}>
-        {cells.map((cell, index) => (
-          <BoardCell
-            key={index}
-            cellIdentifier={cell.cellIdentifier}
-            cellCache={controller.databaseViewCache.getRowCache().getCellCache()}
-            fieldController={controller.fieldController}
-          ></BoardCell>
-        ))}
-      </div>
-    </div>
+    <Draggable draggableId={rowInfo.row.id} index={index}>
+      {(provided) => (
+        <div
+          ref={provided.innerRef}
+          {...provided.draggableProps}
+          {...provided.dragHandleProps}
+          onClick={() => onOpenRow(rowInfo)}
+          className={`relative cursor-pointer select-none rounded-lg border border-shade-6 bg-white px-3 py-2 transition-transform duration-100 hover:bg-main-selector `}
+        >
+          <button className={'absolute right-4 top-2.5 h-5 w-5 rounded hover:bg-surface-2'}>
+            <Details2Svg></Details2Svg>
+          </button>
+          <div className={'flex flex-col gap-3'}>
+            {cells
+              .filter((cell) => cell.fieldId !== groupByFieldId)
+              .map((cell, cellIndex) => (
+                <BoardCell
+                  key={cellIndex}
+                  cellIdentifier={cell.cellIdentifier}
+                  cellCache={controller.databaseViewCache.getRowCache().getCellCache()}
+                  fieldController={controller.fieldController}
+                ></BoardCell>
+              ))}
+          </div>
+        </div>
+      )}
+    </Draggable>
   );
 };

+ 7 - 0
frontend/appflowy_tauri/src/appflowy_app/components/board/BoardCell.tsx

@@ -5,6 +5,7 @@ import { FieldType } from '../../../services/backend';
 import { BoardOptionsCell } from './BoardOptionsCell';
 import { BoardDateCell } from './BoardDateCell';
 import { BoardTextCell } from './BoardTextCell';
+import { BoardUrlCell } from '$app/components/board/BoardUrlCell';
 
 export const BoardCell = ({
   cellIdentifier,
@@ -31,6 +32,12 @@ export const BoardCell = ({
           cellCache={cellCache}
           fieldController={fieldController}
         ></BoardDateCell>
+      ) : cellIdentifier.fieldType === FieldType.URL ? (
+        <BoardUrlCell
+          cellIdentifier={cellIdentifier}
+          cellCache={cellCache}
+          fieldController={fieldController}
+        ></BoardUrlCell>
       ) : (
         <BoardTextCell
           cellIdentifier={cellIdentifier}

+ 7 - 3
frontend/appflowy_tauri/src/appflowy_app/components/board/BoardOptionsCell.tsx

@@ -3,6 +3,7 @@ import { useCell } from '../_shared/database-hooks/useCell';
 import { CellIdentifier } from '../../stores/effects/database/cell/cell_bd_svc';
 import { CellCache } from '../../stores/effects/database/cell/cell_cache';
 import { FieldController } from '../../stores/effects/database/field/field_controller';
+import { getBgColor } from '$app/components/_shared/getColor';
 
 export const BoardOptionsCell = ({
   cellIdentifier,
@@ -16,10 +17,13 @@ export const BoardOptionsCell = ({
   const { data } = useCell(cellIdentifier, cellCache, fieldController);
 
   return (
-    <>
+    <div className={'flex flex-wrap items-center gap-2 py-2 text-xs text-black'}>
       {(data as SelectOptionCellDataPB | undefined)?.select_options?.map((option, index) => (
-        <div key={index}>{option?.name || ''}</div>
+        <div className={`${getBgColor(option.color)} rounded px-2 py-0.5`} key={index}>
+          {option?.name || ''}
+        </div>
       )) || ''}
-    </>
+      &nbsp;
+    </div>
   );
 };

+ 10 - 4
frontend/appflowy_tauri/src/appflowy_app/components/board/BoardTextCell.tsx

@@ -1,6 +1,6 @@
-import { CellIdentifier } from '../../stores/effects/database/cell/cell_bd_svc';
-import { CellCache } from '../../stores/effects/database/cell/cell_cache';
-import { FieldController } from '../../stores/effects/database/field/field_controller';
+import { CellIdentifier } from '$app/stores/effects/database/cell/cell_bd_svc';
+import { CellCache } from '$app/stores/effects/database/cell/cell_cache';
+import { FieldController } from '$app/stores/effects/database/field/field_controller';
 import { useCell } from '../_shared/database-hooks/useCell';
 
 export const BoardTextCell = ({
@@ -14,5 +14,11 @@ export const BoardTextCell = ({
 }) => {
   const { data } = useCell(cellIdentifier, cellCache, fieldController);
 
-  return <div>{(data as string | undefined) || ''}</div>;
+  return (
+    <div>
+      {((data as string | undefined) || '').split('\n').map((line, index) => (
+        <div key={index}>{line}</div>
+      ))}
+    </div>
+  );
 };

+ 29 - 0
frontend/appflowy_tauri/src/appflowy_app/components/board/BoardUrlCell.tsx

@@ -0,0 +1,29 @@
+import { CellIdentifier } from '$app/stores/effects/database/cell/cell_bd_svc';
+import { CellCache } from '$app/stores/effects/database/cell/cell_cache';
+import { FieldController } from '$app/stores/effects/database/field/field_controller';
+import { useCell } from '$app/components/_shared/database-hooks/useCell';
+import { URLCellDataPB } from '@/services/backend';
+
+export const BoardUrlCell = ({
+  cellIdentifier,
+  cellCache,
+  fieldController,
+}: {
+  cellIdentifier: CellIdentifier;
+  cellCache: CellCache;
+  fieldController: FieldController;
+}) => {
+  const { data } = useCell(cellIdentifier, cellCache, fieldController);
+
+  return (
+    <>
+      <a
+        className={'text-main-accent hover:underline'}
+        href={(data as URLCellDataPB | undefined)?.url || ''}
+        target={'_blank'}
+      >
+        {(data as URLCellDataPB | undefined)?.content || ''}
+      </a>
+    </>
+  );
+};

+ 2 - 2
frontend/appflowy_tauri/src/appflowy_app/components/error/ErrorModal.tsx

@@ -3,7 +3,7 @@ import { CloseSvg } from '../_shared/svg/CloseSvg';
 
 export const ErrorModal = ({ message, onClose }: { message: string; onClose: () => void }) => {
   return (
-    <div className={'fixed inset-0 z-20 flex items-center justify-center bg-white/30 backdrop-blur-sm'}>
+    <div className={'fixed inset-0 z-10 flex items-center justify-center bg-white/30 backdrop-blur-sm'}>
       <div
         className={
           'relative flex flex-col items-center gap-8 rounded-xl border border-shade-5 bg-white px-16 py-8 shadow-md'
@@ -11,7 +11,7 @@ export const ErrorModal = ({ message, onClose }: { message: string; onClose: ()
       >
         <button
           onClick={() => onClose()}
-          className={'absolute right-0 top-0 z-20 px-2 py-2 text-shade-5 hover:text-black'}
+          className={'absolute right-0 top-0 z-10 px-2 py-2 text-shade-5 hover:text-black'}
         >
           <i className={'block h-8 w-8'}>
             <CloseSvg></CloseSvg>

+ 1 - 1
frontend/appflowy_tauri/src/appflowy_app/components/grid/GridTableHeader/GridTableHeader.hooks.tsx

@@ -1,5 +1,5 @@
 import { nanoid } from 'nanoid';
-import { FieldType } from '../../../../services/backend/models/flowy-database/field_entities';
+import { FieldType } from '@/services/backend/models/flowy-database/field_entities';
 import { gridActions } from '../../../stores/reducers/grid/slice';
 import { useAppDispatch, useAppSelector } from '../../../stores/store';
 

+ 1 - 1
frontend/appflowy_tauri/src/appflowy_app/components/grid/GridTableHeader/GridTableHeader.tsx

@@ -7,7 +7,7 @@ import { SingleSelectTypeSvg } from '../../_shared/svg/SingleSelectTypeSvg';
 import { MultiSelectTypeSvg } from '../../_shared/svg/MultiSelectTypeSvg';
 import { ChecklistTypeSvg } from '../../_shared/svg/ChecklistTypeSvg';
 import { UrlTypeSvg } from '../../_shared/svg/UrlTypeSvg';
-import { FieldType } from '../../../../services/backend/models/flowy-database/field_entities';
+import { FieldType } from '@/services/backend/models/flowy-database/field_entities';
 
 export const GridTableHeader = () => {
   const { fields, onAddField } = useGridTableHeaderHooks();

+ 15 - 0
frontend/appflowy_tauri/src/appflowy_app/components/layout/HeaderPanel/LanguageButton.tsx

@@ -0,0 +1,15 @@
+import { EarthSvg } from '$app/components/_shared/svg/EarthSvg';
+import { useState } from 'react';
+import { LanguageSelectPopup } from '$app/components/_shared/LanguageSelectPopup';
+
+export const LanguageButton = () => {
+  const [showPopup, setShowPopup] = useState(false);
+  return (
+    <>
+      <button onClick={() => setShowPopup(!showPopup)} className={'h-5 w-5'}>
+        <EarthSvg></EarthSvg>
+      </button>
+      {showPopup && <LanguageSelectPopup onClose={() => setShowPopup(false)}></LanguageSelectPopup>}
+    </>
+  );
+};

+ 4 - 1
frontend/appflowy_tauri/src/appflowy_app/components/layout/HeaderPanel/PageOptions.tsx

@@ -2,6 +2,7 @@ import { Button } from '../../_shared/Button';
 import { Details2Svg } from '../../_shared/svg/Details2Svg';
 import { usePageOptions } from './PageOptions.hooks';
 import { OptionsPopup } from './OptionsPopup';
+import { LanguageButton } from '$app/components/layout/HeaderPanel/LanguageButton';
 
 export const PageOptions = () => {
   const { showOptionsPopup, onOptionsClick, onClose, onSignOutClick } = usePageOptions();
@@ -13,7 +14,9 @@ export const PageOptions = () => {
           Share
         </Button>
 
-        <button id='option-button' className={'relative h-8 w-8'} onClick={onOptionsClick}  >
+        <LanguageButton></LanguageButton>
+
+        <button id='option-button' className={'relative h-8 w-8'} onClick={onOptionsClick}>
           <Details2Svg></Details2Svg>
         </button>
       </div>

+ 1 - 1
frontend/appflowy_tauri/src/appflowy_app/components/layout/NavigationPanel/FolderItem.hooks.ts

@@ -2,7 +2,7 @@ import { foldersActions, IFolder } from '../../../stores/reducers/folders/slice'
 import { useEffect, useState } from 'react';
 import { useAppDispatch, useAppSelector } from '../../../stores/store';
 import { IPage, pagesActions } from '../../../stores/reducers/pages/slice';
-import { AppPB, ViewLayoutTypePB } from '../../../../services/backend';
+import { AppPB, ViewLayoutTypePB } from '@/services/backend';
 import { AppBackendService } from '../../../stores/effects/folder/app/app_bd_svc';
 import { WorkspaceBackendService } from '../../../stores/effects/folder/workspace/workspace_bd_svc';
 import { useError } from '../../error/Error.hooks';

+ 1 - 1
frontend/appflowy_tauri/src/appflowy_app/components/layout/NavigationPanel/NavigationPanel.hooks.ts

@@ -1,7 +1,7 @@
 import { useAppSelector } from '../../../stores/store';
 import { useNavigate } from 'react-router-dom';
 import { IPage } from '../../../stores/reducers/pages/slice';
-import { ViewLayoutTypePB } from '../../../../services/backend';
+import { ViewLayoutTypePB } from '@/services/backend';
 import { useState } from 'react';
 
 export const useNavigationPanelHooks = function () {

+ 1 - 1
frontend/appflowy_tauri/src/appflowy_app/components/layout/NavigationPanel/PageItem.tsx

@@ -7,7 +7,7 @@ import { IPage } from '../../../stores/reducers/pages/slice';
 import { Button } from '../../_shared/Button';
 import { usePageEvents } from './PageItem.hooks';
 import { RenamePopup } from './RenamePopup';
-import { ViewLayoutTypePB } from '../../../../services/backend';
+import { ViewLayoutTypePB } from '@/services/backend';
 import { useEffect, useRef, useState } from 'react';
 import { PAGE_ITEM_HEIGHT } from '../../_shared/constants';
 

+ 2 - 2
frontend/appflowy_tauri/src/appflowy_app/components/user/application/notifications/parser.ts

@@ -1,5 +1,5 @@
-import { FlowyError, UserNotification } from '../../../../../services/backend';
-import { NotificationParser, OnNotificationError } from '../../../../../services/backend/notifications';
+import { FlowyError, UserNotification } from '@/services/backend';
+import { NotificationParser, OnNotificationError } from '@/services/backend/notifications';
 import { Result } from 'ts-results';
 
 declare type UserNotificationCallback = (ty: UserNotification, payload: Result<Uint8Array, FlowyError>) => void;

+ 2 - 2
frontend/appflowy_tauri/src/appflowy_app/components/user/application/notifications/user_listener.ts

@@ -1,5 +1,5 @@
-import { FlowyError, UserNotification, UserProfilePB } from '../../../../../services/backend';
-import { AFNotificationObserver, OnNotificationError } from '../../../../../services/backend/notifications';
+import { FlowyError, UserNotification, UserProfilePB } from '@/services/backend';
+import { AFNotificationObserver, OnNotificationError } from '@/services/backend/notifications';
 import { UserNotificationParser } from './parser';
 import { Ok, Result } from 'ts-results';
 

+ 2 - 3
frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/cell/cell_bd_svc.ts

@@ -1,6 +1,5 @@
-import { DatabaseEventGetCell, DatabaseEventUpdateCell } from '../../../../../services/backend/events/flowy-database';
-import { CellChangesetPB, CellIdPB } from '../../../../../services/backend/models/flowy-database/cell_entities';
-import { FieldType } from '../../../../../services/backend/models/flowy-database/field_entities';
+import { DatabaseEventGetCell, DatabaseEventUpdateCell } from '@/services/backend/events/flowy-database';
+import { CellChangesetPB, CellIdPB, FieldType } from '@/services/backend';
 
 class CellIdentifier {
   constructor(

+ 1 - 0
frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/cell/cell_controller.ts

@@ -108,6 +108,7 @@ export class CellController<T, D> {
   };
 
   dispose = async () => {
+    this.cellDataNotifier.unsubscribe();
     await this.cellObserver.unsubscribe();
     await this.fieldNotifier.unsubscribe();
   };

+ 2 - 2
frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/cell/cell_observer.ts

@@ -1,7 +1,7 @@
 import { Ok, Result } from 'ts-results';
-import { ChangeNotifier } from '../../../../utils/change_notifier';
+import { ChangeNotifier } from '$app/utils/change_notifier';
 import { DatabaseNotificationObserver } from '../notifications/observer';
-import { DatabaseNotification, FlowyError } from '../../../../../services/backend';
+import { DatabaseNotification, FlowyError } from '@/services/backend';
 
 type UpdateCellNotifiedValue = Result<void, FlowyError>;
 

+ 1 - 1
frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/cell/controller_builder.ts

@@ -1,4 +1,4 @@
-import { DateCellDataPB, FieldType, SelectOptionCellDataPB, URLCellDataPB } from '../../../../../services/backend';
+import { DateCellDataPB, FieldType, SelectOptionCellDataPB, URLCellDataPB } from '@/services/backend';
 import { CellIdentifier } from './cell_bd_svc';
 import { CellController } from './cell_controller';
 import {

+ 2 - 4
frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/cell/data_parser.ts

@@ -1,10 +1,8 @@
 import utf8 from 'utf8';
 import { CellBackendService, CellIdentifier } from './cell_bd_svc';
-import { DateCellDataPB } from '../../../../../services/backend/models/flowy-database/date_type_option_entities';
-import { SelectOptionCellDataPB } from '../../../../../services/backend/models/flowy-database/select_type_option';
-import { URLCellDataPB } from '../../../../../services/backend/models/flowy-database/url_type_option_entities';
+import { SelectOptionCellDataPB, URLCellDataPB, DateCellDataPB } from '@/services/backend';
 import { Err, None, Ok, Option, Some } from 'ts-results';
-import { Log } from '../../../../utils/log';
+import { Log } from '$app/utils/log';
 
 abstract class CellDataParser<T> {
   abstract parserData(data: Uint8Array): Option<T>;

+ 2 - 4
frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/cell/data_persistence.ts

@@ -1,10 +1,8 @@
 import { Result } from 'ts-results';
-import { FlowyError } from '../../../../../services/backend/models/flowy-error';
 import { CellBackendService, CellIdentifier } from './cell_bd_svc';
 import { CalendarData } from './controller_builder';
-import { DateChangesetPB } from '../../../../../services/backend/models/flowy-database/date_type_option_entities';
-import { CellIdPB } from '../../../../../services/backend/models/flowy-database/cell_entities';
-import { DatabaseEventUpdateDateCell } from '../../../../../services/backend/events/flowy-database';
+import { DateChangesetPB, FlowyError, CellIdPB } from '@/services/backend';
+import { DatabaseEventUpdateDateCell } from '@/services/backend/events/flowy-database';
 
 export abstract class CellDataPersistence<D> {
   abstract save(data: D): Promise<Result<void, FlowyError>>;

+ 2 - 2
frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/cell/select_option_bd_svc.ts

@@ -5,13 +5,13 @@ import {
   SelectOptionCellChangesetPB,
   SelectOptionChangesetPB,
   SelectOptionPB,
-} from '../../../../../services/backend';
+} from '@/services/backend';
 import {
   DatabaseEventCreateSelectOption,
   DatabaseEventGetSelectOptionCellData,
   DatabaseEventUpdateSelectOption,
   DatabaseEventUpdateSelectOptionCell,
-} from '../../../../../services/backend/events/flowy-database';
+} from '@/services/backend/events/flowy-database';
 
 export class SelectOptionBackendService {
   constructor(public readonly viewId: string, public readonly fieldId: string) {}

+ 37 - 3
frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/database_bd_svc.ts

@@ -1,15 +1,20 @@
 import {
   DatabaseEventCreateRow,
   DatabaseEventGetDatabase,
+  DatabaseEventGetDatabaseSetting,
   DatabaseEventGetFields,
   DatabaseEventGetGroup,
   DatabaseEventGetGroups,
+  DatabaseEventMoveField,
   DatabaseEventMoveGroup,
   DatabaseEventMoveGroupRow,
+  DatabaseEventMoveRow,
   DatabaseGroupIdPB,
+  MoveFieldPayloadPB,
   MoveGroupPayloadPB,
   MoveGroupRowPayloadPB,
-} from '../../../../services/backend/events/flowy-database';
+  MoveRowPayloadPB,
+} from '@/services/backend/events/flowy-database';
 import {
   GetFieldPayloadPB,
   RepeatedFieldIdPB,
@@ -17,9 +22,10 @@ import {
   DatabaseViewIdPB,
   CreateRowPayloadPB,
   ViewIdPB,
-} from '../../../../services/backend';
-import { FolderEventCloseView } from '../../../../services/backend/events/flowy-folder';
+} from '@/services/backend';
+import { FolderEventCloseView } from '@/services/backend/events/flowy-folder';
 
+/// A service that wraps the backend service
 export class DatabaseBackendService {
   viewId: string;
 
@@ -27,6 +33,7 @@ export class DatabaseBackendService {
     this.viewId = viewId;
   }
 
+  /// Open a database
   openDatabase = async () => {
     const payload = DatabaseViewIdPB.fromObject({
       value: this.viewId,
@@ -34,6 +41,7 @@ export class DatabaseBackendService {
     return DatabaseEventGetDatabase(payload);
   };
 
+  /// Close a database
   closeDatabase = async () => {
     const payload = ViewIdPB.fromObject({ value: this.viewId });
     return FolderEventCloseView(payload);
@@ -72,6 +80,15 @@ export class DatabaseBackendService {
     return DatabaseEventMoveGroupRow(payload);
   };
 
+  exchangeRow = (fromRowId: string, toRowId: string) => {
+    const payload = MoveRowPayloadPB.fromObject({
+      view_id: this.viewId,
+      from_row_id: fromRowId,
+      to_row_id: toRowId,
+    });
+    return DatabaseEventMoveRow(payload);
+  };
+
   moveGroup = (fromGroupId: string, toGroupId: string) => {
     const payload = MoveGroupPayloadPB.fromObject({
       view_id: this.viewId,
@@ -81,6 +98,17 @@ export class DatabaseBackendService {
     return DatabaseEventMoveGroup(payload);
   };
 
+  moveField = (fieldId: string, fromIndex: number, toIndex: number) => {
+    const payload = MoveFieldPayloadPB.fromObject({
+      view_id: this.viewId,
+      field_id: fieldId,
+      from_index: fromIndex,
+      to_index: toIndex,
+    });
+    return DatabaseEventMoveField(payload);
+  };
+
+  /// Get all fields in database
   getFields = async (fieldIds?: FieldIdPB[]) => {
     const payload = GetFieldPayloadPB.fromObject({ view_id: this.viewId });
 
@@ -91,6 +119,7 @@ export class DatabaseBackendService {
     return DatabaseEventGetFields(payload).then((result) => result.map((value) => value.items));
   };
 
+  /// Get a group by id
   getGroup = (groupId: string) => {
     const payload = DatabaseGroupIdPB.fromObject({ view_id: this.viewId, group_id: groupId });
     return DatabaseEventGetGroup(payload);
@@ -102,4 +131,9 @@ export class DatabaseBackendService {
     const payload = DatabaseViewIdPB.fromObject({ value: this.viewId });
     return DatabaseEventGetGroups(payload);
   };
+
+  getSettings = () => {
+    const payload = DatabaseViewIdPB.fromObject({ value: this.viewId });
+    return DatabaseEventGetDatabaseSetting(payload);
+  };
 }

+ 30 - 3
frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/database_controller.ts

@@ -1,13 +1,13 @@
 import { DatabaseBackendService } from './database_bd_svc';
 import { FieldController, FieldInfo } from './field/field_controller';
 import { DatabaseViewCache } from './view/database_view_cache';
-import { DatabasePB, GroupPB } from '../../../../services/backend';
+import { DatabasePB, FlowyError, GroupPB } from '@/services/backend';
 import { RowChangedReason, RowInfo } from './row/row_cache';
-import { Err } from 'ts-results';
+import { Err, Ok } from 'ts-results';
 import { DatabaseGroupController } from './group/group_controller';
 import { BehaviorSubject } from 'rxjs';
 import { DatabaseGroupObserver } from './group/group_observer';
-import { Log } from '../../../utils/log';
+import { Log } from '$app/utils/log';
 
 export type DatabaseSubscriberCallbacks = {
   onViewChanged?: (data: DatabasePB) => void;
@@ -71,6 +71,20 @@ export class DatabaseController {
     }
   };
 
+  getGroupByFieldId = async () => {
+    const settingsResult = await this.backendService.getSettings();
+    if (settingsResult.ok) {
+      const settings = settingsResult.val;
+      const groupConfig = settings.group_configurations.items;
+      if (groupConfig.length === 0) {
+        return Err(new FlowyError({ msg: 'this database has no groups' }));
+      }
+      return Ok(settings.group_configurations.items[0].field_id);
+    } else {
+      return Err(settingsResult.val);
+    }
+  };
+
   createRow = () => {
     return this.backendService.createRow();
   };
@@ -79,10 +93,19 @@ export class DatabaseController {
     return this.backendService.moveGroupRow(rowId, groupId);
   };
 
+  exchangeRow = async (fromRowId: string, toRowId: string) => {
+    await this.backendService.exchangeRow(fromRowId, toRowId);
+    await this.loadGroup();
+  };
+
   moveGroup = (fromGroupId: string, toGroupId: string) => {
     return this.backendService.moveGroup(fromGroupId, toGroupId);
   };
 
+  moveField = (fieldId: string, fromIndex: number, toIndex: number) => {
+    return this.backendService.moveField(fieldId, fromIndex, toIndex);
+  };
+
   private loadGroup = async () => {
     const result = await this.backendService.loadGroups();
     if (result.ok) {
@@ -146,6 +169,10 @@ export class DatabaseController {
   };
 
   dispose = async () => {
+    this.groups.value.forEach((group) => {
+      void group.dispose();
+    });
+    await this.groupsObserver.unsubscribe();
     await this.backendService.closeDatabase();
     await this.fieldController.dispose();
     await this.databaseViewCache.dispose();

+ 2 - 2
frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/field/field_bd_svc.ts

@@ -5,14 +5,14 @@ import {
   FieldType,
   TypeOptionChangesetPB,
   TypeOptionPathPB,
-} from '../../../../../services/backend/models/flowy-database/field_entities';
+} from '@/services/backend';
 import {
   DatabaseEventDeleteField,
   DatabaseEventDuplicateField,
   DatabaseEventGetTypeOption,
   DatabaseEventUpdateField,
   DatabaseEventUpdateFieldTypeOption,
-} from '../../../../../services/backend/events/flowy-database';
+} from '@/services/backend/events/flowy-database';
 
 export abstract class TypeOptionParser<T> {
   abstract fromBuffer(buffer: Uint8Array): T;

+ 3 - 3
frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/field/field_controller.ts

@@ -1,8 +1,8 @@
-import { Log } from '../../../../utils/log';
+import { Log } from '$app/utils/log';
 import { DatabaseBackendService } from '../database_bd_svc';
 import { DatabaseFieldChangesetObserver } from './field_observer';
-import { FieldIdPB, FieldPB, IndexFieldPB } from '../../../../../services/backend';
-import { ChangeNotifier } from '../../../../utils/change_notifier';
+import { FieldIdPB, FieldPB, IndexFieldPB } from '@/services/backend';
+import { ChangeNotifier } from '$app/utils/change_notifier';
 
 export class FieldController {
   private backendService: DatabaseBackendService;

+ 2 - 2
frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/field/field_observer.ts

@@ -1,6 +1,6 @@
 import { Ok, Result } from 'ts-results';
-import { DatabaseNotification, DatabaseFieldChangesetPB, FlowyError, FieldPB } from '../../../../../services/backend';
-import { ChangeNotifier } from '../../../../utils/change_notifier';
+import { DatabaseNotification, DatabaseFieldChangesetPB, FlowyError, FieldPB } from '@/services/backend';
+import { ChangeNotifier } from '$app/utils/change_notifier';
 import { DatabaseNotificationObserver } from '../notifications/observer';
 
 export type FieldChangesetSubscribeCallback = (value: Result<DatabaseFieldChangesetPB, FlowyError>) => void;

+ 2 - 2
frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/field/type_option/type_option_bd_svc.ts

@@ -3,12 +3,12 @@ import {
   FieldType,
   TypeOptionPathPB,
   UpdateFieldTypePayloadPB,
-} from '../../../../../../services/backend';
+} from '@/services/backend';
 import {
   DatabaseEventCreateTypeOption,
   DatabaseEventGetTypeOption,
   DatabaseEventUpdateFieldType,
-} from '../../../../../../services/backend/events/flowy-database';
+} from '@/services/backend/events/flowy-database';
 
 export class TypeOptionBackendService {
   constructor(public readonly viewId: string) {}

+ 1 - 1
frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/field/type_option/type_option_context.ts

@@ -9,7 +9,7 @@ import {
   NumberTypeOptionPB,
   SingleSelectTypeOptionPB,
   URLTypeOptionPB,
-} from '../../../../../../services/backend';
+} from '@/services/backend';
 import { utf8Decoder, utf8Encoder } from '../../cell/data_parser';
 import { DatabaseFieldObserver } from '../field_observer';
 

+ 3 - 3
frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/field/type_option/type_option_controller.ts

@@ -1,7 +1,7 @@
-import { FieldPB, FieldType, TypeOptionPB } from '../../../../../../services/backend';
-import { ChangeNotifier } from '../../../../../utils/change_notifier';
+import { FieldPB, FieldType, TypeOptionPB } from '@/services/backend';
+import { ChangeNotifier } from '$app/utils/change_notifier';
 import { FieldBackendService } from '../field_bd_svc';
-import { Log } from '../../../../../utils/log';
+import { Log } from '$app/utils/log';
 import { None, Option, Some } from 'ts-results';
 import { FieldInfo } from '../field_controller';
 import { TypeOptionBackendService } from './type_option_bd_svc';

+ 3 - 9
frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/group/group_controller.ts

@@ -1,14 +1,8 @@
-import {
-  DatabaseNotification,
-  FlowyError,
-  GroupPB,
-  GroupRowsNotificationPB,
-  RowPB,
-} from '../../../../../services/backend';
-import { ChangeNotifier } from '../../../../utils/change_notifier';
+import { DatabaseNotification, FlowyError, GroupPB, GroupRowsNotificationPB, RowPB } from '@/services/backend';
+import { ChangeNotifier } from '$app/utils/change_notifier';
 import { None, Ok, Option, Result, Some } from 'ts-results';
 import { DatabaseNotificationObserver } from '../notifications/observer';
-import { Log } from '../../../../utils/log';
+import { Log } from '$app/utils/log';
 import { DatabaseBackendService } from '../database_bd_svc';
 
 export type GroupDataCallbacks = {

+ 2 - 2
frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/group/group_observer.ts

@@ -1,6 +1,6 @@
-import { ChangeNotifier } from '../../../../utils/change_notifier';
+import { ChangeNotifier } from '$app/utils/change_notifier';
 import { Ok, Result } from 'ts-results';
-import { DatabaseNotification, FlowyError, GroupChangesetPB, GroupPB } from '../../../../../services/backend';
+import { DatabaseNotification, FlowyError, GroupChangesetPB, GroupPB } from '@/services/backend';
 import { DatabaseNotificationObserver } from '../notifications/observer';
 
 export type GroupByFieldCallback = (value: Result<GroupPB[], FlowyError>) => void;

+ 2 - 2
frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/notifications/observer.ts

@@ -1,5 +1,5 @@
-import { DatabaseNotification, FlowyError } from '../../../../../services/backend';
-import { AFNotificationObserver } from '../../../../../services/backend/notifications';
+import { DatabaseNotification, FlowyError } from '@/services/backend';
+import { AFNotificationObserver } from '@/services/backend/notifications';
 import { DatabaseNotificationParser } from './parser';
 import { Result } from 'ts-results';
 

+ 2 - 2
frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/notifications/parser.ts

@@ -1,5 +1,5 @@
-import { DatabaseNotification, FlowyError } from '../../../../../services/backend';
-import { NotificationParser } from '../../../../../services/backend/notifications';
+import { DatabaseNotification, FlowyError } from '@/services/backend';
+import { NotificationParser } from '@/services/backend/notifications';
 import { Result } from 'ts-results';
 
 declare type DatabaseNotificationCallback = (ty: DatabaseNotification, payload: Result<Uint8Array, FlowyError>) => void;

+ 2 - 2
frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/row/row_bd_svc.ts

@@ -1,10 +1,10 @@
-import { CreateRowPayloadPB, RowIdPB } from '../../../../../services/backend';
+import { CreateRowPayloadPB, RowIdPB } from '@/services/backend';
 import {
   DatabaseEventCreateRow,
   DatabaseEventDeleteRow,
   DatabaseEventDuplicateRow,
   DatabaseEventGetRow,
-} from '../../../../../services/backend/events/flowy-database';
+} from '@/services/backend/events/flowy-database';
 
 export class RowBackendService {
   constructor(public readonly viewId: string) {}

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

@@ -7,14 +7,14 @@ import {
   RowsChangesetPB,
   RowsVisibilityChangesetPB,
   ReorderSingleRowPB,
-} from '../../../../../services/backend';
-import { ChangeNotifier } from '../../../../utils/change_notifier';
+} from '@/services/backend';
+import { ChangeNotifier } from '$app/utils/change_notifier';
 import { FieldInfo } from '../field/field_controller';
 import { CellCache, CellCacheKey } from '../cell/cell_cache';
 import { CellIdentifier } from '../cell/cell_bd_svc';
-import { DatabaseEventGetRow } from '../../../../../services/backend/events/flowy-database';
+import { DatabaseEventGetRow } from '@/services/backend/events/flowy-database';
 import { None, Option, Some } from 'ts-results';
-import { Log } from '../../../../utils/log';
+import { Log } from '$app/utils/log';
 
 export type CellByFieldId = Map<string, CellIdentifier>;
 

+ 1 - 2
frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/view/database_view_cache.ts

@@ -1,8 +1,7 @@
 import { DatabaseViewRowsObserver } from './view_row_observer';
 import { RowCache, RowInfo } from '../row/row_cache';
 import { FieldController } from '../field/field_controller';
-import { RowPB } from '../../../../../services/backend';
-import { Subscription } from 'rxjs';
+import { RowPB } from '@/services/backend';
 
 export class DatabaseViewCache {
   private readonly rowsObserver: DatabaseViewRowsObserver;

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

@@ -6,8 +6,8 @@ import {
   ReorderSingleRowPB,
   RowsChangesetPB,
   RowsVisibilityChangesetPB,
-} from '../../../../../services/backend';
-import { ChangeNotifier } from '../../../../utils/change_notifier';
+} from '@/services/backend';
+import { ChangeNotifier } from '$app/utils/change_notifier';
 import { DatabaseNotificationObserver } from '../notifications/observer';
 
 export type RowsVisibilityNotifyValue = Result<RowsVisibilityChangesetPB, FlowyError>;

+ 3 - 3
frontend/appflowy_tauri/src/appflowy_app/stores/effects/document/document_bd_svc.ts

@@ -5,10 +5,10 @@ import {
   FlowyError,
   OpenDocumentPayloadPB,
   ViewIdPB,
-} from '../../../../services/backend';
-import { DocumentEventApplyEdit, DocumentEventGetDocument } from '../../../../services/backend/events/flowy-document';
+} from '@/services/backend';
+import { DocumentEventApplyEdit, DocumentEventGetDocument } from '@/services/backend/events/flowy-document';
 import { Result } from 'ts-results';
-import { FolderEventCloseView } from '../../../../services/backend/events/flowy-folder';
+import { FolderEventCloseView } from '@/services/backend/events/flowy-folder';
 
 export class DocumentBackendService {
   constructor(public readonly viewId: string) {}

+ 2 - 2
frontend/appflowy_tauri/src/appflowy_app/stores/effects/folder/app/app_bd_svc.ts

@@ -6,7 +6,7 @@ import {
   FolderEventReadApp,
   FolderEventUpdateApp,
   ViewLayoutTypePB,
-} from '../../../../../services/backend/events/flowy-folder';
+} from '@/services/backend/events/flowy-folder';
 import {
   AppIdPB,
   UpdateAppPayloadPB,
@@ -16,7 +16,7 @@ import {
   MoveFolderItemPayloadPB,
   MoveFolderItemType,
   FlowyError,
-} from '../../../../../services/backend';
+} from '@/services/backend';
 import { None, Result, Some } from 'ts-results';
 
 export class AppBackendService {

+ 2 - 2
frontend/appflowy_tauri/src/appflowy_app/stores/effects/folder/app/app_observer.ts

@@ -1,6 +1,6 @@
 import { Ok, Result } from 'ts-results';
-import { AppPB, FlowyError, FolderNotification } from '../../../../../services/backend';
-import { ChangeNotifier } from '../../../../utils/change_notifier';
+import { AppPB, FlowyError, FolderNotification } from '@/services/backend';
+import { ChangeNotifier } from '$app/utils/change_notifier';
 import { FolderNotificationObserver } from '../notifications/observer';
 
 export type AppUpdateNotifyCallback = (value: Result<AppPB, FlowyError>) => void;

+ 2 - 2
frontend/appflowy_tauri/src/appflowy_app/stores/effects/folder/notifications/observer.ts

@@ -1,6 +1,6 @@
-import { OnNotificationError, AFNotificationObserver } from '../../../../../services/backend/notifications';
+import { OnNotificationError, AFNotificationObserver } from '@/services/backend/notifications';
 import { FolderNotificationParser } from './parser';
-import { FlowyError, FolderNotification } from '../../../../../services/backend';
+import { FlowyError, FolderNotification } from '@/services/backend';
 import { Result } from 'ts-results';
 
 export type ParserHandler = (notification: FolderNotification, payload: Result<Uint8Array, FlowyError>) => void;

+ 2 - 2
frontend/appflowy_tauri/src/appflowy_app/stores/effects/folder/notifications/parser.ts

@@ -1,5 +1,5 @@
-import { NotificationParser, OnNotificationError } from '../../../../../services/backend/notifications';
-import { FlowyError, FolderNotification } from '../../../../../services/backend';
+import { NotificationParser, OnNotificationError } from '@/services/backend/notifications';
+import { FlowyError, FolderNotification } from '@/services/backend';
 import { Result } from 'ts-results';
 
 declare type FolderNotificationCallback = (ty: FolderNotification, payload: Result<Uint8Array, FlowyError>) => void;

+ 2 - 2
frontend/appflowy_tauri/src/appflowy_app/stores/effects/folder/view/view_bd_svc.ts

@@ -1,9 +1,9 @@
-import { UpdateViewPayloadPB, RepeatedViewIdPB, ViewPB } from '../../../../../services/backend/models/flowy-folder/view';
+import { UpdateViewPayloadPB, RepeatedViewIdPB, ViewPB } from '@/services/backend';
 import {
   FolderEventDeleteView,
   FolderEventDuplicateView,
   FolderEventUpdateView,
-} from '../../../../../services/backend/events/flowy-folder';
+} from '@/services/backend/events/flowy-folder';
 
 export class ViewBackendService {
   constructor(public readonly viewId: string) {}

+ 2 - 3
frontend/appflowy_tauri/src/appflowy_app/stores/effects/folder/view/view_observer.ts

@@ -1,7 +1,6 @@
 import { Ok, Result } from 'ts-results';
-import { FlowyError } from '../../../../../services/backend/models/flowy-error/errors';
-import { DeletedViewPB, FolderNotification, ViewPB } from '../../../../../services/backend/models/flowy-folder';
-import { ChangeNotifier } from '../../../../utils/change_notifier';
+import { DeletedViewPB, FolderNotification, ViewPB, FlowyError } from '@/services/backend';
+import { ChangeNotifier } from '$app/utils/change_notifier';
 import { FolderNotificationObserver } from '../notifications/observer';
 
 type DeleteViewNotifyValue = Result<ViewPB, FlowyError>;

+ 2 - 2
frontend/appflowy_tauri/src/appflowy_app/stores/effects/folder/workspace/workspace_bd_svc.ts

@@ -4,8 +4,8 @@ import {
   FolderEventMoveItem,
   FolderEventReadWorkspaceApps,
   FolderEventReadWorkspaces,
-} from '../../../../../services/backend/events/flowy-folder';
-import { CreateAppPayloadPB, WorkspaceIdPB, FlowyError, MoveFolderItemPayloadPB } from '../../../../../services/backend';
+} from '@/services/backend/events/flowy-folder';
+import { CreateAppPayloadPB, WorkspaceIdPB, FlowyError, MoveFolderItemPayloadPB } from '@/services/backend';
 import assert from 'assert';
 
 export class WorkspaceBackendService {

+ 2 - 2
frontend/appflowy_tauri/src/appflowy_app/stores/effects/folder/workspace/workspace_observer.ts

@@ -1,6 +1,6 @@
 import { Ok, Result } from 'ts-results';
-import { AppPB, FolderNotification, RepeatedAppPB, WorkspacePB, FlowyError } from '../../../../../services/backend';
-import { ChangeNotifier } from '../../../../utils/change_notifier';
+import { AppPB, FolderNotification, RepeatedAppPB, WorkspacePB, FlowyError } from '@/services/backend';
+import { ChangeNotifier } from '$app/utils/change_notifier';
 import { FolderNotificationObserver } from '../notifications/observer';
 
 export type AppListNotifyValue = Result<AppPB[], FlowyError>;

+ 3 - 3
frontend/appflowy_tauri/src/appflowy_app/stores/effects/user/user_bd_svc.ts

@@ -6,7 +6,7 @@ import {
   UserEventSignOut,
   UserEventSignUp,
   UserEventUpdateUserProfile,
-} from '../../../../services/backend/events/flowy-user';
+} from '@/services/backend/events/flowy-user';
 import {
   SignInPayloadPB,
   SignUpPayloadPB,
@@ -15,13 +15,13 @@ import {
   CreateWorkspacePayloadPB,
   WorkspaceSettingPB,
   WorkspacePB,
-} from '../../../../services/backend';
+} from '@/services/backend';
 import {
   FolderEventCreateWorkspace,
   FolderEventOpenWorkspace,
   FolderEventReadCurrentWorkspace,
   FolderEventReadWorkspaces,
-} from '../../../../services/backend/events/flowy-folder';
+} from '@/services/backend/events/flowy-folder';
 
 export class UserBackendService {
   constructor(public readonly userId: string) {}

+ 1 - 1
frontend/appflowy_tauri/src/appflowy_app/stores/reducers/current-user/slice.ts

@@ -1,6 +1,6 @@
 import { createSlice, PayloadAction } from '@reduxjs/toolkit';
 import { nanoid } from 'nanoid';
-import { WorkspaceSettingPB } from '../../../../services/backend/models/flowy-folder/workspace';
+import { WorkspaceSettingPB } from '@/services/backend/models/flowy-folder/workspace';
 
 export interface ICurrentUser {
   id?: string;

+ 2 - 2
frontend/appflowy_tauri/src/appflowy_app/stores/reducers/database/slice.ts

@@ -1,6 +1,6 @@
 import { createSlice, PayloadAction } from '@reduxjs/toolkit';
-import { FieldType } from '../../../../services/backend/models/flowy-database/field_entities';
-import { DateFormat, NumberFormat, SelectOptionColorPB, TimeFormat } from '../../../../services/backend';
+import { FieldType } from '@/services/backend/models/flowy-database/field_entities';
+import { DateFormat, NumberFormat, SelectOptionColorPB, TimeFormat } from '@/services/backend';
 
 export interface ISelectOption {
   selectOptionId: string;

+ 2 - 2
frontend/appflowy_tauri/src/appflowy_app/stores/reducers/folders/notifications/parser.ts

@@ -1,5 +1,5 @@
-import { FlowyError, FolderNotification } from '../../../../../services/backend';
-import { NotificationParser, OnNotificationError } from '../../../../../services/backend/notifications';
+import { FlowyError, FolderNotification } from '@/services/backend';
+import { NotificationParser, OnNotificationError } from '@/services/backend/notifications';
 import { Result } from 'ts-results';
 
 declare type FolderNotificationCallback = (ty: FolderNotification, payload: Result<Uint8Array, FlowyError>) => void;

+ 1 - 1
frontend/appflowy_tauri/src/appflowy_app/stores/reducers/grid/slice.ts

@@ -1,6 +1,6 @@
 import { createSlice, PayloadAction } from '@reduxjs/toolkit';
 import { nanoid } from 'nanoid';
-import { FieldType } from '../../../../services/backend/models/flowy-database/field_entities';
+import { FieldType } from '@/services/backend/models/flowy-database/field_entities';
 
 const initialState = {
   title: 'My plans on the week',

+ 1 - 1
frontend/appflowy_tauri/src/appflowy_app/stores/reducers/pages/slice.ts

@@ -1,5 +1,5 @@
 import { createSlice, PayloadAction } from '@reduxjs/toolkit';
-import { ViewLayoutTypePB } from '../../../../services/backend';
+import { ViewLayoutTypePB } from '@/services/backend';
 
 export interface IPage {
   id: string;

+ 1 - 0
frontend/appflowy_tauri/src/main.tsx

@@ -4,5 +4,6 @@ import App from './appflowy_app/App';
 import './styles/tailwind.css';
 import './styles/font.css';
 import './styles/template.css';
+import './styles/Calendar.css';
 
 ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(<App />);

+ 141 - 0
frontend/appflowy_tauri/src/styles/Calendar.css

@@ -0,0 +1,141 @@
+.react-calendar {
+  width: 250px;
+  max-width: 100%;
+  background: white;
+  line-height: 1.125em;
+}
+
+.react-calendar--doubleView {
+  width: 700px;
+}
+
+.react-calendar--doubleView .react-calendar__viewContainer {
+  display: flex;
+  margin: -0.5em;
+}
+
+.react-calendar--doubleView .react-calendar__viewContainer > * {
+  width: 50%;
+  margin: 0.5em;
+}
+
+.react-calendar,
+.react-calendar *,
+.react-calendar *:before,
+.react-calendar *:after {
+  -moz-box-sizing: border-box;
+  -webkit-box-sizing: border-box;
+  box-sizing: border-box;
+}
+
+.react-calendar button {
+  margin: 0;
+  border: 0;
+  outline: none;
+}
+
+.react-calendar button:enabled:hover {
+  cursor: pointer;
+}
+
+.react-calendar__navigation {
+  display: flex;
+  height: 44px;
+  margin-bottom: 1em;
+}
+
+.react-calendar__navigation button {
+  min-width: 44px;
+  background: none;
+}
+
+.react-calendar__navigation button:disabled {
+  background-color: #f0f0f0;
+}
+
+.react-calendar__navigation button:enabled:hover,
+.react-calendar__navigation button:enabled:focus {
+  background-color: #e6e6e6;
+}
+
+.react-calendar__month-view__weekdays {
+  @apply mb-2 text-center text-xs uppercase text-shade-3;
+}
+
+.react-calendar__month-view__weekdays abbr {
+  @apply no-underline;
+}
+
+.react-calendar__month-view__weekdays__weekday {
+  padding: 0.5em;
+}
+
+.react-calendar__month-view__weekNumbers .react-calendar__tile {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 0.75em;
+  font-weight: 400;
+}
+
+.react-calendar__month-view__days__day--weekend {
+  @apply text-main-alert;
+}
+
+.react-calendar__month-view__days__day--neighboringMonth {
+  @apply text-shade-4;
+}
+
+.react-calendar__year-view .react-calendar__tile,
+.react-calendar__decade-view .react-calendar__tile,
+.react-calendar__century-view .react-calendar__tile {
+  padding: 2em 0.5em;
+}
+
+.react-calendar__tile {
+  max-width: 100%;
+  background: none;
+  text-align: center;
+  line-height: 16px;
+  @apply rounded py-2;
+}
+
+.react-calendar__tile:disabled {
+  background-color: #f0f0f0;
+}
+
+.react-calendar__tile:enabled:hover,
+.react-calendar__tile:enabled:focus {
+  background-color: #e6e6e6;
+}
+
+.react-calendar__tile--now {
+  @apply bg-shade-6;
+}
+
+.react-calendar__tile--now:enabled:hover,
+.react-calendar__tile--now:enabled:focus {
+  @apply bg-shade-6;
+}
+
+.react-calendar__tile--hasActive {
+  background: #76baff;
+}
+
+.react-calendar__tile--hasActive:enabled:hover,
+.react-calendar__tile--hasActive:enabled:focus {
+  background: #a9d4ff;
+}
+
+.react-calendar__tile--active {
+  @apply bg-main-accent text-white;
+}
+
+.react-calendar__tile--active:enabled:hover,
+.react-calendar__tile--active:enabled:focus {
+  @apply bg-main-hovered;
+}
+
+.react-calendar--selectRange .react-calendar__tile--hover {
+  @apply bg-shade-4;
+}

+ 9 - 1
frontend/appflowy_tauri/tailwind.config.cjs

@@ -1,6 +1,11 @@
 /** @type {import('tailwindcss').Config} */
 module.exports = {
-  content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
+  content: [
+    './index.html',
+    './src/**/*.{js,ts,jsx,tsx}',
+    './node_modules/react-tailwindcss-datepicker/dist/index.esm.js',
+  ],
+  darkMode: 'class',
   theme: {
     extend: {
       colors: {
@@ -41,6 +46,9 @@ module.exports = {
           fiol: '#2C144B',
         },
       },
+      boxShadow: {
+        md: '0px 0px 20px rgba(0, 0, 0, 0.1);',
+      },
     },
   },
   plugins: [],