chore: add edit / create field test

This commit is contained in:
nathan 2023-03-01 14:04:59 +08:00
parent f6957fb160
commit 3619fadf57
10 changed files with 478 additions and 12 deletions

View File

@ -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();
}
}

View File

@ -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>
);

View File

@ -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>

View File

@ -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 {

View File

@ -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);
};

View File

@ -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();
}

View File

@ -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();
};
}

View File

@ -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);
};
}

View File

@ -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);
};
}

View File

@ -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);
};
}