소스 검색

Fix/tauri board events (#2678)

* chore: focus name input on field edit

* fix: remove random id for duplicated view

* fix: use alias for imports

* fix: folder, grid, board bugs

* chore: remove log

* fix: update options list on add

* chore: close on delete option

* chore: show and hide field

* chore: add field with specific type

* chore: small cleanup

* fix: create view on another folder and views notifier reorganize

---------

Co-authored-by: qinluhe <[email protected]>
Askarbek Zadauly 1 년 전
부모
커밋
6a43dd871d
26개의 변경된 파일254개의 추가작업 그리고 116개의 파일을 삭제
  1. 1 0
      frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/CheckList/EditCheckListPopup.tsx
  2. 15 1
      frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/EditFieldPopup.tsx
  3. 20 15
      frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/EditRow.tsx
  4. 1 0
      frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/Options/EditCellOptionPopup.tsx
  5. 57 12
      frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/PropertiesPanel.tsx
  6. 10 6
      frontend/appflowy_tauri/src/appflowy_app/components/_shared/database-hooks/loadField.ts
  7. 33 2
      frontend/appflowy_tauri/src/appflowy_app/components/_shared/database-hooks/useCell.ts
  8. 7 3
      frontend/appflowy_tauri/src/appflowy_app/components/_shared/database-hooks/useDatabase.ts
  9. 7 3
      frontend/appflowy_tauri/src/appflowy_app/components/_shared/database-hooks/useRow.ts
  10. 1 2
      frontend/appflowy_tauri/src/appflowy_app/components/board/Board.tsx
  11. 5 1
      frontend/appflowy_tauri/src/appflowy_app/components/board/BoardCard.tsx
  12. 20 7
      frontend/appflowy_tauri/src/appflowy_app/components/board/BoardGroup.tsx
  13. 12 12
      frontend/appflowy_tauri/src/appflowy_app/components/layout/NavigationPanel/FolderItem.hooks.ts
  14. 3 4
      frontend/appflowy_tauri/src/appflowy_app/components/layout/NavigationPanel/NavigationPanel.hooks.ts
  15. 4 4
      frontend/appflowy_tauri/src/appflowy_app/components/layout/NavigationPanel/NavigationPanel.tsx
  16. 2 2
      frontend/appflowy_tauri/src/appflowy_app/components/layout/NavigationPanel/NavigationResizer.tsx
  17. 3 3
      frontend/appflowy_tauri/src/appflowy_app/components/layout/NavigationPanel/NewFolderButton.hooks.ts
  18. 1 5
      frontend/appflowy_tauri/src/appflowy_app/components/layout/NavigationPanel/PageItem.hooks.ts
  19. 1 1
      frontend/appflowy_tauri/src/appflowy_app/components/layout/NavigationPanel/PageItem.tsx
  20. 1 1
      frontend/appflowy_tauri/src/appflowy_app/components/layout/Screen.tsx
  21. 17 9
      frontend/appflowy_tauri/src/appflowy_app/components/layout/Workspace.hooks.ts
  22. 1 1
      frontend/appflowy_tauri/src/appflowy_app/components/layout/WorkspaceUser.tsx
  23. 16 0
      frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/field/type_option/type_option_controller.ts
  24. 11 17
      frontend/appflowy_tauri/src/appflowy_app/stores/effects/folder/app/app_observer.ts
  25. 3 3
      frontend/appflowy_tauri/src/appflowy_app/stores/reducers/database/slice.ts
  26. 2 2
      frontend/appflowy_tauri/src/appflowy_app/stores/reducers/pages/slice.ts

+ 1 - 0
frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/CheckList/EditCheckListPopup.tsx

@@ -53,6 +53,7 @@ export const EditCheckListPopup = ({
   const onDeleteOptionClick = async () => {
     const svc = new SelectOptionCellBackendService(cellIdentifier);
     await svc.deleteOption([editingSelectOption]);
+    onOutsideClick();
   };
 
   return (

+ 15 - 1
frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/EditFieldPopup.tsx

@@ -1,4 +1,4 @@
-import { MouseEventHandler, useEffect, useRef, useState } from 'react';
+import { FocusEventHandler, MouseEventHandler, useEffect, useRef, useState } from 'react';
 import { FieldTypeIcon } from '$app/components/_shared/EditRow/FieldTypeIcon';
 import { FieldTypeName } from '$app/components/_shared/EditRow/FieldTypeName';
 import { useTranslation } from 'react-i18next';
@@ -36,12 +36,24 @@ export const EditFieldPopup = ({
   const databaseStore = useAppSelector((state) => state.database);
   const { t } = useTranslation();
   const changeTypeButtonRef = useRef<HTMLDivElement>(null);
+  const inputRef = useRef<HTMLInputElement>(null);
   const [name, setName] = useState('');
 
   useEffect(() => {
     setName(databaseStore.fields[cellIdentifier.fieldId].title);
   }, [databaseStore, cellIdentifier]);
 
+  // focus input on mount
+  useEffect(() => {
+    if (!inputRef.current || !name) return;
+    inputRef.current.focus();
+  }, [inputRef, name]);
+
+  const selectAll: FocusEventHandler<HTMLInputElement> = (e) => {
+    e.target.selectionStart = 0;
+    e.target.selectionEnd = e.target.value.length;
+  };
+
   const save = async () => {
     if (!fieldInfo) return;
     const controller = new TypeOptionController(viewId, Some(fieldInfo));
@@ -80,6 +92,8 @@ export const EditFieldPopup = ({
     >
       <div className={'flex flex-col gap-2'}>
         <input
+          ref={inputRef}
+          onFocus={selectAll}
           value={name}
           onChange={(e) => setName(e.target.value)}
           onBlur={() => save()}

+ 20 - 15
frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/EditRow.tsx

@@ -22,6 +22,7 @@ import { EditCheckListPopup } from '$app/components/_shared/EditRow/CheckList/Ed
 import { PropertiesPanel } from '$app/components/_shared/EditRow/PropertiesPanel';
 import { ImageSvg } from '$app/components/_shared/svg/ImageSvg';
 import { PromptWindow } from '$app/components/_shared/PromptWindow';
+import { useAppSelector } from '$app/stores/store';
 
 export const EditRow = ({
   onClose,
@@ -34,6 +35,7 @@ export const EditRow = ({
   controller: DatabaseController;
   rowInfo: RowInfo;
 }) => {
+  const databaseStore = useAppSelector((state) => state.database);
   const { cells, onNewColumnClick } = useRow(viewId, controller, rowInfo);
   const { t } = useTranslation();
   const [unveil, setUnveil] = useState(false);
@@ -226,19 +228,21 @@ export const EditRow = ({
                         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={onEditFieldClick}
-                          onEditOptionsClick={onEditOptionsClick}
-                          onEditDateClick={onEditDateClick}
-                          onEditCheckListClick={onEditCheckListClick}
-                        ></EditCellWrapper>
-                      ))}
+                      {cells
+                        .filter((cell) => databaseStore.fields[cell.cellIdentifier.fieldId].visible)
+                        .map((cell, cellIndex) => (
+                          <EditCellWrapper
+                            index={cellIndex}
+                            key={cellIndex}
+                            cellIdentifier={cell.cellIdentifier}
+                            cellCache={controller.databaseViewCache.getRowCache().getCellCache()}
+                            fieldController={controller.fieldController}
+                            onEditFieldClick={onEditFieldClick}
+                            onEditOptionsClick={onEditOptionsClick}
+                            onEditDateClick={onEditDateClick}
+                            onEditCheckListClick={onEditCheckListClick}
+                          ></EditCellWrapper>
+                        ))}
                     </div>
                   )}
                 </Droppable>
@@ -261,6 +265,7 @@ export const EditRow = ({
               controller={controller}
               rowInfo={rowInfo}
               onDeletePropertyClick={onDeletePropertyClick}
+              onNewColumnClick={onNewColumnClick}
             ></PropertiesPanel>
           </div>
 
@@ -292,7 +297,7 @@ export const EditRow = ({
               cellIdentifier={editingCell}
               cellCache={controller.databaseViewCache.getRowCache().getCellCache()}
               fieldController={controller.fieldController}
-              onOutsideClick={() => setShowChangeOptionsPopup(false)}
+              onOutsideClick={() => !showEditCellOption && setShowChangeOptionsPopup(false)}
               openOptionDetail={onOpenOptionDetailClick}
             ></CellOptionsPopup>
           )}
@@ -335,7 +340,7 @@ export const EditRow = ({
               cellIdentifier={editingCell}
               cellCache={controller.databaseViewCache.getRowCache().getCellCache()}
               fieldController={controller.fieldController}
-              onOutsideClick={() => setShowCheckListPopup(false)}
+              onOutsideClick={() => !showEditCheckList && setShowCheckListPopup(false)}
               openCheckListDetail={onOpenCheckListDetailClick}
             ></CheckListPopup>
           )}

+ 1 - 0
frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/Options/EditCellOptionPopup.tsx

@@ -67,6 +67,7 @@ export const EditCellOptionPopup = ({
   const onDeleteOptionClick = async () => {
     const svc = new SelectOptionCellBackendService(cellIdentifier);
     await svc.deleteOption([editingSelectOption]);
+    onOutsideClick();
   };
 
   return (

+ 57 - 12
frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/PropertiesPanel.tsx

@@ -1,5 +1,5 @@
 import { DropDownShowSvg } from '$app/components/_shared/svg/DropDownShowSvg';
-import { useEffect, useState } from 'react';
+import { useState } from 'react';
 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';
@@ -12,6 +12,9 @@ import { TrashSvg } from '$app/components/_shared/svg/TrashSvg';
 import { MultiSelectTypeSvg } from '$app/components/_shared/svg/MultiSelectTypeSvg';
 import { DocumentSvg } from '$app/components/_shared/svg/DocumentSvg';
 import { SingleSelectTypeSvg } from '$app/components/_shared/svg/SingleSelectTypeSvg';
+import { TypeOptionController } from '$app/stores/effects/database/field/type_option/type_option_controller';
+import { Some } from 'ts-results';
+import { useTranslation } from 'react-i18next';
 
 const typesOrder: FieldType[] = [
   FieldType.RichText,
@@ -29,28 +32,67 @@ export const PropertiesPanel = ({
   controller,
   rowInfo,
   onDeletePropertyClick,
+  onNewColumnClick,
 }: {
   viewId: string;
   controller: DatabaseController;
   rowInfo: RowInfo;
   onDeletePropertyClick: (fieldId: string) => void;
+  onNewColumnClick: (initialFieldType: FieldType, name?: string) => Promise<void>;
 }) => {
   const { cells } = useRow(viewId, controller, rowInfo);
   const databaseStore = useAppSelector((state) => state.database);
+  const { t } = useTranslation();
 
   const [showAddedProperties, setShowAddedProperties] = useState(true);
   const [showBasicProperties, setShowBasicProperties] = useState(false);
   const [showAdvancedProperties, setShowAdvancedProperties] = useState(false);
 
   const [hoveredPropertyIndex, setHoveredPropertyIndex] = useState(-1);
-  const [hiddenProperties, setHiddenProperties] = useState<boolean[]>([]);
 
-  useEffect(() => {
-    setHiddenProperties(cells.map(() => false));
-  }, [cells]);
+  const toggleHideProperty = async (v: boolean, index: number) => {
+    const fieldInfo = controller.fieldController.getField(cells[index].fieldId);
+    if (fieldInfo) {
+      const typeController = new TypeOptionController(viewId, Some(fieldInfo));
+      await typeController.initialize();
+      if (fieldInfo.field.visibility) {
+        await typeController.hideField();
+      } else {
+        await typeController.showField();
+      }
+    }
+  };
+
+  const addSelectedFieldType = async (fieldType: FieldType) => {
+    let name = 'New Field';
+    switch (fieldType) {
+      case FieldType.RichText:
+        name = t('grid.field.textFieldName');
+        break;
+      case FieldType.Number:
+        name = t('grid.field.numberFieldName');
+        break;
+      case FieldType.DateTime:
+        name = t('grid.field.dateFieldName');
+        break;
+      case FieldType.SingleSelect:
+        name = t('grid.field.singleSelectFieldName');
+        break;
+      case FieldType.MultiSelect:
+        name = t('grid.field.multiSelectFieldName');
+        break;
+      case FieldType.Checklist:
+        name = t('grid.field.checklistFieldName');
+        break;
+      case FieldType.URL:
+        name = t('grid.field.urlFieldName');
+        break;
+      case FieldType.Checkbox:
+        name = t('grid.field.checkboxFieldName');
+        break;
+    }
 
-  const toggleHideProperty = (v: boolean, index: number) => {
-    setHiddenProperties(hiddenProperties.map((h, i) => (i === index ? !v : h)));
+    await onNewColumnClick(fieldType, name);
   };
 
   return (
@@ -91,7 +133,10 @@ export const PropertiesPanel = ({
                 >
                   <TrashSvg></TrashSvg>
                 </i>
-                <Switch value={!hiddenProperties[cellIndex]} setValue={(v) => toggleHideProperty(v, cellIndex)}></Switch>
+                <Switch
+                  value={!!databaseStore.fields[cell.cellIdentifier.fieldId]?.visible}
+                  setValue={(v) => toggleHideProperty(v, cellIndex)}
+                ></Switch>
               </div>
             </div>
           ))}
@@ -108,17 +153,17 @@ export const PropertiesPanel = ({
       <div className={'flex flex-col gap-2 text-xs'}>
         {showBasicProperties && (
           <div className={'flex flex-col'}>
-            {typesOrder.map((t, i) => (
+            {typesOrder.map((type, i) => (
               <button
-                onClick={() => console.log('type clicked')}
+                onClick={() => addSelectedFieldType(type)}
                 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>
+                  <FieldTypeIcon fieldType={type}></FieldTypeIcon>
                 </i>
                 <span>
-                  <FieldTypeName fieldType={t}></FieldTypeName>
+                  <FieldTypeName fieldType={type}></FieldTypeName>
                 </span>
               </button>
             ))}

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

@@ -1,6 +1,6 @@
-import { TypeOptionController } from '../../../stores/effects/database/field/type_option/type_option_controller';
+import { TypeOptionController } from '$app/stores/effects/database/field/type_option/type_option_controller';
 import { Some } from 'ts-results';
-import { IDatabaseField, ISelectOption } from '../../../stores/reducers/database/slice';
+import { IDatabaseField, ISelectOption } from '$app_reducers/database/slice';
 import { ChecklistTypeOptionPB, FieldType, MultiSelectTypeOptionPB, SingleSelectTypeOptionPB } from '@/services/backend';
 import {
   makeChecklistTypeOptionContext,
@@ -8,10 +8,10 @@ import {
   makeMultiSelectTypeOptionContext,
   makeNumberTypeOptionContext,
   makeSingleSelectTypeOptionContext,
-} from '../../../stores/effects/database/field/type_option/type_option_context';
-import { boardActions } from '../../../stores/reducers/board/slice';
-import { FieldInfo } from '../../../stores/effects/database/field/field_controller';
-import { AppDispatch } from '../../../stores/store';
+} from '$app/stores/effects/database/field/type_option/type_option_context';
+import { boardActions } from '$app_reducers/board/slice';
+import { FieldInfo } from '$app/stores/effects/database/field/field_controller';
+import { AppDispatch } from '$app/stores/store';
 
 export default async function (viewId: string, fieldInfo: FieldInfo, dispatch?: AppDispatch): Promise<IDatabaseField> {
   const field = fieldInfo.field;
@@ -53,6 +53,7 @@ export default async function (viewId: string, fieldInfo: FieldInfo, dispatch?:
         fieldId: field.id,
         title: field.name,
         fieldType: field.field_type,
+        visible: field.visibility,
         fieldOptions: {
           selectOptions,
         },
@@ -64,6 +65,7 @@ export default async function (viewId: string, fieldInfo: FieldInfo, dispatch?:
       return {
         fieldId: field.id,
         title: field.name,
+        visible: field.visibility,
         fieldType: field.field_type,
         fieldOptions: {
           numberFormat: typeOption.format,
@@ -76,6 +78,7 @@ export default async function (viewId: string, fieldInfo: FieldInfo, dispatch?:
       return {
         fieldId: field.id,
         title: field.name,
+        visible: field.visibility,
         fieldType: field.field_type,
         fieldOptions: {
           dateFormat: typeOption.date_format,
@@ -88,6 +91,7 @@ export default async function (viewId: string, fieldInfo: FieldInfo, dispatch?:
       return {
         fieldId: field.id,
         title: field.name,
+        visible: field.visibility,
         fieldType: field.field_type,
       };
     }

+ 33 - 2
frontend/appflowy_tauri/src/appflowy_app/components/_shared/database-hooks/useCell.ts

@@ -2,13 +2,17 @@ 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 { DateCellDataPB, SelectOptionCellDataPB, URLCellDataPB } from '@/services/backend';
 import { useEffect, useState } from 'react';
 import { CellController } from '$app/stores/effects/database/cell/cell_controller';
+import { useAppDispatch, useAppSelector } from '$app/stores/store';
+import { databaseActions, ISelectOptionType } from '$app_reducers/database/slice';
 
 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>>();
+  const databaseStore = useAppSelector((state) => state.database);
+  const dispatch = useAppDispatch();
 
   useEffect(() => {
     if (!cellIdentifier || !cellCache || !fieldController) return;
@@ -19,7 +23,34 @@ export const useCell = (cellIdentifier: CellIdentifier, cellCache: CellCache, fi
     c.subscribeChanged({
       onCellChanged: (cellData) => {
         if (cellData.some) {
-          setData(cellData.val);
+          const value = cellData.val;
+          setData(value);
+
+          // update redux store for database field if there are new select options
+          if (
+            value instanceof SelectOptionCellDataPB &&
+            (databaseStore.fields[cellIdentifier.fieldId].fieldOptions as ISelectOptionType).selectOptions.length !==
+              value.options.length
+          ) {
+            const field = { ...databaseStore.fields[cellIdentifier.fieldId] };
+            const selectOptions = value.options.map((option) => ({
+              selectOptionId: option.id,
+              title: option.name,
+              color: option.color,
+            }));
+
+            dispatch(
+              databaseActions.updateField({
+                field: {
+                  ...field,
+                  fieldOptions: {
+                    ...field.fieldOptions,
+                    selectOptions: selectOptions,
+                  },
+                },
+              })
+            );
+          }
         }
       },
     });

+ 7 - 3
frontend/appflowy_tauri/src/appflowy_app/components/_shared/database-hooks/useDatabase.ts

@@ -45,12 +45,12 @@ export const useDatabase = (viewId: string, type?: ViewLayoutPB) => {
   };
 
   useEffect(() => {
-    if (!controller) return;
-
     void (async () => {
+      if (!controller) return;
       controller.subscribe({
         onRowsChanged: (rowInfos) => {
-          setRows(rowInfos);
+          // TODO: this is a hack to make sure that the row cache is updated
+          setRows([...rowInfos]);
         },
         onFieldsChanged: (fieldInfos) => {
           void loadFields(fieldInfos);
@@ -72,6 +72,10 @@ export const useDatabase = (viewId: string, type?: ViewLayoutPB) => {
         setGroups(controller.groups.value);
       }
     })();
+
+    return () => {
+      void controller?.dispose();
+    };
   }, [controller]);
 
   const onNewRowClick = async (index: number) => {

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

@@ -4,8 +4,9 @@ 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';
+import { FieldType } from '@/services/backend';
+import { None } from 'ts-results';
 
 export const useRow = (viewId: string, databaseController: DatabaseController, rowInfo: RowInfo) => {
   const [cells, setCells] = useState<{ fieldId: string; cellIdentifier: CellIdentifier }[]>([]);
@@ -42,10 +43,13 @@ export const useRow = (viewId: string, databaseController: DatabaseController, r
     })();
   }, [rowController, databaseStore.columns]);
 
-  const onNewColumnClick = async () => {
+  const onNewColumnClick = async (initialFieldType: FieldType = FieldType.RichText, name?: string) => {
     if (!databaseController) return;
-    const controller = new TypeOptionController(viewId, None);
+    const controller = new TypeOptionController(viewId, None, initialFieldType);
     await controller.initialize();
+    if (name) {
+      await controller.setFieldName(name);
+    }
   };
 
   return {

+ 1 - 2
frontend/appflowy_tauri/src/appflowy_app/components/board/Board.tsx

@@ -9,7 +9,7 @@ import { EditRow } from '$app/components/_shared/EditRow/EditRow';
 import { BoardToolbar } from '$app/components/board/BoardToolbar';
 
 export const Board = ({ viewId, title }: { viewId: string; title: string }) => {
-  const { controller, rows, groups, groupByFieldId, onNewRowClick, onDragEnd } = useDatabase(viewId, ViewLayoutPB.Board);
+  const { controller, groups, groupByFieldId, onNewRowClick, onDragEnd } = useDatabase(viewId, ViewLayoutPB.Board);
   const [showBoardRow, setShowBoardRow] = useState(false);
   const [boardRowInfo, setBoardRowInfo] = useState<RowInfo>();
 
@@ -38,7 +38,6 @@ export const Board = ({ viewId, title }: { viewId: string; title: string }) => {
                   viewId={viewId}
                   controller={controller}
                   group={group}
-                  allRows={rows}
                   groupByFieldId={groupByFieldId}
                   onNewRowClick={() => onNewRowClick(index)}
                   onOpenRow={onOpenRow}

+ 5 - 1
frontend/appflowy_tauri/src/appflowy_app/components/board/BoardCard.tsx

@@ -9,6 +9,7 @@ import { PopupWindow } from '$app/components/_shared/PopupWindow';
 import { TrashSvg } from '$app/components/_shared/svg/TrashSvg';
 import { RowBackendService } from '$app/stores/effects/database/row/row_bd_svc';
 import { useTranslation } from 'react-i18next';
+import { useAppSelector } from '$app/stores/store';
 
 export const BoardCard = ({
   index,
@@ -25,6 +26,7 @@ export const BoardCard = ({
   groupByFieldId: string;
   onOpenRow: (rowId: RowInfo) => void;
 }) => {
+  const databaseStore = useAppSelector((state) => state.database);
   const { t } = useTranslation();
 
   const { cells } = useRow(viewId, controller, rowInfo);
@@ -70,7 +72,9 @@ export const BoardCard = ({
             </button>
             <div className={'flex flex-col gap-3'}>
               {cells
-                .filter((cell) => cell.fieldId !== groupByFieldId)
+                .filter(
+                  (cell) => cell.fieldId !== groupByFieldId && databaseStore.fields[cell.cellIdentifier.fieldId].visible
+                )
                 .map((cell, cellIndex) => (
                   <BoardCell
                     key={cellIndex}

+ 20 - 7
frontend/appflowy_tauri/src/appflowy_app/components/board/BoardGroup.tsx

@@ -6,11 +6,11 @@ import { DatabaseController } from '$app/stores/effects/database/database_contro
 import { Droppable } from 'react-beautiful-dnd';
 import { DatabaseGroupController } from '$app/stores/effects/database/group/group_controller';
 import { useTranslation } from 'react-i18next';
+import { useEffect, useState } from 'react';
 
 export const BoardGroup = ({
   viewId,
   controller,
-  allRows,
   groupByFieldId,
   onNewRowClick,
   onOpenRow,
@@ -18,7 +18,6 @@ export const BoardGroup = ({
 }: {
   viewId: string;
   controller: DatabaseController;
-  allRows: readonly RowInfo[];
   groupByFieldId: string;
   onNewRowClick: () => void;
   onOpenRow: (rowId: RowInfo) => void;
@@ -26,6 +25,23 @@ export const BoardGroup = ({
 }) => {
   const { t } = useTranslation();
 
+  const [rows, setRows] = useState<RowInfo[]>([]);
+  useEffect(() => {
+    const reloadRows = () => {
+      setRows(group.rows.map((rowPB) => new RowInfo(viewId, controller.fieldController.fieldInfos, rowPB)));
+    };
+    reloadRows();
+    group.subscribe({
+      onRemoveRow: reloadRows,
+      onInsertRow: reloadRows,
+      onUpdateRow: reloadRows,
+      onCreateRow: reloadRows,
+    });
+    return () => {
+      group.unsubscribe();
+    };
+  }, [controller, group, viewId]);
+
   return (
     <div className={'flex h-full w-[250px] flex-col rounded-lg bg-surface-1'}>
       <div className={'flex items-center justify-between p-4'}>
@@ -49,9 +65,8 @@ export const BoardGroup = ({
             {...provided.droppableProps}
             ref={provided.innerRef}
           >
-            {group.rows.map((row_pb, index) => {
-              const row = allRows.find((r) => r.row.id === row_pb.id);
-              return row ? (
+            {rows.map((row, index) => {
+              return (
                 <BoardCard
                   viewId={viewId}
                   controller={controller}
@@ -61,8 +76,6 @@ export const BoardGroup = ({
                   groupByFieldId={groupByFieldId}
                   onOpenRow={onOpenRow}
                 ></BoardCard>
-              ) : (
-                <span key={index}></span>
               );
             })}
           </div>

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

@@ -32,21 +32,20 @@ export const useFolderEvents = (folder: IFolder, pages: IPage[]) => {
 
   // Backend services
   const appBackendService = new AppBackendService(folder.id);
-  const workspaceBackendService = new WorkspaceBackendService(workspace.id ?? '');
 
   useEffect(() => {
     void appObserver.subscribe({
-      onAppChanged: (change) => {
-        if (change.ok) {
-          const views = change.val;
-          const updatedPages: IPage[] = views.items.map((view) => ({
-            id: view.id,
-            folderId: view.parent_view_id,
-            pageType: view.layout,
-            title: view.name,
-          }));
-          appDispatch(pagesActions.didReceivePages(updatedPages));
-        }
+      onViewsChanged: async () => {
+        const result = await appBackendService.getAllViews();
+        if (!result.ok) return;
+        const views = result.val;
+        const updatedPages: IPage[] = views.map((view) => ({
+          id: view.id,
+          folderId: view.parent_view_id,
+          pageType: view.layout,
+          title: view.name,
+        }));
+        appDispatch(pagesActions.didReceivePages({ pages: updatedPages, folderId: folder.id }));
       },
     });
     return () => {
@@ -100,6 +99,7 @@ export const useFolderEvents = (folder: IFolder, pages: IPage[]) => {
 
   const duplicateFolder = async () => {
     closePopup();
+    const workspaceBackendService = new WorkspaceBackendService(workspace.id ?? '');
     const newApp = await workspaceBackendService.createApp({
       name: folder.title,
     });

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

@@ -1,6 +1,6 @@
-import { useAppSelector } from '../../../stores/store';
+import { useAppSelector } from '$app/stores/store';
 import { useNavigate } from 'react-router-dom';
-import { IPage } from '../../../stores/reducers/pages/slice';
+import { IPage } from '$app_reducers/pages/slice';
 import { ViewLayoutPB } from '@/services/backend';
 import { useState } from 'react';
 
@@ -21,11 +21,10 @@ export const useNavigationPanelHooks = function () {
   };
 
   const onPageClick = (page: IPage) => {
-    let pageTypeRoute = (() => {
+    const pageTypeRoute = (() => {
       switch (page.pageType) {
         case ViewLayoutPB.Document:
           return 'document';
-          break;
         case ViewLayoutPB.Grid:
           return 'grid';
         case ViewLayoutPB.Board:

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

@@ -4,11 +4,11 @@ import { FolderItem } from './FolderItem';
 import { TrashButton } from './TrashButton';
 import { NewFolderButton } from './NewFolderButton';
 import { NavigationResizer } from './NavigationResizer';
-import { IFolder } from '../../../stores/reducers/folders/slice';
-import { IPage } from '../../../stores/reducers/pages/slice';
+import { IFolder } from '$app_reducers/folders/slice';
+import { IPage } from '$app_reducers/pages/slice';
 import { useLocation, useNavigate } from 'react-router-dom';
 import React, { useEffect, useRef, useState } from 'react';
-import { useAppSelector } from '../../../stores/store';
+import { useAppSelector } from '$app/stores/store';
 import {
   ANIMATION_DURATION,
   FOLDER_MARGIN,
@@ -59,7 +59,7 @@ export const NavigationPanel = ({
       let height = 0;
       for (let i = 0; i < folderIndex; i++) {
         height += INITIAL_FOLDER_HEIGHT + FOLDER_MARGIN;
-        if (foldersStore[i].showPages === true) {
+        if (foldersStore[i].showPages) {
           height += pagesStore.filter((p) => p.folderId === foldersStore[i].id).length * PAGE_ITEM_HEIGHT;
         }
       }

+ 2 - 2
frontend/appflowy_tauri/src/appflowy_app/components/layout/NavigationPanel/NavigationResizer.tsx

@@ -1,7 +1,7 @@
 import { useResizer } from '../../_shared/useResizer';
-import { useAppDispatch, useAppSelector } from '../../../stores/store';
+import { useAppDispatch, useAppSelector } from '$app/stores/store';
 import { useEffect } from 'react';
-import { navigationWidthActions } from '../../../stores/reducers/navigation-width/slice';
+import { navigationWidthActions } from '$app_reducers/navigation-width/slice';
 
 export const NavigationResizer = ({ minWidth }: { minWidth: number }) => {
   const width = useAppSelector((state) => state.navigationWidth);

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

@@ -1,6 +1,6 @@
-import { useAppDispatch, useAppSelector } from '../../../stores/store';
-import { foldersActions } from '../../../stores/reducers/folders/slice';
-import { WorkspaceBackendService } from '../../../stores/effects/folder/workspace/workspace_bd_svc';
+import { useAppDispatch, useAppSelector } from '$app/stores/store';
+import { foldersActions } from '$app_reducers/folders/slice';
+import { WorkspaceBackendService } from '$app/stores/effects/folder/workspace/workspace_bd_svc';
 
 export const useNewFolder = () => {
   const appDispatch = useAppDispatch();

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

@@ -1,7 +1,6 @@
 import { IPage, pagesActions } from '$app_reducers/pages/slice';
 import { useAppDispatch } from '$app/stores/store';
 import { useEffect, useState } from 'react';
-import { nanoid } from 'nanoid';
 import { ViewBackendService } from '$app/stores/effects/folder/view/view_bd_svc';
 import { useLocation } from 'react-router-dom';
 import { ViewPB } from '@/services/backend';
@@ -43,10 +42,7 @@ export const usePageEvents = (page: IPage) => {
 
   const duplicatePage = async () => {
     closePopup();
-    await viewBackendService.duplicate(ViewPB.fromObject({}));
-    appDispatch(
-      pagesActions.addPage({ id: nanoid(8), pageType: page.pageType, title: page.title, folderId: page.folderId })
-    );
+    await viewBackendService.duplicate(ViewPB.fromObject(page));
   };
 
   const closePopup = () => {

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

@@ -3,7 +3,7 @@ import { BoardSvg } from '../../_shared/svg/BoardSvg';
 import { GridSvg } from '../../_shared/svg/GridSvg';
 import { Details2Svg } from '../../_shared/svg/Details2Svg';
 import { NavItemOptionsPopup } from './NavItemOptionsPopup';
-import { IPage } from '../../../stores/reducers/pages/slice';
+import { IPage } from '$app_reducers/pages/slice';
 import { Button } from '../../_shared/Button';
 import { usePageEvents } from './PageItem.hooks';
 import { RenamePopup } from './RenamePopup';

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

@@ -3,7 +3,7 @@ import { NavigationPanel } from './NavigationPanel/NavigationPanel';
 import { MainPanel } from './MainPanel';
 import { useNavigationPanelHooks } from './NavigationPanel/NavigationPanel.hooks';
 import { useWorkspace } from './Workspace.hooks';
-import { useAppSelector } from '../../stores/store';
+import { useAppSelector } from '$app/stores/store';
 
 export const Screen = ({ children }: { children: ReactNode }) => {
   const currentUser = useAppSelector((state) => state.currentUser);

+ 17 - 9
frontend/appflowy_tauri/src/appflowy_app/components/layout/Workspace.hooks.ts

@@ -1,8 +1,10 @@
-import { foldersActions } from '../../stores/reducers/folders/slice';
-import { useAppDispatch, useAppSelector } from '../../stores/store';
-import { pagesActions } from '../../stores/reducers/pages/slice';
-import { workspaceActions } from '../../stores/reducers/workspace/slice';
-import { UserBackendService } from '../../stores/effects/user/user_bd_svc';
+import { foldersActions } from '$app_reducers/folders/slice';
+import { useAppDispatch, useAppSelector } from '$app/stores/store';
+import { pagesActions } from '$app_reducers/pages/slice';
+import { workspaceActions } from '$app_reducers/workspace/slice';
+import { UserBackendService } from '$app/stores/effects/user/user_bd_svc';
+import { AppBackendService } from '$app/stores/effects/folder/app/app_bd_svc';
+import { Log } from '$app/utils/log';
 
 export const useWorkspace = () => {
   const currentUser = useAppSelector((state) => state.currentUser);
@@ -22,10 +24,16 @@ export const useWorkspace = () => {
       const apps = workspace.views;
       for (const app of apps) {
         appDispatch(foldersActions.addFolder({ id: app.id, title: app.name }));
-
-        const views = app.child_views;
-        for (const view of views) {
-          appDispatch(pagesActions.addPage({ folderId: app.id, id: view.id, pageType: view.layout, title: view.name }));
+        const service = new AppBackendService(app.id);
+        const result = await service.getAllViews();
+        if (result.ok) {
+          for (const view of result.val) {
+            appDispatch(
+              pagesActions.addPage({ folderId: app.id, id: view.id, pageType: view.layout, title: view.name })
+            );
+          }
+        } else {
+          Log.error('Failed to get views, folderId: ' + app.id);
         }
       }
     } catch (e1) {

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

@@ -1,4 +1,4 @@
-import { useAppSelector } from '../../stores/store';
+import { useAppSelector } from '$app/stores/store';
 
 export const WorkspaceUser = () => {
   const currentUser = useAppSelector((state) => state.currentUser);

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

@@ -73,6 +73,22 @@ export class TypeOptionController {
     }
   };
 
+  hideField = async () => {
+    if (this.fieldBackendSvc) {
+      void this.fieldBackendSvc.updateField({ visibility: false });
+    } else {
+      throw Error('Unexpected empty field backend service');
+    }
+  };
+
+  showField = async () => {
+    if (this.fieldBackendSvc) {
+      void this.fieldBackendSvc.updateField({ visibility: true });
+    } else {
+      throw Error('Unexpected empty field backend service');
+    }
+  };
+
   saveTypeOption = async (data: Uint8Array) => {
     if (this.typeOptionData.some) {
       this.typeOptionData.val.type_option_data = data;

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

@@ -1,40 +1,34 @@
-import { Ok, Result } from "ts-results";
-import { FlowyError, FolderNotification, RepeatedViewPB } from "@/services/backend";
-import { ChangeNotifier } from "$app/utils/change_notifier";
-import { FolderNotificationObserver } from "../notifications/observer";
-
-export type AppUpdateNotifyCallback = (value: Result<RepeatedViewPB, FlowyError>) => void;
+import { FolderNotification } from '@/services/backend';
+import { ChangeNotifier } from '$app/utils/change_notifier';
+import { FolderNotificationObserver } from '../notifications/observer';
 
 export class AppObserver {
-  _appNotifier = new ChangeNotifier<Result<RepeatedViewPB, FlowyError>>();
+  _viewsNotifier = new ChangeNotifier<void>();
   _listener?: FolderNotificationObserver;
 
-  constructor(public readonly appId: string) {
-  }
+  constructor(public readonly appId: string) {}
 
-  subscribe = async (callbacks: { onAppChanged: AppUpdateNotifyCallback }) => {
-    this._appNotifier?.observer?.subscribe(callbacks.onAppChanged);
+  subscribe = async (callbacks: { onViewsChanged: () => void }) => {
+    this._viewsNotifier?.observer?.subscribe(callbacks.onViewsChanged);
     this._listener = new FolderNotificationObserver({
       viewId: this.appId,
       parserHandler: (notification, result) => {
         switch (notification) {
-          case FolderNotification.DidUpdateWorkspaceViews:
+          case FolderNotification.DidUpdateChildViews:
             if (result.ok) {
-              this._appNotifier?.notify(Ok(RepeatedViewPB.deserializeBinary(result.val)));
-            } else {
-              this._appNotifier?.notify(result);
+              this._viewsNotifier?.notify();
             }
             break;
           default:
             break;
         }
-      }
+      },
     });
     await this._listener.start();
   };
 
   unsubscribe = async () => {
-    this._appNotifier.unsubscribe();
+    this._viewsNotifier.unsubscribe();
     await this._listener?.stop();
   };
 }

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

@@ -25,6 +25,7 @@ export interface INumberType {
 export interface IDatabaseField {
   fieldId: string;
   title: string;
+  visible: boolean;
   fieldType: FieldType;
   fieldOptions?: ISelectOptionType | IDateType | INumberType;
 }
@@ -112,11 +113,10 @@ export const databaseSlice = createSlice({
       });
     },*/
 
-    /*updateField: (state, action: PayloadAction<{ field: IDatabaseField }>) => {
+    updateField: (state, action: PayloadAction<{ field: IDatabaseField }>) => {
       const { field } = action.payload;
-
       state.fields[field.fieldId] = field;
-    },*/
+    },
 
     /*addFieldSelectOption: (state, action: PayloadAction<{ fieldId: string; option: ISelectOption }>) => {
       const { fieldId, option } = action.payload;

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

@@ -14,8 +14,8 @@ export const pagesSlice = createSlice({
   name: 'pages',
   initialState: initialState,
   reducers: {
-    didReceivePages(state, action: PayloadAction<IPage[]>) {
-      return action.payload;
+    didReceivePages(state, action: PayloadAction<{ pages: IPage[]; folderId: string }>) {
+      return state.filter((page) => page.folderId !== action.payload.folderId).concat(action.payload.pages);
     },
     addPage(state, action: PayloadAction<IPage>) {
       state.push(action.payload);