Przeglądaj źródła

chore: add edit / create field test

nathan 2 lat temu
rodzic
commit
3619fadf57

+ 18 - 0
frontend/appflowy_tauri/src/appflowy_app/components/TestApiButton/DatabaseTestHelper.ts

@@ -13,6 +13,8 @@ import {
 } from '../../stores/effects/database/cell/controller_builder';
 import assert from 'assert';
 import { None, Option, Some } from 'ts-results';
+import { TypeOptionBackendService } from '../../stores/effects/database/field/type_option/type_option_bd_svc';
+import { DatabaseBackendService } from '../../stores/effects/database/database_bd_svc';
 
 // Create a database view for specific layout type
 // Do not use it production code. Just for testing
@@ -104,3 +106,19 @@ export async function makeCellControllerBuilder(
 
   return None;
 }
+
+export async function assertFieldName(viewId: string, fieldId: string, fieldType: FieldType, expected: string) {
+  const svc = new TypeOptionBackendService(viewId);
+  const typeOptionPB = await svc.getTypeOption(fieldId, fieldType).then((result) => result.unwrap());
+  if (typeOptionPB.field.name !== expected) {
+    throw Error();
+  }
+}
+
+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();
+  }
+}

+ 11 - 1
frontend/appflowy_tauri/src/appflowy_app/components/TestApiButton/TestAPI.tsx

@@ -1,6 +1,13 @@
 import React from 'react';
 import TestApiButton from './TestApiButton';
-import { TestCreateGrid, TestCreateSelectOption, TestEditCell } from './TestGrid';
+import {
+  TestCreateGrid,
+  TestCreateNewField,
+  TestCreateSelectOption,
+  TestDeleteField,
+  TestEditCell,
+  TestEditField,
+} from './TestGrid';
 
 export const TestAPI = () => {
   return (
@@ -10,6 +17,9 @@ export const TestAPI = () => {
         <TestCreateGrid></TestCreateGrid>
         <TestEditCell></TestEditCell>
         <TestCreateSelectOption></TestCreateSelectOption>
+        <TestEditField></TestEditField>
+        <TestCreateNewField></TestCreateNewField>
+        {/*<TestDeleteField></TestDeleteField>*/}
       </ul>
     </React.Fragment>
   );

+ 58 - 0
frontend/appflowy_tauri/src/appflowy_app/components/TestApiButton/TestGrid.tsx

@@ -2,6 +2,8 @@ import React from 'react';
 import { SelectOptionCellDataPB, ViewLayoutTypePB } from '../../../services/backend';
 import { Log } from '../../utils/log';
 import {
+  assertFieldName,
+  assertNumberOfFields,
   assertTextCell,
   createTestDatabaseView,
   editTextCell,
@@ -10,6 +12,9 @@ import {
 } from './DatabaseTestHelper';
 import assert from 'assert';
 import { SelectOptionBackendService } 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';
 
 export const TestCreateGrid = () => {
   async function createBuildInGrid() {
@@ -80,6 +85,59 @@ export const TestCreateSelectOption = () => {
   return TestButton('Test create a select option', testCreateOption);
 };
 
+export const TestEditField = () => {
+  async function testEditField() {
+    const view = await createTestDatabaseView(ViewLayoutTypePB.Grid);
+    const databaseController = await openTestDatabase(view.id);
+    await databaseController.open().then((result) => result.unwrap());
+    const fieldInfos = databaseController.fieldController.fieldInfos;
+
+    // Modify the name of the field
+    const firstFieldInfo = fieldInfos[0];
+    const controller = new TypeOptionController(view.id, Some(firstFieldInfo));
+    await controller.initialize();
+    await controller.setFieldName('hello world');
+
+    await assertFieldName(view.id, firstFieldInfo.field.id, firstFieldInfo.field.field_type, 'hello world');
+  }
+
+  return TestButton('Test edit the column name', testEditField);
+};
+
+export const TestCreateNewField = () => {
+  async function testCreateNewField() {
+    const view = await createTestDatabaseView(ViewLayoutTypePB.Grid);
+    const databaseController = await openTestDatabase(view.id);
+    await databaseController.open().then((result) => result.unwrap());
+    await assertNumberOfFields(view.id, 3);
+
+    // Modify the name of the field
+    const controller = new TypeOptionController(view.id, None);
+    await controller.initialize();
+    await assertNumberOfFields(view.id, 4);
+  }
+
+  return TestButton('Test create a new column', testCreateNewField);
+};
+
+export const TestDeleteField = () => {
+  async function testDeleteField() {
+    const view = await createTestDatabaseView(ViewLayoutTypePB.Grid);
+    const databaseController = await openTestDatabase(view.id);
+    await databaseController.open().then((result) => result.unwrap());
+
+    // Modify the name of the field
+    const fieldInfo = databaseController.fieldController.fieldInfos[0];
+    const controller = new TypeOptionController(view.id, Some(fieldInfo));
+    await controller.initialize();
+    await assertNumberOfFields(view.id, 3);
+    await controller.deleteField();
+    await assertNumberOfFields(view.id, 2);
+  }
+
+  return TestButton('Test delete a new column', testDeleteField);
+};
+
 const TestButton = (title: string, onClick: () => void) => {
   return (
     <React.Fragment>

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

@@ -30,7 +30,8 @@ class CellDataLoader<T> {
   };
 }
 
-const utf8Decoder = new TextDecoder('utf-8');
+export const utf8Decoder = new TextDecoder('utf-8');
+export const utf8Encoder = new TextEncoder();
 
 class StringCellDataParser extends CellDataParser<string> {
   parserData(data: Uint8Array): string {

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

@@ -23,7 +23,7 @@ export class FieldBackendService {
 
   updateField = (data: {
     name?: string;
-    fieldType: FieldType;
+    fieldType?: FieldType;
     frozen?: boolean;
     visibility?: boolean;
     width?: number;
@@ -65,7 +65,6 @@ export class FieldBackendService {
 
   deleteField = () => {
     const payload = DeleteFieldPayloadPB.fromObject({ view_id: this.viewId, field_id: this.fieldId });
-
     return DatabaseEventDeleteField(payload);
   };
 

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

@@ -1,17 +1,17 @@
 import { Log } from '../../../../utils/log';
 import { DatabaseBackendService } from '../database_bd_svc';
-import { DatabaseFieldObserver } from './field_observer';
+import { DatabaseFieldChangesetObserver } from './field_observer';
 import { FieldIdPB, FieldPB, IndexFieldPB } from '../../../../../services/backend/models/flowy-database/field_entities';
 import { ChangeNotifier } from '../../../../utils/change_notifier';
 
 export class FieldController {
-  private _fieldListener: DatabaseFieldObserver;
+  private _fieldListener: DatabaseFieldChangesetObserver;
   private _backendService: DatabaseBackendService;
   private _fieldNotifier = new FieldNotifier([]);
 
   constructor(public readonly viewId: string) {
     this._backendService = new DatabaseBackendService(viewId);
-    this._fieldListener = new DatabaseFieldObserver(viewId);
+    this._fieldListener = new DatabaseFieldChangesetObserver(viewId);
 
     this._listenOnFieldChanges();
   }

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

@@ -1,14 +1,12 @@
-import { Err, Ok, Result } from 'ts-results';
-import { DatabaseNotification } from '../../../../../services/backend';
-import { DatabaseFieldChangesetPB } from '../../../../../services/backend/models/flowy-database/field_entities';
-import { FlowyError } from '../../../../../services/backend/models/flowy-error';
+import { Ok, Result } from 'ts-results';
+import { DatabaseNotification, DatabaseFieldChangesetPB, FlowyError, FieldPB } from '../../../../../services/backend';
 import { ChangeNotifier } from '../../../../utils/change_notifier';
 import { DatabaseNotificationObserver } from '../notifications/observer';
 
 type UpdateFieldNotifiedValue = Result<DatabaseFieldChangesetPB, FlowyError>;
 export type DatabaseNotificationCallback = (value: UpdateFieldNotifiedValue) => void;
 
-export class DatabaseFieldObserver {
+export class DatabaseFieldChangesetObserver {
   private _notifier?: ChangeNotifier<UpdateFieldNotifiedValue>;
   private _listener?: DatabaseNotificationObserver;
 
@@ -42,3 +40,41 @@ export class DatabaseFieldObserver {
     await this._listener?.stop();
   };
 }
+
+type FieldNotifiedValue = Result<FieldPB, FlowyError>;
+export type FieldNotificationCallback = (value: FieldNotifiedValue) => void;
+
+export class DatabaseFieldObserver {
+  private _notifier?: ChangeNotifier<FieldNotifiedValue>;
+  private _listener?: DatabaseNotificationObserver;
+
+  constructor(public readonly fieldId: string) {}
+
+  subscribe = (callbacks: { onFieldsChanged: FieldNotificationCallback }) => {
+    this._notifier = new ChangeNotifier();
+    this._notifier?.observer.subscribe(callbacks.onFieldsChanged);
+
+    this._listener = new DatabaseNotificationObserver({
+      viewId: this.fieldId,
+      parserHandler: (notification, result) => {
+        switch (notification) {
+          case DatabaseNotification.DidUpdateField:
+            if (result.ok) {
+              this._notifier?.notify(Ok(FieldPB.deserializeBinary(result.val)));
+            } else {
+              this._notifier?.notify(result);
+            }
+            break;
+          default:
+            break;
+        }
+      },
+    });
+    return undefined;
+  };
+
+  unsubscribe = async () => {
+    this._notifier?.unsubscribe();
+    await this._listener?.stop();
+  };
+}

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

@@ -0,0 +1,38 @@
+import {
+  CreateFieldPayloadPB,
+  FieldType,
+  TypeOptionPathPB,
+  UpdateFieldTypePayloadPB,
+} from '../../../../../../services/backend';
+import {
+  DatabaseEventCreateTypeOption,
+  DatabaseEventGetTypeOption,
+  DatabaseEventUpdateFieldType,
+} from '../../../../../../services/backend/events/flowy-database';
+
+export class TypeOptionBackendService {
+  constructor(public readonly viewId: string) {}
+
+  createTypeOption = (fieldType: FieldType) => {
+    const payload = CreateFieldPayloadPB.fromObject({ view_id: this.viewId, field_type: fieldType });
+    return DatabaseEventCreateTypeOption(payload);
+  };
+
+  getTypeOption = (fieldId: string, fieldType: FieldType) => {
+    const payload = TypeOptionPathPB.fromObject({
+      view_id: this.viewId,
+      field_id: fieldId,
+      field_type: fieldType,
+    });
+    return DatabaseEventGetTypeOption(payload);
+  };
+
+  updateTypeOptionType = (fieldId: string, fieldType: FieldType) => {
+    const payload = UpdateFieldTypePayloadPB.fromObject({
+      view_id: this.viewId,
+      field_id: fieldId,
+      field_type: fieldType,
+    });
+    return DatabaseEventUpdateFieldType(payload);
+  };
+}

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

@@ -0,0 +1,193 @@
+import { None, Ok, Option, Result, Some } from 'ts-results';
+import { TypeOptionController } from './type_option_controller';
+import {
+  CheckboxTypeOptionPB,
+  ChecklistTypeOptionPB,
+  DateTypeOptionPB,
+  FlowyError,
+  MultiSelectTypeOptionPB,
+  NumberTypeOptionPB,
+  SingleSelectTypeOptionPB,
+  URLTypeOptionPB,
+} from '../../../../../../services/backend';
+import { utf8Decoder, utf8Encoder } from '../../cell/data_parser';
+
+abstract class TypeOptionSerde<T> {
+  abstract deserialize(buffer: Uint8Array): T;
+
+  abstract serialize(value: T): Uint8Array;
+}
+
+// RichText
+export function makeRichTextTypeOptionContext(controller: TypeOptionController): RichTextTypeOptionContext {
+  const parser = new RichTextTypeOptionSerde();
+  return new TypeOptionContext<string>(parser, controller);
+}
+
+export type RichTextTypeOptionContext = TypeOptionContext<string>;
+
+class RichTextTypeOptionSerde extends TypeOptionSerde<string> {
+  deserialize(buffer: Uint8Array): string {
+    return utf8Decoder.decode(buffer);
+  }
+
+  serialize(value: string): Uint8Array {
+    return utf8Encoder.encode(value);
+  }
+}
+
+// Number
+export function makeNumberTypeOptionContext(controller: TypeOptionController): NumberTypeOptionContext {
+  const parser = new NumberTypeOptionSerde();
+  return new TypeOptionContext<NumberTypeOptionPB>(parser, controller);
+}
+
+export type NumberTypeOptionContext = TypeOptionContext<NumberTypeOptionPB>;
+
+class NumberTypeOptionSerde extends TypeOptionSerde<NumberTypeOptionPB> {
+  deserialize(buffer: Uint8Array): NumberTypeOptionPB {
+    return NumberTypeOptionPB.deserializeBinary(buffer);
+  }
+
+  serialize(value: NumberTypeOptionPB): Uint8Array {
+    return value.serializeBinary();
+  }
+}
+
+// Checkbox
+export function makeCheckboxTypeOptionContext(controller: TypeOptionController): CheckboxTypeOptionContext {
+  const parser = new CheckboxTypeOptionSerde();
+  return new TypeOptionContext<CheckboxTypeOptionPB>(parser, controller);
+}
+
+export type CheckboxTypeOptionContext = TypeOptionContext<CheckboxTypeOptionPB>;
+
+class CheckboxTypeOptionSerde extends TypeOptionSerde<CheckboxTypeOptionPB> {
+  deserialize(buffer: Uint8Array): CheckboxTypeOptionPB {
+    return CheckboxTypeOptionPB.deserializeBinary(buffer);
+  }
+
+  serialize(value: CheckboxTypeOptionPB): Uint8Array {
+    return value.serializeBinary();
+  }
+}
+
+// URL
+export function makeURLTypeOptionContext(controller: TypeOptionController): URLTypeOptionContext {
+  const parser = new URLTypeOptionSerde();
+  return new TypeOptionContext<URLTypeOptionPB>(parser, controller);
+}
+
+export type URLTypeOptionContext = TypeOptionContext<URLTypeOptionPB>;
+
+class URLTypeOptionSerde extends TypeOptionSerde<URLTypeOptionPB> {
+  deserialize(buffer: Uint8Array): URLTypeOptionPB {
+    return URLTypeOptionPB.deserializeBinary(buffer);
+  }
+
+  serialize(value: URLTypeOptionPB): Uint8Array {
+    return value.serializeBinary();
+  }
+}
+
+// Date
+export function makeDateTypeOptionContext(controller: TypeOptionController): DateTypeOptionContext {
+  const parser = new DateTypeOptionSerde();
+  return new TypeOptionContext<DateTypeOptionPB>(parser, controller);
+}
+
+export type DateTypeOptionContext = TypeOptionContext<DateTypeOptionPB>;
+
+class DateTypeOptionSerde extends TypeOptionSerde<DateTypeOptionPB> {
+  deserialize(buffer: Uint8Array): DateTypeOptionPB {
+    return DateTypeOptionPB.deserializeBinary(buffer);
+  }
+
+  serialize(value: DateTypeOptionPB): Uint8Array {
+    return value.serializeBinary();
+  }
+}
+
+// SingleSelect
+export function makeSingleSelectTypeOptionContext(controller: TypeOptionController): SingleSelectTypeOptionContext {
+  const parser = new SingleSelectTypeOptionSerde();
+  return new TypeOptionContext<SingleSelectTypeOptionPB>(parser, controller);
+}
+
+export type SingleSelectTypeOptionContext = TypeOptionContext<SingleSelectTypeOptionPB>;
+
+class SingleSelectTypeOptionSerde extends TypeOptionSerde<SingleSelectTypeOptionPB> {
+  deserialize(buffer: Uint8Array): SingleSelectTypeOptionPB {
+    return SingleSelectTypeOptionPB.deserializeBinary(buffer);
+  }
+
+  serialize(value: SingleSelectTypeOptionPB): Uint8Array {
+    return value.serializeBinary();
+  }
+}
+
+// Multi-select
+export function makeMultiSelectTypeOptionContext(controller: TypeOptionController): MultiSelectTypeOptionContext {
+  const parser = new MultiSelectTypeOptionSerde();
+  return new TypeOptionContext<MultiSelectTypeOptionPB>(parser, controller);
+}
+
+export type MultiSelectTypeOptionContext = TypeOptionContext<MultiSelectTypeOptionPB>;
+
+class MultiSelectTypeOptionSerde extends TypeOptionSerde<MultiSelectTypeOptionPB> {
+  deserialize(buffer: Uint8Array): MultiSelectTypeOptionPB {
+    return MultiSelectTypeOptionPB.deserializeBinary(buffer);
+  }
+
+  serialize(value: MultiSelectTypeOptionPB): Uint8Array {
+    return value.serializeBinary();
+  }
+}
+
+// Checklist
+export function makeChecklistTypeOptionContext(controller: TypeOptionController): ChecklistTypeOptionContext {
+  const parser = new ChecklistTypeOptionSerde();
+  return new TypeOptionContext<ChecklistTypeOptionPB>(parser, controller);
+}
+
+export type ChecklistTypeOptionContext = TypeOptionContext<ChecklistTypeOptionPB>;
+
+class ChecklistTypeOptionSerde extends TypeOptionSerde<ChecklistTypeOptionPB> {
+  deserialize(buffer: Uint8Array): ChecklistTypeOptionPB {
+    return ChecklistTypeOptionPB.deserializeBinary(buffer);
+  }
+
+  serialize(value: ChecklistTypeOptionPB): Uint8Array {
+    return value.serializeBinary();
+  }
+}
+
+export class TypeOptionContext<T> {
+  private typeOption: Option<T>;
+
+  constructor(public readonly parser: TypeOptionSerde<T>, private readonly controller: TypeOptionController) {
+    this.typeOption = None;
+  }
+
+  get viewId(): string {
+    return this.controller.viewId;
+  }
+
+  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));
+    } else {
+      return result;
+    }
+  };
+
+  setTypeOption = (typeOption: T) => {
+    this.controller.typeOption = this.parser.serialize(typeOption);
+    this.typeOption = Some(typeOption);
+  };
+}

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

@@ -0,0 +1,113 @@
+import { FieldPB, FieldType, TypeOptionPB } from '../../../../../../services/backend';
+import { ChangeNotifier } from '../../../../../utils/change_notifier';
+import { FieldBackendService } from '../field_bd_svc';
+import { Log } from '../../../../../utils/log';
+import { None, Option, Some } from 'ts-results';
+import { FieldInfo } from '../field_controller';
+import { TypeOptionBackendService } from './type_option_bd_svc';
+
+export class TypeOptionController {
+  private fieldNotifier = new ChangeNotifier<FieldPB>();
+  private typeOptionData: Option<TypeOptionPB>;
+  private fieldBackendSvc?: FieldBackendService;
+  private typeOptionBackendSvc: TypeOptionBackendService;
+
+  // Must call [initialize] if the passed-in fieldInfo is None
+  constructor(public readonly viewId: string, private initialFieldInfo: Option<FieldInfo> = None) {
+    this.typeOptionData = None;
+    this.typeOptionBackendSvc = new TypeOptionBackendService(viewId);
+  }
+
+  initialize = async () => {
+    if (this.initialFieldInfo.none) {
+      await this.createTypeOption();
+    } else {
+      await this.getTypeOption();
+    }
+  };
+
+  get fieldId(): string {
+    return this.getFieldInfo().field.id;
+  }
+
+  get fieldType(): FieldType {
+    return this.getFieldInfo().field.field_type;
+  }
+
+  getFieldInfo = (): FieldInfo => {
+    if (this.typeOptionData.none) {
+      if (this.initialFieldInfo.some) {
+        return this.initialFieldInfo.val;
+      } else {
+        throw Error('Unexpect empty type option data. Should call initialize first');
+      }
+    }
+    return new FieldInfo(this.typeOptionData.val.field);
+  };
+
+  switchToField = (fieldType: FieldType) => {
+    return this.typeOptionBackendSvc.updateTypeOptionType(this.fieldId, fieldType);
+  };
+
+  setFieldName = async (name: string) => {
+    if (this.typeOptionData.some) {
+      this.typeOptionData.val.field.name = name;
+      void this.fieldBackendSvc?.updateField({ name: name });
+      this.fieldNotifier.notify(this.typeOptionData.val.field);
+    } else {
+      throw Error('Unexpect empty type option data. Should call initialize first');
+    }
+  };
+
+  set typeOption(data: Uint8Array) {
+    if (this.typeOptionData.some) {
+      this.typeOptionData.val.type_option_data = data;
+      void this.fieldBackendSvc?.updateTypeOption(data).then((result) => {
+        if (result.err) {
+          Log.error(result.val);
+        }
+      });
+    } else {
+      throw Error('Unexpect empty type option data. Should call initialize first');
+    }
+  }
+
+  deleteField = async () => {
+    if (this.fieldBackendSvc === undefined) {
+      Log.error('Unexpect empty field backend service');
+    }
+    return this.fieldBackendSvc?.deleteField();
+  };
+
+  duplicateField = async () => {
+    if (this.fieldBackendSvc === undefined) {
+      Log.error('Unexpect empty field backend service');
+    }
+    return this.fieldBackendSvc?.duplicateField();
+  };
+
+  // Returns the type option for specific field with specific fieldType
+  getTypeOption = async () => {
+    return this.typeOptionBackendSvc.getTypeOption(this.fieldId, this.fieldType).then((result) => {
+      if (result.ok) {
+        this.updateTypeOptionData(result.val);
+      }
+      return result;
+    });
+  };
+
+  private createTypeOption = (fieldType: FieldType = FieldType.RichText) => {
+    return this.typeOptionBackendSvc.createTypeOption(fieldType).then((result) => {
+      if (result.ok) {
+        this.updateTypeOptionData(result.val);
+      }
+      return result;
+    });
+  };
+
+  private updateTypeOptionData = (typeOptionData: TypeOptionPB) => {
+    this.typeOptionData = Some(typeOptionData);
+    this.fieldBackendSvc = new FieldBackendService(this.viewId, typeOptionData.field.id);
+    this.fieldNotifier.notify(typeOptionData.field);
+  };
+}