Browse Source

Merge remote-tracking branch 'origin/feat/appflowy_tauri_3' into feat/appflowy_tauri_3

ascarbek 2 năm trước cách đây
mục cha
commit
08bb4e4116
27 tập tin đã thay đổi với 392 bổ sung225 xóa
  1. 1 1
      frontend/appflowy_tauri/src-tauri/build.rs
  2. 16 15
      frontend/appflowy_tauri/src-tauri/src/init.rs
  3. 0 1
      frontend/appflowy_tauri/src-tauri/src/request.rs
  4. 2 3
      frontend/appflowy_tauri/src-tauri/tauri.conf.json
  5. 11 3
      frontend/appflowy_tauri/src/appflowy_app/components/TestApiButton/DatabaseTestHelper.ts
  6. 8 2
      frontend/appflowy_tauri/src/appflowy_app/components/TestApiButton/TestAPI.tsx
  7. 118 37
      frontend/appflowy_tauri/src/appflowy_app/components/TestApiButton/TestGrid.tsx
  8. 15 10
      frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/cell/cell_cache.ts
  9. 21 17
      frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/cell/cell_controller.ts
  10. 2 2
      frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/cell/cell_observer.ts
  11. 24 10
      frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/cell/select_option_bd_svc.ts
  12. 1 2
      frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/database_bd_svc.ts
  13. 7 5
      frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/database_controller.ts
  14. 17 19
      frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/field/field_controller.ts
  15. 13 13
      frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/field/field_observer.ts
  16. 19 7
      frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/field/type_option/type_option_context.ts
  17. 3 3
      frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/field/type_option/type_option_controller.ts
  18. 1 2
      frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/notifications/observer.ts
  19. 32 0
      frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/row/row_bd_svc.ts
  20. 21 22
      frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/view/database_view_cache.ts
  21. 22 21
      frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/view/view_row_observer.ts
  22. 3 4
      frontend/appflowy_tauri/src/appflowy_app/stores/effects/folder/app/app_observer.ts
  23. 2 2
      frontend/appflowy_tauri/src/appflowy_app/stores/effects/folder/view/view_observer.ts
  24. 18 15
      frontend/appflowy_tauri/src/appflowy_app/stores/effects/folder/workspace/workspace_observer.ts
  25. 12 6
      frontend/appflowy_tauri/src/services/backend/notifications/observer.ts
  26. 2 2
      frontend/rust-lib/flowy-codegen/src/ts_event/event_template.tera
  27. 1 1
      frontend/rust-lib/flowy-database/src/entities/row_entities.rs

+ 1 - 1
frontend/appflowy_tauri/src-tauri/build.rs

@@ -1,3 +1,3 @@
 fn main() {
-  tauri_build::build()
+    tauri_build::build()
 }

+ 16 - 15
frontend/appflowy_tauri/src-tauri/src/init.rs

@@ -1,21 +1,22 @@
 use flowy_core::{get_client_server_configuration, AppFlowyCore, AppFlowyCoreConfig};
 
 pub fn init_flowy_core() -> AppFlowyCore {
-  let config_json = include_str!("../tauri.conf.json");
-  let config: tauri_utils::config::Config = serde_json::from_str(config_json).unwrap();
+    let config_json = include_str!("../tauri.conf.json");
+    let config: tauri_utils::config::Config = serde_json::from_str(config_json).unwrap();
 
-  let mut data_path = tauri::api::path::app_local_data_dir(&config).unwrap();
-  if cfg!(debug_assertions) {
-    data_path.push("dev");
-  }
-  data_path.push("data");
+    let mut data_path = tauri::api::path::app_local_data_dir(&config).unwrap();
+    if cfg!(debug_assertions) {
+        data_path.push("dev");
+    }
+    data_path.push("data");
 
-  let server_config = get_client_server_configuration().unwrap();
-  let config = AppFlowyCoreConfig::new(
-    data_path.to_str().unwrap(),
-    "AppFlowy".to_string(),
-    server_config,
-  )
-  .log_filter("trace", vec!["appflowy_tauri".to_string()]);
-  AppFlowyCore::new(config)
+    std::env::set_var("RUST_LOG", "debug");
+    let server_config = get_client_server_configuration().unwrap();
+    let config = AppFlowyCoreConfig::new(
+        data_path.to_str().unwrap(),
+        "AppFlowy".to_string(),
+        server_config,
+    )
+        .log_filter("trace", vec!["appflowy_tauri".to_string()]);
+    AppFlowyCore::new(config)
 }

+ 0 - 1
frontend/appflowy_tauri/src-tauri/src/request.rs

@@ -32,7 +32,6 @@ impl std::convert::From<AFPluginEventResponse> for AFTauriResponse {
 }
 
 // Learn more about Tauri commands at https://tauri.app/v1/guides/features/command
-#[tracing::instrument(level = "trace", skip(app_handler))]
 #[tauri::command]
 pub async fn invoke_request(
   request: AFTauriRequest,

+ 2 - 3
frontend/appflowy_tauri/src-tauri/tauri.conf.json

@@ -60,11 +60,10 @@
     "windows": [
       {
         "fullscreen": false,
-        "height": 1000,
+        "height": 1200,
         "resizable": true,
         "title": "AppFlowy",
-        "width": 1200,
-        "transparent": true
+        "width": 1200
       }
     ]
   }

+ 11 - 3
frontend/appflowy_tauri/src/appflowy_app/components/TestApiButton/DatabaseTestHelper.ts

@@ -35,7 +35,7 @@ export async function assertTextCell(rowInfo: RowInfo, databaseController: Datab
     onCellChanged: (value) => {
       const cellContent = value.unwrap();
       if (cellContent !== expectedContent) {
-        throw Error();
+        throw Error('Text cell content is not match');
       }
     },
   });
@@ -111,7 +111,7 @@ export async function assertFieldName(viewId: string, fieldId: string, fieldType
   const svc = new TypeOptionBackendService(viewId);
   const typeOptionPB = await svc.getTypeOption(fieldId, fieldType).then((result) => result.unwrap());
   if (typeOptionPB.field.name !== expected) {
-    throw Error();
+    throw Error('Expect field name:' + expected + 'but receive:' + typeOptionPB.field.name);
   }
 }
 
@@ -119,6 +119,14 @@ export async function assertNumberOfFields(viewId: string, expected: number) {
   const svc = new DatabaseBackendService(viewId);
   const databasePB = await svc.openDatabase().then((result) => result.unwrap());
   if (databasePB.fields.length !== expected) {
-    throw Error();
+    throw Error('Expect number of fields:' + expected + 'but receive:' + databasePB.fields.length);
+  }
+}
+
+export async function assertNumberOfRows(viewId: string, expected: number) {
+  const svc = new DatabaseBackendService(viewId);
+  const databasePB = await svc.openDatabase().then((result) => result.unwrap());
+  if (databasePB.rows.length !== expected) {
+    throw Error('Expect number of rows:' + expected + 'but receive:' + databasePB.rows.length);
   }
 }

+ 8 - 2
frontend/appflowy_tauri/src/appflowy_app/components/TestApiButton/TestAPI.tsx

@@ -3,10 +3,13 @@ import TestApiButton from './TestApiButton';
 import {
   TestCreateGrid,
   TestCreateNewField,
-  TestCreateSelectOption,
+  TestCreateRow,
+  TestCreateSelectOptionInCell,
   TestDeleteField,
+  TestDeleteRow,
   TestEditCell,
   TestEditField,
+  TestGetSingleSelectFieldData,
 } from './TestGrid';
 
 export const TestAPI = () => {
@@ -15,8 +18,11 @@ export const TestAPI = () => {
       <ul className='m-6, space-y-2'>
         <TestApiButton></TestApiButton>
         <TestCreateGrid></TestCreateGrid>
+        <TestCreateRow></TestCreateRow>
+        <TestDeleteRow></TestDeleteRow>
         <TestEditCell></TestEditCell>
-        <TestCreateSelectOption></TestCreateSelectOption>
+        <TestCreateSelectOptionInCell></TestCreateSelectOptionInCell>
+        <TestGetSingleSelectFieldData></TestGetSingleSelectFieldData>
         <TestEditField></TestEditField>
         <TestCreateNewField></TestCreateNewField>
         <TestDeleteField></TestDeleteField>

+ 118 - 37
frontend/appflowy_tauri/src/appflowy_app/components/TestApiButton/TestGrid.tsx

@@ -1,20 +1,29 @@
 import React from 'react';
-import { SelectOptionCellDataPB, ViewLayoutTypePB } from '../../../services/backend';
+import {
+  FieldType,
+  SelectOptionCellDataPB,
+  SingleSelectTypeOptionPB,
+  ViewLayoutTypePB,
+} from '../../../services/backend';
 import { Log } from '../../utils/log';
 import {
   assertFieldName,
   assertNumberOfFields,
+  assertNumberOfRows,
   assertTextCell,
   createTestDatabaseView,
   editTextCell,
   makeSingleSelectCellController,
   openTestDatabase,
 } from './DatabaseTestHelper';
-import assert from 'assert';
-import { SelectOptionBackendService } from '../../stores/effects/database/cell/select_option_bd_svc';
+import {
+  SelectOptionBackendService,
+  SelectOptionCellBackendService,
+} from '../../stores/effects/database/cell/select_option_bd_svc';
 import { TypeOptionController } from '../../stores/effects/database/field/type_option/type_option_controller';
 import { None, Some } from 'ts-results';
-import { TypeOptionBackendService } from '../../stores/effects/database/field/type_option/type_option_bd_svc';
+import { RowBackendService } from '../../stores/effects/database/row/row_bd_svc';
+import { makeSingleSelectTypeOptionContext } from '../../stores/effects/database/field/type_option/type_option_context';
 
 export const TestCreateGrid = () => {
   async function createBuildInGrid() {
@@ -26,16 +35,17 @@ export const TestCreateGrid = () => {
       },
       onRowsChanged: async (rows) => {
         if (rows.length !== 3) {
-          throw Error();
+          throw Error('Expected number of rows is 3, but receive ' + rows.length + view.id);
         }
       },
       onFieldsChanged: (fields) => {
         if (fields.length !== 3) {
-          throw Error();
+          throw Error('Expected number of fields is 3, but receive ' + fields.length);
         }
       },
     });
     await databaseController.open().then((result) => result.unwrap());
+    await databaseController.dispose();
   }
 
   return TestButton('Test create build-in grid', createBuildInGrid);
@@ -45,48 +55,116 @@ export const TestEditCell = () => {
   async function testGridRow() {
     const view = await createTestDatabaseView(ViewLayoutTypePB.Grid);
     const databaseController = await openTestDatabase(view.id);
-    databaseController.subscribe({
-      onRowsChanged: async (rows) => {
-        for (const [index, row] of rows.entries()) {
-          const cellContent = index.toString();
-          await editTextCell(row, databaseController, cellContent);
-          await assertTextCell(row, databaseController, cellContent);
-        }
-      },
-    });
     await databaseController.open().then((result) => result.unwrap());
+
+    for (const [index, row] of databaseController.databaseViewCache.rowInfos.entries()) {
+      const cellContent = index.toString();
+      await editTextCell(row, databaseController, cellContent);
+      await assertTextCell(row, databaseController, cellContent);
+    }
   }
 
   return TestButton('Test editing cell', testGridRow);
 };
 
-export const TestCreateSelectOption = () => {
-  async function testCreateOption() {
+export const TestCreateRow = () => {
+  async function testCreateRow() {
     const view = await createTestDatabaseView(ViewLayoutTypePB.Grid);
     const databaseController = await openTestDatabase(view.id);
-    databaseController.subscribe({
-      onRowsChanged: async (rows) => {
-        for (const [index, row] of rows.entries()) {
-          if (index === 0) {
-            const cellController = await makeSingleSelectCellController(row, databaseController).then((result) =>
-              result.unwrap()
-            );
-            cellController.subscribeChanged({
-              onCellChanged: (value) => {
-                const option: SelectOptionCellDataPB = value.unwrap();
-                console.log(option);
-              },
-            });
-            const backendSvc = new SelectOptionBackendService(cellController.cellIdentifier);
-            await backendSvc.createOption({ name: 'option' + index });
-          }
-        }
-      },
-    });
     await databaseController.open().then((result) => result.unwrap());
+    await assertNumberOfRows(view.id, 3);
+
+    // Create a row from a DatabaseController or create using the RowBackendService
+    await databaseController.createRow();
+    await assertNumberOfRows(view.id, 4);
+    await databaseController.dispose();
+  }
+
+  return TestButton('Test create row', testCreateRow);
+};
+export const TestDeleteRow = () => {
+  async function testDeleteRow() {
+    const view = await createTestDatabaseView(ViewLayoutTypePB.Grid);
+    const databaseController = await openTestDatabase(view.id);
+    await databaseController.open().then((result) => result.unwrap());
+
+    const rows = databaseController.databaseViewCache.rowInfos;
+    const svc = new RowBackendService(view.id);
+    await svc.deleteRow(rows[0].row.id);
+    await assertNumberOfRows(view.id, 2);
+
+    // Wait the databaseViewCache get the change notification and
+    // update the rows.
+    await new Promise((resolve) => setTimeout(resolve, 200));
+    if (databaseController.databaseViewCache.rowInfos.length !== 2) {
+      throw Error('The number of rows is not match');
+    }
+    await databaseController.dispose();
+  }
+
+  return TestButton('Test delete row', testDeleteRow);
+};
+export const TestCreateSelectOptionInCell = () => {
+  async function testCreateOptionInCell() {
+    const view = await createTestDatabaseView(ViewLayoutTypePB.Grid);
+    const databaseController = await openTestDatabase(view.id);
+    await databaseController.open().then((result) => result.unwrap());
+    for (const [index, row] of databaseController.databaseViewCache.rowInfos.entries()) {
+      if (index === 0) {
+        const cellController = await makeSingleSelectCellController(row, databaseController).then((result) =>
+          result.unwrap()
+        );
+        cellController.subscribeChanged({
+          onCellChanged: (value) => {
+            const option: SelectOptionCellDataPB = value.unwrap();
+            console.log(option);
+          },
+        });
+        const backendSvc = new SelectOptionCellBackendService(cellController.cellIdentifier);
+        await backendSvc.createOption({ name: 'option' + index });
+        await cellController.dispose();
+      }
+    }
+    await databaseController.dispose();
+  }
+
+  return TestButton('Test create a select option in cell', testCreateOptionInCell);
+};
+
+export const TestGetSingleSelectFieldData = () => {
+  async function testGetSingleSelectFieldData() {
+    const view = await createTestDatabaseView(ViewLayoutTypePB.Grid);
+    const databaseController = await openTestDatabase(view.id);
+    await databaseController.open().then((result) => result.unwrap());
+
+    // Find the single select column
+    const singleSelect = databaseController.fieldController.fieldInfos.find(
+      (fieldInfo) => fieldInfo.field.field_type === FieldType.SingleSelect
+    )!;
+    const typeOptionController = new TypeOptionController(view.id, Some(singleSelect));
+    const singleSelectTypeOptionContext = makeSingleSelectTypeOptionContext(typeOptionController);
+
+    // Create options
+    const singleSelectTypeOptionPB: SingleSelectTypeOptionPB = await singleSelectTypeOptionContext
+      .getTypeOption()
+      .then((result) => result.unwrap());
+    const backendSvc = new SelectOptionBackendService(view.id, singleSelect.field.id);
+    const option1 = await backendSvc.createOption({ name: 'Task 1' }).then((result) => result.unwrap());
+    singleSelectTypeOptionPB.options.splice(0, 0, option1);
+    const option2 = await backendSvc.createOption({ name: 'Task 2' }).then((result) => result.unwrap());
+    singleSelectTypeOptionPB.options.splice(0, 0, option2);
+    const option3 = await backendSvc.createOption({ name: 'Task 3' }).then((result) => result.unwrap());
+    singleSelectTypeOptionPB.options.splice(0, 0, option3);
+    await singleSelectTypeOptionContext.setTypeOption(singleSelectTypeOptionPB);
+
+    // Read options
+    const options = singleSelectTypeOptionPB.options;
+    console.log(options);
+
+    await databaseController.dispose();
   }
 
-  return TestButton('Test create a select option', testCreateOption);
+  return TestButton('Test get single-select column data', testGetSingleSelectFieldData);
 };
 
 export const TestEditField = () => {
@@ -104,6 +182,7 @@ export const TestEditField = () => {
     await controller.setFieldName(newName);
 
     await assertFieldName(view.id, firstFieldInfo.field.id, firstFieldInfo.field.field_type, newName);
+    await databaseController.dispose();
   }
 
   return TestButton('Test edit the column name', testEditField);
@@ -120,6 +199,7 @@ export const TestCreateNewField = () => {
     const controller = new TypeOptionController(view.id, None);
     await controller.initialize();
     await assertNumberOfFields(view.id, 4);
+    await databaseController.dispose();
   }
 
   return TestButton('Test create a new column', testCreateNewField);
@@ -140,6 +220,7 @@ export const TestDeleteField = () => {
     await assertNumberOfFields(view.id, 3);
     await controller.deleteField();
     await assertNumberOfFields(view.id, 2);
+    await databaseController.dispose();
   }
 
   return TestButton('Test delete a new column', testDeleteField);

+ 15 - 10
frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/cell/cell_cache.ts

@@ -5,32 +5,37 @@ export class CellCacheKey {
   constructor(public readonly fieldId: string, public readonly rowId: string) {}
 }
 
+type CellDataByRowId = Map<string, any>;
+
 export class CellCache {
-  _cellDataByFieldId = new Map<string, Map<string, any>>();
+  private cellDataByFieldId = new Map<string, CellDataByRowId>();
 
   constructor(public readonly databaseId: string) {}
 
   remove = (key: CellCacheKey) => {
-    const inner = this._cellDataByFieldId.get(key.fieldId);
-    if (inner !== undefined) {
-      inner.delete(key.rowId);
+    const cellDataByRowId = this.cellDataByFieldId.get(key.fieldId);
+    if (cellDataByRowId !== undefined) {
+      cellDataByRowId.delete(key.rowId);
     }
   };
 
   removeWithFieldId = (fieldId: string) => {
-    this._cellDataByFieldId.delete(fieldId);
+    this.cellDataByFieldId.delete(fieldId);
   };
 
   insert = (key: CellCacheKey, value: any) => {
-    let inner = this._cellDataByFieldId.get(key.fieldId);
-    if (inner === undefined) {
-      inner = this._cellDataByFieldId.set(key.fieldId, new Map());
+    const cellDataByRowId = this.cellDataByFieldId.get(key.fieldId);
+    if (cellDataByRowId === undefined) {
+      const map = new Map();
+      map.set(key.rowId, value);
+      this.cellDataByFieldId.set(key.fieldId, map);
+    } else {
+      cellDataByRowId.set(key.rowId, value);
     }
-    inner.set(key.rowId, value);
   };
 
   get<T>(key: CellCacheKey): Option<T> {
-    const inner = this._cellDataByFieldId.get(key.fieldId);
+    const inner = this.cellDataByFieldId.get(key.fieldId);
     if (inner === undefined) {
       return None;
     } else {

+ 21 - 17
frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/cell/cell_controller.ts

@@ -14,10 +14,10 @@ export abstract class CellFieldNotifier {
 }
 
 export class CellController<T, D> {
-  private _fieldBackendService: FieldBackendService;
-  private _cellDataNotifier: CellDataNotifier<Option<T>>;
-  private _cellObserver: CellObserver;
-  private _cacheKey: CellCacheKey;
+  private fieldBackendService: FieldBackendService;
+  private cellDataNotifier: CellDataNotifier<Option<T>>;
+  private cellObserver: CellObserver;
+  private readonly cacheKey: CellCacheKey;
 
   constructor(
     public readonly cellIdentifier: CellIdentifier,
@@ -26,17 +26,17 @@ export class CellController<T, D> {
     private readonly cellDataLoader: CellDataLoader<T>,
     private readonly cellDataPersistence: CellDataPersistence<D>
   ) {
-    this._fieldBackendService = new FieldBackendService(cellIdentifier.viewId, cellIdentifier.fieldId);
-    this._cacheKey = new CellCacheKey(cellIdentifier.rowId, cellIdentifier.fieldId);
-    this._cellDataNotifier = new CellDataNotifier(cellCache.get<T>(this._cacheKey));
-    this._cellObserver = new CellObserver(cellIdentifier.rowId, cellIdentifier.fieldId);
-    this._cellObserver.subscribe({
+    this.fieldBackendService = new FieldBackendService(cellIdentifier.viewId, cellIdentifier.fieldId);
+    this.cacheKey = new CellCacheKey(cellIdentifier.rowId, cellIdentifier.fieldId);
+    this.cellDataNotifier = new CellDataNotifier(cellCache.get<T>(this.cacheKey));
+    this.cellObserver = new CellObserver(cellIdentifier.rowId, cellIdentifier.fieldId);
+    void this.cellObserver.subscribe({
       /// 1.Listen on user edit event and load the new cell data if needed.
       /// For example:
       ///  user input: 12
       ///  cell display: $12
       onCellChanged: async () => {
-        this.cellCache.remove(this._cacheKey);
+        this.cellCache.remove(this.cacheKey);
         await this._loadCellData();
       },
     });
@@ -55,7 +55,7 @@ export class CellController<T, D> {
       }
     });
 
-    this._cellDataNotifier.observer.subscribe((cellData) => {
+    this.cellDataNotifier.observer.subscribe((cellData) => {
       if (cellData !== null) {
         callbacks.onCellChanged(cellData);
       }
@@ -63,7 +63,7 @@ export class CellController<T, D> {
   };
 
   getTypeOption = async <P extends TypeOptionParser<PD>, PD>(parser: P) => {
-    const result = await this._fieldBackendService.getTypeOptionData(this.cellIdentifier.fieldType);
+    const result = await this.fieldBackendService.getTypeOptionData(this.cellIdentifier.fieldType);
     if (result.ok) {
       return Ok(parser.fromBuffer(result.val.type_option_data));
     } else {
@@ -83,7 +83,7 @@ export class CellController<T, D> {
   /// data from the backend and then the [onCellChanged] will
   /// get called
   getCellData = (): Option<T> => {
-    const cellData = this.cellCache.get<T>(this._cacheKey);
+    const cellData = this.cellCache.get<T>(this.cacheKey);
     if (cellData.none) {
       void this._loadCellData();
     }
@@ -93,14 +93,18 @@ export class CellController<T, D> {
   private _loadCellData = () => {
     return this.cellDataLoader.loadData().then((result) => {
       if (result.ok && result.val !== undefined) {
-        this.cellCache.insert(this._cacheKey, result.val);
-        this._cellDataNotifier.cellData = Some(result.val);
+        this.cellCache.insert(this.cacheKey, result.val);
+        this.cellDataNotifier.cellData = Some(result.val);
       } else {
-        this.cellCache.remove(this._cacheKey);
-        this._cellDataNotifier.cellData = None;
+        this.cellCache.remove(this.cacheKey);
+        this.cellDataNotifier.cellData = None;
       }
     });
   };
+
+  dispose = async () => {
+    await this.cellObserver.unsubscribe();
+  };
 }
 
 export class CellFieldNotifierImpl extends CellFieldNotifier {

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

@@ -13,7 +13,7 @@ export class CellObserver {
 
   constructor(public readonly rowId: string, public readonly fieldId: string) {}
 
-  subscribe = (callbacks: { onCellChanged: CellChangedCallback }) => {
+  subscribe = async (callbacks: { onCellChanged: CellChangedCallback }) => {
     this._notifier = new ChangeNotifier();
     this._notifier?.observer.subscribe(callbacks.onCellChanged);
 
@@ -33,7 +33,7 @@ export class CellObserver {
         }
       },
     });
-    return undefined;
+    await this._listener.start();
   };
 
   unsubscribe = async () => {

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

@@ -14,6 +14,20 @@ import {
 } from '../../../../../services/backend/events/flowy-database';
 
 export class SelectOptionBackendService {
+  constructor(public readonly viewId: string, public readonly fieldId: string) {}
+
+  createOption = async (params: { name: string }) => {
+    const payload = CreateSelectOptionPayloadPB.fromObject({
+      option_name: params.name,
+      view_id: this.viewId,
+      field_id: this.fieldId,
+    });
+
+    return DatabaseEventCreateSelectOption(payload);
+  };
+}
+
+export class SelectOptionCellBackendService {
   constructor(public readonly cellIdentifier: CellIdentifier) {}
 
   createOption = async (params: { name: string; isSelect?: boolean }) => {
@@ -31,6 +45,16 @@ export class SelectOptionBackendService {
     }
   };
 
+  private _insertOption = (option: SelectOptionPB, isSelect: boolean) => {
+    const payload = SelectOptionChangesetPB.fromObject({ cell_identifier: this._cellIdentifier() });
+    if (isSelect) {
+      payload.insert_options.push(option);
+    } else {
+      payload.update_options.push(option);
+    }
+    return DatabaseEventUpdateSelectOption(payload);
+  };
+
   updateOption = (option: SelectOptionPB) => {
     const payload = SelectOptionChangesetPB.fromObject({ cell_identifier: this._cellIdentifier() });
     payload.update_options.push(option);
@@ -59,16 +83,6 @@ export class SelectOptionBackendService {
     return DatabaseEventUpdateSelectOptionCell(payload);
   };
 
-  private _insertOption = (option: SelectOptionPB, isSelect: boolean) => {
-    const payload = SelectOptionChangesetPB.fromObject({ cell_identifier: this._cellIdentifier() });
-    if (isSelect) {
-      payload.insert_options.push(option);
-    } else {
-      payload.update_options.push(option);
-    }
-    return DatabaseEventUpdateSelectOption(payload);
-  };
-
   private _cellIdentifier = () => {
     return CellIdPB.fromObject({
       view_id: this.cellIdentifier.viewId,

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

@@ -33,8 +33,7 @@ export class DatabaseBackendService {
   };
 
   createRow = async (rowId?: string) => {
-    const props = { database_id: this.viewId, start_row_id: rowId ?? undefined };
-    const payload = CreateRowPayloadPB.fromObject(props);
+    const payload = CreateRowPayloadPB.fromObject({ view_id: this.viewId, start_row_id: rowId ?? undefined });
     return DatabaseEventCreateRow(payload);
   };
 

+ 7 - 5
frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/database_controller.ts

@@ -12,13 +12,13 @@ export type SubscribeCallback = {
 };
 
 export class DatabaseController {
-  private _backendService: DatabaseBackendService;
+  private backendService: DatabaseBackendService;
   fieldController: FieldController;
   databaseViewCache: DatabaseViewCache;
   private _callback?: SubscribeCallback;
 
   constructor(public readonly viewId: string) {
-    this._backendService = new DatabaseBackendService(viewId);
+    this.backendService = new DatabaseBackendService(viewId);
     this.fieldController = new FieldController(viewId);
     this.databaseViewCache = new DatabaseViewCache(viewId, this.fieldController);
   }
@@ -32,11 +32,13 @@ export class DatabaseController {
   };
 
   open = async () => {
-    const result = await this._backendService.openDatabase();
+    const result = await this.backendService.openDatabase();
     if (result.ok) {
       const database: DatabasePB = result.val;
       this._callback?.onViewChanged?.(database);
       await this.fieldController.loadFields(database.fields);
+      await this.databaseViewCache.listenOnRowsChanged();
+      await this.fieldController.listenOnFieldChanges();
       this.databaseViewCache.initializeWithRows(database.rows);
       return Ok.EMPTY;
     } else {
@@ -45,11 +47,11 @@ export class DatabaseController {
   };
 
   createRow = async () => {
-    return this._backendService.createRow();
+    return this.backendService.createRow();
   };
 
   dispose = async () => {
-    await this._backendService.closeDatabase();
+    await this.backendService.closeDatabase();
     await this.fieldController.dispose();
     await this.databaseViewCache.dispose();
   };

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

@@ -5,47 +5,45 @@ import { FieldIdPB, FieldPB, IndexFieldPB } from '../../../../../services/backen
 import { ChangeNotifier } from '../../../../utils/change_notifier';
 
 export class FieldController {
-  private _fieldListener: DatabaseFieldChangesetObserver;
-  private _backendService: DatabaseBackendService;
-  private _fieldNotifier = new FieldNotifier([]);
+  private fieldListener: DatabaseFieldChangesetObserver;
+  private backendService: DatabaseBackendService;
+  private fieldNotifier = new FieldNotifier([]);
 
   constructor(public readonly viewId: string) {
-    this._backendService = new DatabaseBackendService(viewId);
-    this._fieldListener = new DatabaseFieldChangesetObserver(viewId);
-
-    this._listenOnFieldChanges();
+    this.backendService = new DatabaseBackendService(viewId);
+    this.fieldListener = new DatabaseFieldChangesetObserver(viewId);
   }
 
   dispose = async () => {
-    this._fieldNotifier.unsubscribe();
-    await this._fieldListener.unsubscribe();
+    this.fieldNotifier.unsubscribe();
+    await this.fieldListener.unsubscribe();
   };
 
   get fieldInfos(): readonly FieldInfo[] {
-    return this._fieldNotifier.fieldInfos;
+    return this.fieldNotifier.fieldInfos;
   }
 
   getField = (fieldId: string): FieldInfo | undefined => {
-    return this._fieldNotifier.fieldInfos.find((element) => element.field.id === fieldId);
+    return this.fieldNotifier.fieldInfos.find((element) => element.field.id === fieldId);
   };
 
   loadFields = async (fieldIds: FieldIdPB[]) => {
-    const result = await this._backendService.getFields(fieldIds);
+    const result = await this.backendService.getFields(fieldIds);
     if (result.ok) {
-      this._fieldNotifier.fieldInfos = result.val.map((field) => new FieldInfo(field));
+      this.fieldNotifier.fieldInfos = result.val.map((field) => new FieldInfo(field));
     } else {
       Log.error(result.val);
     }
   };
 
   subscribeOnFieldsChanged = (callback?: (fieldInfos: readonly FieldInfo[]) => void) => {
-    return this._fieldNotifier.observer.subscribe((fieldInfos) => {
+    return this.fieldNotifier.observer.subscribe((fieldInfos) => {
       callback?.(fieldInfos);
     });
   };
 
-  _listenOnFieldChanges = () => {
-    this._fieldListener.subscribe({
+  listenOnFieldChanges = async () => {
+    await this.fieldListener.subscribe({
       onFieldsChanged: (result) => {
         if (result.ok) {
           const changeset = result.val;
@@ -70,7 +68,7 @@ export class FieldController {
     };
     const newFieldInfos = [...this.fieldInfos];
     newFieldInfos.filter(predicate);
-    this._fieldNotifier.fieldInfos = newFieldInfos;
+    this.fieldNotifier.fieldInfos = newFieldInfos;
   };
 
   _insertFields = (insertedFields: IndexFieldPB[]) => {
@@ -86,7 +84,7 @@ export class FieldController {
         newFieldInfos.push(fieldInfo);
       }
     });
-    this._fieldNotifier.fieldInfos = newFieldInfos;
+    this.fieldNotifier.fieldInfos = newFieldInfos;
   };
 
   _updateFields = (updatedFields: FieldPB[]) => {
@@ -104,7 +102,7 @@ export class FieldController {
         }
       });
     });
-    this._fieldNotifier.fieldInfos = newFieldInfos;
+    this.fieldNotifier.fieldInfos = newFieldInfos;
   };
 }
 

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

@@ -7,24 +7,24 @@ type UpdateFieldNotifiedValue = Result<DatabaseFieldChangesetPB, FlowyError>;
 export type DatabaseNotificationCallback = (value: UpdateFieldNotifiedValue) => void;
 
 export class DatabaseFieldChangesetObserver {
-  private _notifier?: ChangeNotifier<UpdateFieldNotifiedValue>;
-  private _listener?: DatabaseNotificationObserver;
+  private notifier?: ChangeNotifier<UpdateFieldNotifiedValue>;
+  private listener?: DatabaseNotificationObserver;
 
   constructor(public readonly viewId: string) {}
 
-  subscribe = (callbacks: { onFieldsChanged: DatabaseNotificationCallback }) => {
-    this._notifier = new ChangeNotifier();
-    this._notifier?.observer.subscribe(callbacks.onFieldsChanged);
+  subscribe = async (callbacks: { onFieldsChanged: DatabaseNotificationCallback }) => {
+    this.notifier = new ChangeNotifier();
+    this.notifier?.observer.subscribe(callbacks.onFieldsChanged);
 
-    this._listener = new DatabaseNotificationObserver({
+    this.listener = new DatabaseNotificationObserver({
       viewId: this.viewId,
       parserHandler: (notification, result) => {
         switch (notification) {
           case DatabaseNotification.DidUpdateFields:
             if (result.ok) {
-              this._notifier?.notify(Ok(DatabaseFieldChangesetPB.deserializeBinary(result.val)));
+              this.notifier?.notify(Ok(DatabaseFieldChangesetPB.deserializeBinary(result.val)));
             } else {
-              this._notifier?.notify(result);
+              this.notifier?.notify(result);
             }
             return;
           default:
@@ -32,12 +32,12 @@ export class DatabaseFieldChangesetObserver {
         }
       },
     });
-    return undefined;
+    await this.listener.start();
   };
 
   unsubscribe = async () => {
-    this._notifier?.unsubscribe();
-    await this._listener?.stop();
+    this.notifier?.unsubscribe();
+    await this.listener?.stop();
   };
 }
 
@@ -50,7 +50,7 @@ export class DatabaseFieldObserver {
 
   constructor(public readonly fieldId: string) {}
 
-  subscribe = (callbacks: { onFieldsChanged: FieldNotificationCallback }) => {
+  subscribe = async (callbacks: { onFieldsChanged: FieldNotificationCallback }) => {
     this._notifier = new ChangeNotifier();
     this._notifier?.observer.subscribe(callbacks.onFieldsChanged);
 
@@ -70,7 +70,7 @@ export class DatabaseFieldObserver {
         }
       },
     });
-    return undefined;
+    await this._listener.start();
   };
 
   unsubscribe = async () => {

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

@@ -11,6 +11,7 @@ import {
   URLTypeOptionPB,
 } from '../../../../../../services/backend';
 import { utf8Decoder, utf8Encoder } from '../../cell/data_parser';
+import { DatabaseFieldObserver } from '../field_observer';
 
 abstract class TypeOptionSerde<T> {
   abstract deserialize(buffer: Uint8Array): T;
@@ -164,9 +165,17 @@ class ChecklistTypeOptionSerde extends TypeOptionSerde<ChecklistTypeOptionPB> {
 
 export class TypeOptionContext<T> {
   private typeOption: Option<T>;
+  private fieldObserver: DatabaseFieldObserver;
 
   constructor(public readonly parser: TypeOptionSerde<T>, private readonly controller: TypeOptionController) {
     this.typeOption = None;
+    this.fieldObserver = new DatabaseFieldObserver(controller.fieldId);
+
+    void this.fieldObserver.subscribe({
+      onFieldsChanged: () => {
+        void this.getTypeOption();
+      },
+    });
   }
 
   get viewId(): string {
@@ -174,20 +183,23 @@ export class TypeOptionContext<T> {
   }
 
   getTypeOption = async (): Promise<Result<T, FlowyError>> => {
-    if (this.typeOption.some) {
-      return Ok(this.typeOption.val);
-    }
-
     const result = await this.controller.getTypeOption();
     if (result.ok) {
-      return Ok(this.parser.deserialize(result.val.type_option_data));
+      const typeOption = this.parser.deserialize(result.val.type_option_data);
+      this.typeOption = Some(typeOption);
+      return Ok(typeOption);
     } else {
       return result;
     }
   };
 
-  setTypeOption = (typeOption: T) => {
-    this.controller.typeOption = this.parser.serialize(typeOption);
+  // Save the typeOption to disk
+  setTypeOption = async (typeOption: T) => {
+    await this.controller.saveTypeOption(this.parser.serialize(typeOption));
     this.typeOption = Some(typeOption);
   };
+
+  dispose = async () => {
+    await this.fieldObserver.unsubscribe();
+  };
 }

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

@@ -59,10 +59,10 @@ export class TypeOptionController {
     }
   };
 
-  set typeOption(data: Uint8Array) {
+  saveTypeOption = async (data: Uint8Array) => {
     if (this.typeOptionData.some) {
       this.typeOptionData.val.type_option_data = data;
-      void this.fieldBackendSvc?.updateTypeOption(data).then((result) => {
+      await this.fieldBackendSvc?.updateTypeOption(data).then((result) => {
         if (result.err) {
           Log.error(result.val);
         }
@@ -70,7 +70,7 @@ export class TypeOptionController {
     } else {
       throw Error('Unexpect empty type option data. Should call initialize first');
     }
-  }
+  };
 
   deleteField = async () => {
     if (this.fieldBackendSvc === undefined) {

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

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

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

@@ -0,0 +1,32 @@
+import { CreateRowPayloadPB, RowIdPB } from '../../../../../services/backend';
+import {
+  DatabaseEventCreateRow,
+  DatabaseEventDeleteRow,
+  DatabaseEventDuplicateRow,
+  DatabaseEventGetRow,
+} from '../../../../../services/backend/events/flowy-database';
+
+export class RowBackendService {
+  constructor(public readonly viewId: string) {}
+
+  // Create a row below the row with rowId
+  createRow = (rowId: string) => {
+    const payload = CreateRowPayloadPB.fromObject({ view_id: this.viewId, start_row_id: rowId });
+    return DatabaseEventCreateRow(payload);
+  };
+
+  deleteRow = (rowId: string) => {
+    const payload = RowIdPB.fromObject({ view_id: this.viewId, row_id: rowId });
+    return DatabaseEventDeleteRow(payload);
+  };
+
+  duplicateRow = (rowId: string) => {
+    const payload = RowIdPB.fromObject({ view_id: this.viewId, row_id: rowId });
+    return DatabaseEventDuplicateRow(payload);
+  };
+
+  getRow = (rowId: string) => {
+    const payload = RowIdPB.fromObject({ view_id: this.viewId, row_id: rowId });
+    return DatabaseEventGetRow(payload);
+  };
+}

+ 21 - 22
frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/view/database_view_cache.ts

@@ -1,63 +1,62 @@
 import { DatabaseViewRowsObserver } from './view_row_observer';
-import { RowCache, RowChangedReason, RowInfo } from '../row/row_cache';
+import { RowCache, RowInfo } from '../row/row_cache';
 import { FieldController } from '../field/field_controller';
-import { RowPB } from '../../../../../services/backend/models/flowy-database/row_entities';
+import { RowPB } from '../../../../../services/backend';
 import { Subscription } from 'rxjs';
 
 export class DatabaseViewCache {
-  private readonly _rowsObserver: DatabaseViewRowsObserver;
-  private readonly _rowCache: RowCache;
-  private readonly _fieldSubscription?: Subscription;
+  private readonly rowsObserver: DatabaseViewRowsObserver;
+  private readonly rowCache: RowCache;
+  private readonly fieldSubscription?: Subscription;
 
   constructor(public readonly viewId: string, fieldController: FieldController) {
-    this._rowsObserver = new DatabaseViewRowsObserver(viewId);
-    this._rowCache = new RowCache(viewId, () => fieldController.fieldInfos);
-    this._fieldSubscription = fieldController.subscribeOnFieldsChanged((fieldInfos) => {
+    this.rowsObserver = new DatabaseViewRowsObserver(viewId);
+    this.rowCache = new RowCache(viewId, () => fieldController.fieldInfos);
+    this.fieldSubscription = fieldController.subscribeOnFieldsChanged((fieldInfos) => {
       fieldInfos.forEach((fieldInfo) => {
-        this._rowCache.onFieldUpdated(fieldInfo);
+        this.rowCache.onFieldUpdated(fieldInfo);
       });
     });
-    this._listenOnRowsChanged();
   }
 
   initializeWithRows = (rows: RowPB[]) => {
-    this._rowCache.initializeRows(rows);
+    this.rowCache.initializeRows(rows);
   };
 
   get rowInfos(): readonly RowInfo[] {
-    return this._rowCache.rows;
+    return this.rowCache.rows;
   }
 
   getRowCache = () => {
-    return this._rowCache;
+    return this.rowCache;
   };
 
   dispose = async () => {
-    this._fieldSubscription?.unsubscribe();
-    await this._rowsObserver.unsubscribe();
-    await this._rowCache.dispose();
+    this.fieldSubscription?.unsubscribe();
+    await this.rowsObserver.unsubscribe();
+    await this.rowCache.dispose();
   };
 
-  _listenOnRowsChanged = () => {
-    this._rowsObserver.subscribe({
+  listenOnRowsChanged = async () => {
+    await this.rowsObserver.subscribe({
       onRowsVisibilityChanged: (result) => {
         if (result.ok) {
-          this._rowCache.applyRowsVisibility(result.val);
+          this.rowCache.applyRowsVisibility(result.val);
         }
       },
       onNumberOfRowsChanged: (result) => {
         if (result.ok) {
-          this._rowCache.applyRowsChanged(result.val);
+          this.rowCache.applyRowsChanged(result.val);
         }
       },
       onReorderRows: (result) => {
         if (result.ok) {
-          this._rowCache.applyReorderRows(result.val);
+          this.rowCache.applyReorderRows(result.val);
         }
       },
       onReorderSingleRow: (result) => {
         if (result.ok) {
-          this._rowCache.applyReorderSingleRow(result.val);
+          this.rowCache.applyReorderSingleRow(result.val);
         }
       },
     });

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

@@ -16,26 +16,26 @@ export type ReorderRowsNotifyValue = Result<string[], FlowyError>;
 export type ReorderSingleRowNotifyValue = Result<ReorderSingleRowPB, FlowyError>;
 
 export class DatabaseViewRowsObserver {
-  private _rowsVisibilityNotifier = new ChangeNotifier<RowsVisibilityNotifyValue>();
-  private _rowsNotifier = new ChangeNotifier<RowsNotifyValue>();
-  private _reorderRowsNotifier = new ChangeNotifier<ReorderRowsNotifyValue>();
-  private _reorderSingleRowNotifier = new ChangeNotifier<ReorderSingleRowNotifyValue>();
+  private rowsVisibilityNotifier = new ChangeNotifier<RowsVisibilityNotifyValue>();
+  private rowsNotifier = new ChangeNotifier<RowsNotifyValue>();
+  private reorderRowsNotifier = new ChangeNotifier<ReorderRowsNotifyValue>();
+  private reorderSingleRowNotifier = new ChangeNotifier<ReorderSingleRowNotifyValue>();
 
   private _listener?: DatabaseNotificationObserver;
 
   constructor(public readonly viewId: string) {}
 
-  subscribe = (callbacks: {
+  subscribe = async (callbacks: {
     onRowsVisibilityChanged?: (value: RowsVisibilityNotifyValue) => void;
     onNumberOfRowsChanged?: (value: RowsNotifyValue) => void;
     onReorderRows?: (value: ReorderRowsNotifyValue) => void;
     onReorderSingleRow?: (value: ReorderSingleRowNotifyValue) => void;
   }) => {
     //
-    this._rowsVisibilityNotifier.observer.subscribe(callbacks.onRowsVisibilityChanged);
-    this._rowsNotifier.observer.subscribe(callbacks.onNumberOfRowsChanged);
-    this._reorderRowsNotifier.observer.subscribe(callbacks.onReorderRows);
-    this._reorderSingleRowNotifier.observer.subscribe(callbacks.onReorderSingleRow);
+    this.rowsVisibilityNotifier.observer.subscribe(callbacks.onRowsVisibilityChanged);
+    this.rowsNotifier.observer.subscribe(callbacks.onNumberOfRowsChanged);
+    this.reorderRowsNotifier.observer.subscribe(callbacks.onReorderRows);
+    this.reorderSingleRowNotifier.observer.subscribe(callbacks.onReorderSingleRow);
 
     this._listener = new DatabaseNotificationObserver({
       viewId: this.viewId,
@@ -43,30 +43,30 @@ export class DatabaseViewRowsObserver {
         switch (notification) {
           case DatabaseNotification.DidUpdateViewRowsVisibility:
             if (result.ok) {
-              this._rowsVisibilityNotifier.notify(Ok(RowsVisibilityChangesetPB.deserializeBinary(result.val)));
+              this.rowsVisibilityNotifier.notify(Ok(RowsVisibilityChangesetPB.deserializeBinary(result.val)));
             } else {
-              this._rowsVisibilityNotifier.notify(result);
+              this.rowsVisibilityNotifier.notify(result);
             }
             break;
           case DatabaseNotification.DidUpdateViewRows:
             if (result.ok) {
-              this._rowsNotifier.notify(Ok(RowsChangesetPB.deserializeBinary(result.val)));
+              this.rowsNotifier.notify(Ok(RowsChangesetPB.deserializeBinary(result.val)));
             } else {
-              this._rowsNotifier.notify(result);
+              this.rowsNotifier.notify(result);
             }
             break;
           case DatabaseNotification.DidReorderRows:
             if (result.ok) {
-              this._reorderRowsNotifier.notify(Ok(ReorderAllRowsPB.deserializeBinary(result.val).row_orders));
+              this.reorderRowsNotifier.notify(Ok(ReorderAllRowsPB.deserializeBinary(result.val).row_orders));
             } else {
-              this._reorderRowsNotifier.notify(result);
+              this.reorderRowsNotifier.notify(result);
             }
             break;
           case DatabaseNotification.DidReorderSingleRow:
             if (result.ok) {
-              this._reorderSingleRowNotifier.notify(Ok(ReorderSingleRowPB.deserializeBinary(result.val)));
+              this.reorderSingleRowNotifier.notify(Ok(ReorderSingleRowPB.deserializeBinary(result.val)));
             } else {
-              this._reorderSingleRowNotifier.notify(result);
+              this.reorderSingleRowNotifier.notify(result);
             }
             break;
           default:
@@ -74,13 +74,14 @@ export class DatabaseViewRowsObserver {
         }
       },
     });
+    await this._listener.start();
   };
 
   unsubscribe = async () => {
-    this._rowsVisibilityNotifier.unsubscribe();
-    this._reorderRowsNotifier.unsubscribe();
-    this._rowsNotifier.unsubscribe();
-    this._reorderSingleRowNotifier.unsubscribe();
+    this.rowsVisibilityNotifier.unsubscribe();
+    this.reorderRowsNotifier.unsubscribe();
+    this.rowsNotifier.unsubscribe();
+    this.reorderSingleRowNotifier.unsubscribe();
     await this._listener?.stop();
   };
 }

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

@@ -6,15 +6,14 @@ import { FolderNotificationObserver } from '../notifications/observer';
 export type AppUpdateNotifyValue = Result<AppPB, FlowyError>;
 export type AppUpdateNotifyCallback = (value: AppUpdateNotifyValue) => void;
 
-export class WorkspaceObserver {
+export class AppObserver {
   _appNotifier = new ChangeNotifier<AppUpdateNotifyValue>();
   _listener?: FolderNotificationObserver;
 
   constructor(public readonly appId: string) {}
 
-  subscribe = (callbacks: { onAppChanged: AppUpdateNotifyCallback }) => {
+  subscribe = async (callbacks: { onAppChanged: AppUpdateNotifyCallback }) => {
     this._appNotifier?.observer.subscribe(callbacks.onAppChanged);
-
     this._listener = new FolderNotificationObserver({
       viewId: this.appId,
       parserHandler: (notification, result) => {
@@ -31,7 +30,7 @@ export class WorkspaceObserver {
         }
       },
     });
-    return undefined;
+    await this._listener.start();
   };
 
   unsubscribe = async () => {

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

@@ -18,7 +18,7 @@ export class ViewObserver {
 
   constructor(public readonly viewId: string) {}
 
-  subscribe = (callbacks: {
+  subscribe = async (callbacks: {
     onViewUpdate?: (value: UpdateViewNotifyValue) => void;
     onViewDelete?: (value: DeleteViewNotifyValue) => void;
     onViewRestored?: (value: RestoreViewNotifyValue) => void;
@@ -77,7 +77,7 @@ export class ViewObserver {
         }
       },
     });
-    return undefined;
+    await this._listener.start();
   };
 
   unsubscribe = async () => {

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

@@ -9,32 +9,35 @@ export type WorkspaceNotifyValue = Result<WorkspacePB, FlowyError>;
 export type WorkspaceNotifyCallback = (value: WorkspaceNotifyValue) => void;
 
 export class WorkspaceObserver {
-  private _appListNotifier = new ChangeNotifier<AppListNotifyValue>();
-  private _workspaceNotifier = new ChangeNotifier<WorkspaceNotifyValue>();
-  private _listener?: FolderNotificationObserver;
+  private appListNotifier = new ChangeNotifier<AppListNotifyValue>();
+  private workspaceNotifier = new ChangeNotifier<WorkspaceNotifyValue>();
+  private listener?: FolderNotificationObserver;
 
   constructor(public readonly workspaceId: string) {}
 
-  subscribe = (callbacks: { onAppListChanged: AppListNotifyCallback; onWorkspaceChanged: WorkspaceNotifyCallback }) => {
-    this._appListNotifier?.observer.subscribe(callbacks.onAppListChanged);
-    this._workspaceNotifier?.observer.subscribe(callbacks.onWorkspaceChanged);
+  subscribe = async (callbacks: {
+    onAppListChanged: AppListNotifyCallback;
+    onWorkspaceChanged: WorkspaceNotifyCallback;
+  }) => {
+    this.appListNotifier?.observer.subscribe(callbacks.onAppListChanged);
+    this.workspaceNotifier?.observer.subscribe(callbacks.onWorkspaceChanged);
 
-    this._listener = new FolderNotificationObserver({
+    this.listener = new FolderNotificationObserver({
       viewId: this.workspaceId,
       parserHandler: (notification, result) => {
         switch (notification) {
           case FolderNotification.DidUpdateWorkspace:
             if (result.ok) {
-              this._workspaceNotifier?.notify(Ok(WorkspacePB.deserializeBinary(result.val)));
+              this.workspaceNotifier?.notify(Ok(WorkspacePB.deserializeBinary(result.val)));
             } else {
-              this._workspaceNotifier?.notify(result);
+              this.workspaceNotifier?.notify(result);
             }
             break;
           case FolderNotification.DidUpdateWorkspaceApps:
             if (result.ok) {
-              this._appListNotifier?.notify(Ok(RepeatedAppPB.deserializeBinary(result.val).items));
+              this.appListNotifier?.notify(Ok(RepeatedAppPB.deserializeBinary(result.val).items));
             } else {
-              this._appListNotifier?.notify(result);
+              this.appListNotifier?.notify(result);
             }
             break;
           default:
@@ -42,12 +45,12 @@ export class WorkspaceObserver {
         }
       },
     });
-    return undefined;
+    await this.listener.start();
   };
 
   unsubscribe = async () => {
-    this._appListNotifier.unsubscribe();
-    this._workspaceNotifier.unsubscribe();
-    await this._listener?.stop();
+    this.appListNotifier.unsubscribe();
+    this.workspaceNotifier.unsubscribe();
+    await this.listener?.stop();
   };
 }

+ 12 - 6
frontend/appflowy_tauri/src/services/backend/notifications/observer.ts

@@ -1,6 +1,6 @@
-import { listen, UnlistenFn } from '@tauri-apps/api/event';
-import { SubscribeObject } from '../models/flowy-notification';
-import { NotificationParser } from './parser';
+import { listen, UnlistenFn } from "@tauri-apps/api/event";
+import { SubscribeObject } from "../models/flowy-notification";
+import { NotificationParser } from "./parser";
 
 export abstract class AFNotificationObserver<T> {
   parser?: NotificationParser<T> | null;
@@ -11,9 +11,15 @@ export abstract class AFNotificationObserver<T> {
   }
 
   async start() {
-    this._listener = await listen('af-notification', (notification) => {
-      const object = SubscribeObject.fromObject(notification.payload as {});
-      this.parser?.parse(object);
+    this._listener = await listen("af-notification", (notification) => {
+      const object: SubscribeObject = SubscribeObject.fromObject(notification.payload as {});
+      if (this.parser?.id !== undefined) {
+        if (object.id === this.parser.id) {
+          this.parser?.parse(object);
+        }
+      } else {
+        this.parser?.parse(object);
+      }
     });
   }
 

+ 2 - 2
frontend/rust-lib/flowy-codegen/src/ts_event/event_template.tera

@@ -24,14 +24,14 @@ export async function {{ event_func_name }}(): Promise<Result<{{ output_deserial
     if (result.code == 0) {
     {%- if has_output  %}
         let object = {{ output_deserializer }}.deserializeBinary(result.payload);
-        console.log("Success:" + JSON.stringify(object.toObject()))
+        console.log({{ event_func_name }}.name, object);
         return Ok(object);
     {%- else %}
         return Ok.EMPTY;
     {%- endif %}
     } else {
         let error = {{ error_deserializer }}.deserializeBinary(result.payload);
-        console.log("Error:" + JSON.stringify(error.toObject()))
+        console.log({{ event_func_name }}.name, error);
         return Err(error);
     }
 }

+ 1 - 1
frontend/rust-lib/flowy-database/src/entities/row_entities.rs

@@ -191,7 +191,7 @@ impl TryInto<CreateRowParams> for CreateRowPayloadPB {
   type Error = ErrorCode;
 
   fn try_into(self) -> Result<CreateRowParams, Self::Error> {
-    let view_id = NotEmptyStr::parse(self.view_id).map_err(|_| ErrorCode::DatabaseIdIsEmpty)?;
+    let view_id = NotEmptyStr::parse(self.view_id).map_err(|_| ErrorCode::ViewIdIsInvalid)?;
 
     Ok(CreateRowParams {
       view_id: view_id.0,