chore: add tauri database group test (#1924)

* chore: add tauri database group test

* chore: add more tests

* chore: enable run all tests

* chore: rename test folder
This commit is contained in:
Nathan.fooo 2023-03-05 16:26:27 +08:00 committed by GitHub
parent 8e22ef2230
commit 7e7cee4bf4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
32 changed files with 1187 additions and 581 deletions

View File

@ -1,9 +1,9 @@
import 'package:appflowy_backend/protobuf/flowy-database/database_entities.pb.dart';
import 'package:dartz/dartz.dart';
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database/field_entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database/grid_entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database/group.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database/row_entities.pb.dart';
@ -60,6 +60,6 @@ class DatabaseBackendService {
Future<Either<RepeatedGroupPB, FlowyError>> loadGroups() {
final payload = DatabaseViewIdPB(value: viewId);
return DatabaseEventGetGroup(payload).send();
return DatabaseEventGetGroups(payload).send();
}
}

View File

@ -1,6 +1,6 @@
import 'dart:collection';
import 'package:appflowy_backend/protobuf/flowy-database/grid_entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database/database_entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import '../grid/presentation/widgets/filter/filter_info.dart';

View File

@ -1,8 +1,8 @@
import 'package:appflowy_backend/protobuf/flowy-database/database_entities.pb.dart';
import 'package:dartz/dartz.dart';
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database/field_entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database/grid_entities.pb.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'field_service.freezed.dart';

View File

@ -1,3 +1,4 @@
import 'package:appflowy_backend/protobuf/flowy-database/database_entities.pb.dart';
import 'package:dartz/dartz.dart';
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/log.dart';
@ -6,7 +7,6 @@ import 'package:appflowy_backend/protobuf/flowy-database/checkbox_filter.pbserve
import 'package:appflowy_backend/protobuf/flowy-database/checklist_filter.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database/date_filter.pbserver.dart';
import 'package:appflowy_backend/protobuf/flowy-database/field_entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database/grid_entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database/number_filter.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database/select_option_filter.pbserver.dart';
import 'package:appflowy_backend/protobuf/flowy-database/setting_entities.pb.dart';

View File

@ -1,7 +1,7 @@
import 'package:appflowy_backend/protobuf/flowy-database/database_entities.pb.dart';
import 'package:dartz/dartz.dart';
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database/grid_entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database/group_changeset.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database/row_entities.pb.dart';

View File

@ -1,8 +1,8 @@
import 'package:appflowy_backend/protobuf/flowy-database/database_entities.pb.dart';
import 'package:dartz/dartz.dart';
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database/field_entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database/grid_entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database/group.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database/setting_entities.pb.dart';

View File

@ -1,9 +1,9 @@
import 'package:appflowy_backend/protobuf/flowy-database/database_entities.pb.dart';
import 'package:dartz/dartz.dart';
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database/field_entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database/grid_entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database/setting_entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database/sort_entities.pb.dart';

View File

@ -68,9 +68,8 @@ class GroupController {
if (index != -1) {
group.rows[index] = updatedRow;
delegate.updateRow(group, updatedRow);
}
delegate.updateRow(group, updatedRow);
}
},
(err) => Log.error(err),
@ -78,29 +77,6 @@ class GroupController {
});
}
// GroupChangesetPB _transformChangeset(GroupChangesetPB changeset) {
// final insertedRows = changeset.insertedRows
// .where(
// (delete) => !changeset.deletedRows.contains(delete.row.id),
// )
// .toList();
// final deletedRows = changeset.deletedRows
// .where((deletedRowId) =>
// changeset.insertedRows
// .indexWhere((insert) => insert.row.id == deletedRowId) ==
// -1)
// .toList();
// return changeset.rebuild((rebuildChangeset) {
// rebuildChangeset.insertedRows.clear();
// rebuildChangeset.insertedRows.addAll(insertedRows);
// rebuildChangeset.deletedRows.clear();
// rebuildChangeset.deletedRows.addAll(deletedRows);
// });
// }
Future<void> dispose() async {
_listener.stop();
}

View File

@ -12,7 +12,7 @@ import { SignUpPage } from './views/SignUpPage';
import { ConfirmAccountPage } from './views/ConfirmAccountPage';
import { ErrorHandlerPage } from './components/error/ErrorHandlerPage';
import initializeI18n from './stores/i18n/initializeI18n';
import { TestAPI } from './components/TestApiButton/TestAPI';
import { TestAPI } from './components/tests/TestAPI';
import { GetStarted } from './components/auth/GetStarted/GetStarted';
initializeI18n();

View File

@ -1,334 +0,0 @@
import React from 'react';
import {
FieldType,
NumberFormat,
NumberTypeOptionPB,
SelectOptionCellDataPB,
SingleSelectTypeOptionPB,
ViewLayoutTypePB,
} from '../../../services/backend';
import { Log } from '../../utils/log';
import {
assertFieldName,
assertNumberOfFields,
assertNumberOfRows,
assertTextCell,
createTestDatabaseView,
editTextCell,
findFirstFieldInfoWithFieldType,
makeMultiSelectCellController,
makeSingleSelectCellController,
makeTextCellController,
openTestDatabase,
} from './DatabaseTestHelper';
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 { RowBackendService } from '../../stores/effects/database/row/row_bd_svc';
import {
makeNumberTypeOptionContext,
makeSingleSelectTypeOptionContext,
} from '../../stores/effects/database/field/type_option/type_option_context';
export const TestCreateGrid = () => {
async function createBuildInGrid() {
const view = await createTestDatabaseView(ViewLayoutTypePB.Grid);
const databaseController = await openTestDatabase(view.id);
databaseController.subscribe({
onViewChanged: (databasePB) => {
Log.debug('Did receive database:' + databasePB);
},
// onRowsChanged: async (rows) => {
// if (rows.length !== 3) {
// throw Error('Expected number of rows is 3, but receive ' + rows.length);
// }
// },
onFieldsChanged: (fields) => {
if (fields.length !== 3) {
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);
};
export const TestEditCell = () => {
async function testGridRow() {
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()) {
const cellContent = index.toString();
const fieldInfo = findFirstFieldInfoWithFieldType(row, FieldType.RichText).unwrap();
await editTextCell(fieldInfo.field.id, row, databaseController, cellContent);
await assertTextCell(fieldInfo.field.id, row, databaseController, cellContent);
}
}
return TestButton('Test editing cell', testGridRow);
};
export const TestCreateRow = () => {
async function testCreateRow() {
const view = await createTestDatabaseView(ViewLayoutTypePB.Grid);
const databaseController = await openTestDatabase(view.id);
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 fieldInfo = findFirstFieldInfoWithFieldType(row, FieldType.SingleSelect).unwrap();
const cellController = await makeSingleSelectCellController(fieldInfo.field.id, row, databaseController).then(
(result) => result.unwrap()
);
await cellController.subscribeChanged({
onCellChanged: (value) => {
if (value.some) {
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 get single-select column data', testGetSingleSelectFieldData);
};
export const TestSwitchFromSingleSelectToNumber = () => {
async function testSwitchFromSingleSelectToNumber() {
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));
await typeOptionController.switchToField(FieldType.Number);
// Check the number type option
const numberTypeOptionContext = makeNumberTypeOptionContext(typeOptionController);
const numberTypeOption: NumberTypeOptionPB = await numberTypeOptionContext
.getTypeOption()
.then((result) => result.unwrap());
const format: NumberFormat = numberTypeOption.format;
if (format !== NumberFormat.Num) {
throw Error('The default format should be number');
}
await databaseController.dispose();
}
return TestButton('Test switch from single-select to number column', testSwitchFromSingleSelectToNumber);
};
export const TestSwitchFromMultiSelectToText = () => {
async function testSwitchFromMultiSelectToRichText() {
const view = await createTestDatabaseView(ViewLayoutTypePB.Grid);
const databaseController = await openTestDatabase(view.id);
await databaseController.open().then((result) => result.unwrap());
// Create multi-select field
const typeOptionController = new TypeOptionController(view.id, None, FieldType.MultiSelect);
await typeOptionController.initialize();
// Insert options to first row
const row = databaseController.databaseViewCache.rowInfos[0];
const multiSelectField = typeOptionController.getFieldInfo();
// const multiSelectField = findFirstFieldInfoWithFieldType(row, FieldType.MultiSelect).unwrap();
const selectOptionCellController = await makeMultiSelectCellController(
multiSelectField.field.id,
row,
databaseController
).then((result) => result.unwrap());
const backendSvc = new SelectOptionCellBackendService(selectOptionCellController.cellIdentifier);
await backendSvc.createOption({ name: 'A' });
await backendSvc.createOption({ name: 'B' });
await backendSvc.createOption({ name: 'C' });
const selectOptionCellData = await selectOptionCellController.getCellData().then((result) => result.unwrap());
if (selectOptionCellData.options.length !== 3) {
throw Error('The options should equal to 3');
}
if (selectOptionCellData.select_options.length !== 3) {
throw Error('The selected options should equal to 3');
}
await selectOptionCellController.dispose();
// Switch to RichText field type
await typeOptionController.switchToField(FieldType.RichText).then((result) => result.unwrap());
if (typeOptionController.fieldType !== FieldType.RichText) {
throw Error('The field type should be text');
}
const textCellController = await makeTextCellController(multiSelectField.field.id, row, databaseController).then(
(result) => result.unwrap()
);
const cellContent = await textCellController.getCellData();
if (cellContent.unwrap() !== 'A,B,C') {
throw Error('The cell content should be A,B,C, but receive: ' + cellContent.unwrap());
}
await databaseController.dispose();
}
return TestButton('Test switch from multi-select to text column', testSwitchFromMultiSelectToRichText);
};
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();
const newName = 'hello world';
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);
};
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);
await databaseController.dispose();
}
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.
// The fieldInfos[0] is the primary field by default, we can't delete it.
// So let choose the second fieldInfo.
const fieldInfo = databaseController.fieldController.fieldInfos[1];
const controller = new TypeOptionController(view.id, Some(fieldInfo));
await controller.initialize();
await assertNumberOfFields(view.id, 3);
await controller.deleteField();
await assertNumberOfFields(view.id, 2);
await databaseController.dispose();
}
return TestButton('Test delete a new column', testDeleteField);
};
const TestButton = (title: string, onClick: () => void) => {
return (
<React.Fragment>
<div>
<button className='rounded-md bg-gray-300 p-4' type='button' onClick={() => onClick()}>
{title}
</button>
</div>
</React.Fragment>
);
};

View File

@ -1,4 +1,10 @@
import { FieldType, ViewLayoutTypePB, ViewPB, WorkspaceSettingPB } from '../../../services/backend';
import {
FieldType,
SingleSelectTypeOptionPB,
ViewLayoutTypePB,
ViewPB,
WorkspaceSettingPB,
} from '../../../services/backend';
import { FolderEventReadCurrentWorkspace } from '../../../services/backend/events/flowy-folder';
import { AppBackendService } from '../../stores/effects/folder/app/app_bd_svc';
import { DatabaseController } from '../../stores/effects/database/database_controller';
@ -14,6 +20,10 @@ import {
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';
import { FieldInfo } from '../../stores/effects/database/field/field_controller';
import { TypeOptionController } from '../../stores/effects/database/field/type_option/type_option_controller';
import { makeSingleSelectTypeOptionContext } from '../../stores/effects/database/field/type_option/type_option_context';
import { SelectOptionBackendService } from '../../stores/effects/database/cell/select_option_bd_svc';
// Create a database view for specific layout type
// Do not use it production code. Just for testing
@ -168,3 +178,36 @@ export async function assertNumberOfRows(viewId: string, expected: number) {
throw Error('Expect number of rows:' + expected + 'but receive:' + databasePB.rows.length);
}
}
export async function assertNumberOfRowsInGroup(viewId: string, groupId: string, expected: number) {
const svc = new DatabaseBackendService(viewId);
await svc.openDatabase();
const group = await svc.getGroup(groupId).then((result) => result.unwrap());
if (group.rows.length !== expected) {
throw Error('Expect number of rows in group:' + expected + 'but receive:' + group.rows.length);
}
}
export async function createSingleSelectOptions(viewId: string, fieldInfo: FieldInfo, optionNames: string[]) {
assert(fieldInfo.field.field_type === FieldType.SingleSelect, 'Only work on single select');
const typeOptionController = new TypeOptionController(viewId, Some(fieldInfo));
const singleSelectTypeOptionContext = makeSingleSelectTypeOptionContext(typeOptionController);
const singleSelectTypeOptionPB: SingleSelectTypeOptionPB = await singleSelectTypeOptionContext
.getTypeOption()
.then((result) => result.unwrap());
const backendSvc = new SelectOptionBackendService(viewId, fieldInfo.field.id);
for (const optionName of optionNames) {
const option = await backendSvc.createOption({ name: optionName }).then((result) => result.unwrap());
singleSelectTypeOptionPB.options.splice(0, 0, option);
}
await singleSelectTypeOptionContext.setTypeOption(singleSelectTypeOptionPB);
return singleSelectTypeOptionContext;
}
export function assert(condition: boolean, msg?: string) {
if (!condition) {
throw Error(msg);
}
}

View File

@ -1,5 +1,6 @@
import React from 'react';
import {
RunAllGridTests,
TestCreateGrid,
TestCreateNewField,
TestCreateRow,
@ -12,12 +13,21 @@ import {
TestSwitchFromMultiSelectToText,
TestSwitchFromSingleSelectToNumber,
} from './TestGrid';
import {
TestCreateKanbanBoard,
TestCreateKanbanBoardColumn,
TestCreateKanbanBoardRowInNoStatusGroup,
TestAllKanbanTests,
TestMoveKanbanBoardColumn,
TestMoveKanbanBoardRow,
} from './TestGroup';
export const TestAPI = () => {
return (
<React.Fragment>
<ul className='m-6, space-y-2'>
{/*<TestApiButton></TestApiButton>*/}
{/*<tests></tests>*/}
<RunAllGridTests></RunAllGridTests>
<TestCreateGrid></TestCreateGrid>
<TestCreateRow></TestCreateRow>
<TestDeleteRow></TestDeleteRow>
@ -29,6 +39,13 @@ export const TestAPI = () => {
<TestDeleteField></TestDeleteField>
<TestSwitchFromSingleSelectToNumber></TestSwitchFromSingleSelectToNumber>
<TestSwitchFromMultiSelectToText></TestSwitchFromMultiSelectToText>
{/*kanban board */}
<TestAllKanbanTests></TestAllKanbanTests>
<TestCreateKanbanBoard></TestCreateKanbanBoard>
<TestCreateKanbanBoardRowInNoStatusGroup></TestCreateKanbanBoardRowInNoStatusGroup>
<TestMoveKanbanBoardRow></TestMoveKanbanBoardRow>
<TestMoveKanbanBoardColumn></TestMoveKanbanBoardColumn>
<TestCreateKanbanBoardColumn></TestCreateKanbanBoardColumn>
</ul>
</React.Fragment>
);

View File

@ -0,0 +1,349 @@
import React from 'react';
import {
FieldType,
NumberFormat,
NumberTypeOptionPB,
SelectOptionCellDataPB,
ViewLayoutTypePB,
} from '../../../services/backend';
import { Log } from '../../utils/log';
import {
assertFieldName,
assertNumberOfFields,
assertNumberOfRows,
assertTextCell,
createSingleSelectOptions,
createTestDatabaseView,
editTextCell,
findFirstFieldInfoWithFieldType,
makeMultiSelectCellController,
makeSingleSelectCellController,
makeTextCellController,
openTestDatabase,
} from './DatabaseTestHelper';
import { 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 { RowBackendService } from '../../stores/effects/database/row/row_bd_svc';
import { makeNumberTypeOptionContext } from '../../stores/effects/database/field/type_option/type_option_context';
export const RunAllGridTests = () => {
async function run() {
await createBuildInGrid();
await testEditGridRow();
await testCreateRow();
await testDeleteRow();
await testCreateOptionInCell();
await testGetSingleSelectFieldData();
await testSwitchFromSingleSelectToNumber();
await testSwitchFromMultiSelectToRichText();
await testEditField();
await testCreateNewField();
await testDeleteField();
}
return (
<React.Fragment>
<div>
<button className='rounded-md bg-red-400 p-4' type='button' onClick={() => run()}>
Run all grid tests
</button>
</div>
</React.Fragment>
);
};
async function createBuildInGrid() {
const view = await createTestDatabaseView(ViewLayoutTypePB.Grid);
const databaseController = await openTestDatabase(view.id);
databaseController.subscribe({
onViewChanged: (databasePB) => {
Log.debug('Did receive database:' + databasePB);
},
// onRowsChanged: async (rows) => {
// if (rows.length !== 3) {
// throw Error('Expected number of rows is 3, but receive ' + rows.length);
// }
// },
onFieldsChanged: (fields) => {
if (fields.length !== 3) {
throw Error('Expected number of fields is 3, but receive ' + fields.length);
}
},
});
await databaseController.open().then((result) => result.unwrap());
await databaseController.dispose();
}
async function testEditGridRow() {
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()) {
const cellContent = index.toString();
const fieldInfo = findFirstFieldInfoWithFieldType(row, FieldType.RichText).unwrap();
await editTextCell(fieldInfo.field.id, row, databaseController, cellContent);
await assertTextCell(fieldInfo.field.id, row, databaseController, cellContent);
}
}
async function testCreateRow() {
const view = await createTestDatabaseView(ViewLayoutTypePB.Grid);
const databaseController = await openTestDatabase(view.id);
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();
}
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();
}
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 fieldInfo = findFirstFieldInfoWithFieldType(row, FieldType.SingleSelect).unwrap();
const cellController = await makeSingleSelectCellController(fieldInfo.field.id, row, databaseController).then(
(result) => result.unwrap()
);
await cellController.subscribeChanged({
onCellChanged: (value) => {
if (value.some) {
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();
}
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
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const singleSelect = databaseController.fieldController.fieldInfos.find(
(fieldInfo) => fieldInfo.field.field_type === FieldType.SingleSelect
)!;
// Create options
const singleSelectTypeOptionContext = await createSingleSelectOptions(view.id, singleSelect, [
'Task 1',
'Task 2',
'Task 3',
]);
// Read options
const options = await singleSelectTypeOptionContext.getTypeOption().then((result) => result.unwrap());
console.log(options);
await databaseController.dispose();
}
async function testSwitchFromSingleSelectToNumber() {
const view = await createTestDatabaseView(ViewLayoutTypePB.Grid);
const databaseController = await openTestDatabase(view.id);
await databaseController.open().then((result) => result.unwrap());
// Find the single select column
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const singleSelect = databaseController.fieldController.fieldInfos.find(
(fieldInfo) => fieldInfo.field.field_type === FieldType.SingleSelect
)!;
const typeOptionController = new TypeOptionController(view.id, Some(singleSelect));
await typeOptionController.switchToField(FieldType.Number);
// Check the number type option
const numberTypeOptionContext = makeNumberTypeOptionContext(typeOptionController);
const numberTypeOption: NumberTypeOptionPB = await numberTypeOptionContext
.getTypeOption()
.then((result) => result.unwrap());
const format: NumberFormat = numberTypeOption.format;
if (format !== NumberFormat.Num) {
throw Error('The default format should be number');
}
await databaseController.dispose();
}
async function testSwitchFromMultiSelectToRichText() {
const view = await createTestDatabaseView(ViewLayoutTypePB.Grid);
const databaseController = await openTestDatabase(view.id);
await databaseController.open().then((result) => result.unwrap());
// Create multi-select field
const typeOptionController = new TypeOptionController(view.id, None, FieldType.MultiSelect);
await typeOptionController.initialize();
// Insert options to first row
const row = databaseController.databaseViewCache.rowInfos[0];
const multiSelectField = typeOptionController.getFieldInfo();
// const multiSelectField = findFirstFieldInfoWithFieldType(row, FieldType.MultiSelect).unwrap();
const selectOptionCellController = await makeMultiSelectCellController(
multiSelectField.field.id,
row,
databaseController
).then((result) => result.unwrap());
const backendSvc = new SelectOptionCellBackendService(selectOptionCellController.cellIdentifier);
await backendSvc.createOption({ name: 'A' });
await backendSvc.createOption({ name: 'B' });
await backendSvc.createOption({ name: 'C' });
const selectOptionCellData = await selectOptionCellController.getCellData().then((result) => result.unwrap());
if (selectOptionCellData.options.length !== 3) {
throw Error('The options should equal to 3');
}
if (selectOptionCellData.select_options.length !== 3) {
throw Error('The selected options should equal to 3');
}
await selectOptionCellController.dispose();
// Switch to RichText field type
await typeOptionController.switchToField(FieldType.RichText).then((result) => result.unwrap());
if (typeOptionController.fieldType !== FieldType.RichText) {
throw Error('The field type should be text');
}
const textCellController = await makeTextCellController(multiSelectField.field.id, row, databaseController).then(
(result) => result.unwrap()
);
const cellContent = await textCellController.getCellData();
if (cellContent.unwrap() !== 'A,B,C') {
throw Error('The cell content should be A,B,C, but receive: ' + cellContent.unwrap());
}
await databaseController.dispose();
}
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();
const newName = 'hello world';
await controller.setFieldName(newName);
await new Promise((resolve) => setTimeout(resolve, 200));
await assertFieldName(view.id, firstFieldInfo.field.id, firstFieldInfo.field.field_type, newName);
await databaseController.dispose();
}
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);
await databaseController.dispose();
}
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.
// The fieldInfos[0] is the primary field by default, we can't delete it.
// So let choose the second fieldInfo.
const fieldInfo = databaseController.fieldController.fieldInfos[1];
const controller = new TypeOptionController(view.id, Some(fieldInfo));
await controller.initialize();
await assertNumberOfFields(view.id, 3);
await controller.deleteField();
await assertNumberOfFields(view.id, 2);
await databaseController.dispose();
}
export const TestCreateGrid = () => {
return TestButton('Test create build-in grid', createBuildInGrid);
};
export const TestEditCell = () => {
return TestButton('Test editing cell', testEditGridRow);
};
export const TestCreateRow = () => {
return TestButton('Test create row', testCreateRow);
};
export const TestDeleteRow = () => {
return TestButton('Test delete row', testDeleteRow);
};
export const TestCreateSelectOptionInCell = () => {
return TestButton('Test create a select option in cell', testCreateOptionInCell);
};
export const TestGetSingleSelectFieldData = () => {
return TestButton('Test get single-select column data', testGetSingleSelectFieldData);
};
export const TestSwitchFromSingleSelectToNumber = () => {
return TestButton('Test switch from single-select to number column', testSwitchFromSingleSelectToNumber);
};
export const TestSwitchFromMultiSelectToText = () => {
return TestButton('Test switch from multi-select to text column', testSwitchFromMultiSelectToRichText);
};
export const TestEditField = () => {
return TestButton('Test edit the column name', testEditField);
};
export const TestCreateNewField = () => {
return TestButton('Test create a new column', testCreateNewField);
};
export const TestDeleteField = () => {
return TestButton('Test delete a new column', testDeleteField);
};
export const TestButton = (title: string, onClick: () => void) => {
return (
<React.Fragment>
<div>
<button className='rounded-md bg-blue-400 p-4' type='button' onClick={() => onClick()}>
{title}
</button>
</div>
</React.Fragment>
);
};

View File

@ -0,0 +1,150 @@
import {
assert,
assertNumberOfRowsInGroup,
createSingleSelectOptions,
createTestDatabaseView,
openTestDatabase,
} from './DatabaseTestHelper';
import { FieldType, ViewLayoutTypePB } from '../../../services/backend';
import React from 'react';
export const TestAllKanbanTests = () => {
async function run() {
await createBuildInBoard();
await createKanbanBoardRow();
await moveKanbanBoardRow();
await createKanbanBoardColumn();
await createColumnInBoard();
}
return (
<React.Fragment>
<div>
<button className='rounded-md bg-red-400 p-4' type='button' onClick={() => run()}>
Run all kanban board tests
</button>
</div>
</React.Fragment>
);
};
async function createBuildInBoard() {
const view = await createTestDatabaseView(ViewLayoutTypePB.Board);
const databaseController = await openTestDatabase(view.id);
databaseController.subscribe({
onGroupByField: (groups) => {
console.log(groups);
if (groups.length !== 4) {
throw Error('The build-in board should have 4 groups');
}
assert(groups[0].rows.length === 0, 'The no status group should have 0 rows');
assert(groups[1].rows.length === 3, 'The first group should have 3 rows');
assert(groups[2].rows.length === 0, 'The second group should have 0 rows');
assert(groups[3].rows.length === 0, 'The third group should have 0 rows');
},
});
await databaseController.open().then((result) => result.unwrap());
await databaseController.dispose();
}
async function createKanbanBoardRow() {
const view = await createTestDatabaseView(ViewLayoutTypePB.Board);
const databaseController = await openTestDatabase(view.id);
await databaseController.open().then((result) => result.unwrap());
// Create row in no status group
const noStatusGroup = databaseController.groups.getValue()[0];
await noStatusGroup.createRow().then((result) => result.unwrap());
await assertNumberOfRowsInGroup(view.id, noStatusGroup.groupId, 1);
await databaseController.dispose();
}
async function moveKanbanBoardRow() {
const view = await createTestDatabaseView(ViewLayoutTypePB.Board);
const databaseController = await openTestDatabase(view.id);
await databaseController.open().then((result) => result.unwrap());
// Create row in no status group
const firstGroup = databaseController.groups.getValue()[1];
const secondGroup = databaseController.groups.getValue()[2];
const row = firstGroup.rowAtIndex(0).unwrap();
await databaseController.moveRow(row.id, secondGroup.groupId);
assert(firstGroup.rows.length === 2);
await assertNumberOfRowsInGroup(view.id, firstGroup.groupId, 2);
assert(secondGroup.rows.length === 1);
await assertNumberOfRowsInGroup(view.id, secondGroup.groupId, 1);
await databaseController.dispose();
}
async function createKanbanBoardColumn() {
const view = await createTestDatabaseView(ViewLayoutTypePB.Board);
const databaseController = await openTestDatabase(view.id);
await databaseController.open().then((result) => result.unwrap());
// Create row in no status group
const firstGroup = databaseController.groups.getValue()[1];
const secondGroup = databaseController.groups.getValue()[2];
await databaseController.moveGroup(firstGroup.groupId, secondGroup.groupId);
assert(databaseController.groups.getValue()[1].groupId === secondGroup.groupId);
assert(databaseController.groups.getValue()[2].groupId === firstGroup.groupId);
await databaseController.dispose();
}
async function createColumnInBoard() {
const view = await createTestDatabaseView(ViewLayoutTypePB.Board);
const databaseController = await openTestDatabase(view.id);
await databaseController.open().then((result) => result.unwrap());
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const singleSelect = databaseController.fieldController.fieldInfos.find(
(fieldInfo) => fieldInfo.field.field_type === FieldType.SingleSelect
)!;
// Create a option which will cause creating a new group
const name = 'New column';
await createSingleSelectOptions(view.id, singleSelect, [name]);
// Wait the backend posting the notification to update the groups
await new Promise((resolve) => setTimeout(resolve, 200));
assert(databaseController.groups.value.length === 5, 'expect number of groups is 5');
assert(databaseController.groups.value[4].name === name, 'expect the last group name is ' + name);
await databaseController.dispose();
}
export const TestCreateKanbanBoard = () => {
return TestButton('Test create build-in board', createBuildInBoard);
};
export const TestCreateKanbanBoardRowInNoStatusGroup = () => {
return TestButton('Test create row in build-in kanban board', createKanbanBoardRow);
};
export const TestMoveKanbanBoardRow = () => {
return TestButton('Test move row in build-in kanban board', moveKanbanBoardRow);
};
export const TestMoveKanbanBoardColumn = () => {
return TestButton('Test move column in build-in kanban board', createKanbanBoardColumn);
};
export const TestCreateKanbanBoardColumn = () => {
return TestButton('Test create column in build-in kanban board', createColumnInBoard);
};
export const TestButton = (title: string, onClick: () => void) => {
return (
<React.Fragment>
<div>
<button className='rounded-md bg-yellow-200 p-4' type='button' onClick={() => onClick()}>
{title}
</button>
</div>
</React.Fragment>
);
};

View File

@ -1,7 +1,16 @@
import {
CreateBoardCardPayloadPB,
DatabaseEventCreateBoardCard,
DatabaseEventCreateRow,
DatabaseEventGetDatabase,
DatabaseEventGetFields,
DatabaseEventGetGroup,
DatabaseEventGetGroups,
DatabaseEventMoveGroup,
DatabaseEventMoveGroupRow,
DatabaseGroupIdPB,
MoveGroupPayloadPB,
MoveGroupRowPayloadPB,
} from '../../../../services/backend/events/flowy-database';
import {
GetFieldPayloadPB,
@ -37,6 +46,31 @@ export class DatabaseBackendService {
return DatabaseEventCreateRow(payload);
};
createGroupRow = async (groupId: string, startRowId?: string) => {
const payload = CreateBoardCardPayloadPB.fromObject({ view_id: this.viewId, group_id: groupId });
if (startRowId !== undefined) {
payload.start_row_id = startRowId;
}
return DatabaseEventCreateBoardCard(payload);
};
moveRow = (rowId: string, groupId?: string) => {
const payload = MoveGroupRowPayloadPB.fromObject({ view_id: this.viewId, from_row_id: rowId });
if (groupId !== undefined) {
payload.to_group_id = groupId;
}
return DatabaseEventMoveGroupRow(payload);
};
moveGroup = (fromGroupId: string, toGroupId: string) => {
const payload = MoveGroupPayloadPB.fromObject({
view_id: this.viewId,
from_group_id: fromGroupId,
to_group_id: toGroupId,
});
return DatabaseEventMoveGroup(payload);
};
getFields = async (fieldIds?: FieldIdPB[]) => {
const payload = GetFieldPayloadPB.fromObject({ view_id: this.viewId });
@ -46,4 +80,14 @@ export class DatabaseBackendService {
return DatabaseEventGetFields(payload).then((result) => result.map((value) => value.items));
};
getGroup = (groupId: string) => {
const payload = DatabaseGroupIdPB.fromObject({ view_id: this.viewId, group_id: groupId });
return DatabaseEventGetGroup(payload);
};
loadGroups = () => {
const payload = DatabaseViewIdPB.fromObject({ value: this.viewId });
return DatabaseEventGetGroups(payload);
};
}

View File

@ -1,55 +1,150 @@
import { DatabaseBackendService } from './database_bd_svc';
import { FieldController, FieldInfo } from './field/field_controller';
import { DatabaseViewCache } from './view/database_view_cache';
import { DatabasePB } from '../../../../services/backend';
import { DatabasePB, GroupPB } from '../../../../services/backend';
import { RowChangedReason, RowInfo } from './row/row_cache';
import { Err, Ok } from 'ts-results';
import { Err } from 'ts-results';
import { DatabaseGroupController } from './group/group_controller';
import { BehaviorSubject } from 'rxjs';
import { DatabaseGroupObserver } from './group/group_observer';
import { Log } from '../../../utils/log';
export type SubscribeCallbacks = {
export type DatabaseSubscriberCallbacks = {
onViewChanged?: (data: DatabasePB) => void;
onRowsChanged?: (rowInfos: readonly RowInfo[], reason: RowChangedReason) => void;
onFieldsChanged?: (fieldInfos: readonly FieldInfo[]) => void;
onGroupByField?: (groups: GroupPB[]) => void;
onNumOfGroupChanged?: {
onUpdateGroup: (value: GroupPB[]) => void;
onDeleteGroup: (value: GroupPB[]) => void;
onInsertGroup: (value: GroupPB[]) => void;
};
};
export class DatabaseController {
private backendService: DatabaseBackendService;
private readonly backendService: DatabaseBackendService;
fieldController: FieldController;
databaseViewCache: DatabaseViewCache;
private _callback?: SubscribeCallbacks;
private _callback?: DatabaseSubscriberCallbacks;
public groups: BehaviorSubject<DatabaseGroupController[]>;
private groupsObserver: DatabaseGroupObserver;
constructor(public readonly viewId: string) {
this.backendService = new DatabaseBackendService(viewId);
this.fieldController = new FieldController(viewId);
this.databaseViewCache = new DatabaseViewCache(viewId, this.fieldController);
this.groups = new BehaviorSubject<DatabaseGroupController[]>([]);
this.groupsObserver = new DatabaseGroupObserver(viewId);
}
subscribe = (callbacks: SubscribeCallbacks) => {
subscribe = (callbacks: DatabaseSubscriberCallbacks) => {
this._callback = callbacks;
this.fieldController.subscribeOnNumOfFieldsChanged(callbacks.onFieldsChanged);
this.databaseViewCache.getRowCache().subscribeOnRowsChanged((reason) => {
this._callback?.onRowsChanged?.(this.databaseViewCache.rowInfos, reason);
this.fieldController.subscribe({ onNumOfFieldsChanged: callbacks.onFieldsChanged });
this.databaseViewCache.getRowCache().subscribe({
onRowsChanged: (reason) => {
this._callback?.onRowsChanged?.(this.databaseViewCache.rowInfos, reason);
},
});
};
open = async () => {
const result = await this.backendService.openDatabase();
if (result.ok) {
const database: DatabasePB = result.val;
this._callback?.onViewChanged?.(database);
const openDatabaseResult = await this.backendService.openDatabase();
if (openDatabaseResult.ok) {
const database: DatabasePB = openDatabaseResult.val;
await this.databaseViewCache.initialize();
await this.fieldController.initialize();
// subscriptions
await this.subscribeOnGroupsChanged();
// load database initial data
await this.fieldController.loadFields(database.fields);
await this.databaseViewCache.listenOnRowsChanged();
await this.fieldController.listenOnFieldChanges();
const loadGroupResult = await this.loadGroup();
this.databaseViewCache.initializeWithRows(database.rows);
return Ok.EMPTY;
this._callback?.onViewChanged?.(database);
return loadGroupResult;
} else {
return Err(result.val);
return Err(openDatabaseResult.val);
}
};
createRow = async () => {
createRow = () => {
return this.backendService.createRow();
};
moveRow = (rowId: string, groupId: string) => {
return this.backendService.moveRow(rowId, groupId);
};
moveGroup = (fromGroupId: string, toGroupId: string) => {
return this.backendService.moveGroup(fromGroupId, toGroupId);
};
private loadGroup = async () => {
const result = await this.backendService.loadGroups();
if (result.ok) {
const groups = result.val.items;
await this.initialGroups(groups);
}
return result;
};
private initialGroups = async (groups: GroupPB[]) => {
this.groups.getValue().forEach((controller) => {
void controller.dispose();
});
const controllers: DatabaseGroupController[] = [];
for (const groupPB of groups) {
const controller = new DatabaseGroupController(groupPB, this.backendService);
await controller.initialize();
controllers.push(controller);
}
this.groups.next(controllers);
this.groups.value;
};
private subscribeOnGroupsChanged = async () => {
await this.groupsObserver.subscribe({
onGroupBy: async (result) => {
if (result.ok) {
await this.initialGroups(result.val);
}
},
onGroupChangeset: (result) => {
if (result.err) {
Log.error(result.val);
return;
}
const changeset = result.val;
let existControllers = [...this.groups.getValue()];
for (const deleteId of changeset.deleted_groups) {
existControllers = existControllers.filter((c) => c.groupId !== deleteId);
}
for (const update of changeset.update_groups) {
const index = existControllers.findIndex((c) => c.groupId === update.group_id);
if (index !== -1) {
existControllers[index].updateGroup(update);
}
}
for (const insert of changeset.inserted_groups) {
const controller = new DatabaseGroupController(insert.group, this.backendService);
if (insert.index > existControllers.length) {
existControllers.push(controller);
} else {
existControllers.splice(insert.index, 0, controller);
}
}
this.groups.next(existControllers);
},
});
};
dispose = async () => {
await this.backendService.closeDatabase();
await this.fieldController.dispose();

View File

@ -6,17 +6,17 @@ import { ChangeNotifier } from '../../../../utils/change_notifier';
export class FieldController {
private backendService: DatabaseBackendService;
private numOfFieldsObserver: DatabaseFieldChangesetObserver;
private fieldChangesetObserver: DatabaseFieldChangesetObserver;
private numOfFieldsNotifier = new NumOfFieldsNotifier([]);
constructor(public readonly viewId: string) {
this.backendService = new DatabaseBackendService(viewId);
this.numOfFieldsObserver = new DatabaseFieldChangesetObserver(viewId);
this.fieldChangesetObserver = new DatabaseFieldChangesetObserver(viewId);
}
dispose = async () => {
this.numOfFieldsNotifier.unsubscribe();
await this.numOfFieldsObserver.unsubscribe();
await this.fieldChangesetObserver.unsubscribe();
};
get fieldInfos(): readonly FieldInfo[] {
@ -36,14 +36,14 @@ export class FieldController {
}
};
subscribeOnNumOfFieldsChanged = (callback?: (fieldInfos: readonly FieldInfo[]) => void) => {
return this.numOfFieldsNotifier.observer.subscribe((fieldInfos) => {
callback?.(fieldInfos);
subscribe = (callbacks: { onNumOfFieldsChanged?: (fieldInfos: readonly FieldInfo[]) => void}) => {
this.numOfFieldsNotifier.observer.subscribe((fieldInfos) => {
callbacks.onNumOfFieldsChanged?.(fieldInfos);
});
};
listenOnFieldChanges = async () => {
await this.numOfFieldsObserver.subscribe({
initialize = async () => {
await this.fieldChangesetObserver.subscribe({
onFieldsChanged: (result) => {
if (result.ok) {
const changeset = result.val;

View File

@ -3,16 +3,15 @@ import { DatabaseNotification, DatabaseFieldChangesetPB, FlowyError, FieldPB } f
import { ChangeNotifier } from '../../../../utils/change_notifier';
import { DatabaseNotificationObserver } from '../notifications/observer';
type UpdateFieldNotifiedValue = Result<DatabaseFieldChangesetPB, FlowyError>;
export type DatabaseNotificationCallback = (value: UpdateFieldNotifiedValue) => void;
export type FieldChangesetSubscribeCallback = (value: Result<DatabaseFieldChangesetPB, FlowyError>) => void;
export class DatabaseFieldChangesetObserver {
private notifier?: ChangeNotifier<UpdateFieldNotifiedValue>;
private notifier?: ChangeNotifier<Result<DatabaseFieldChangesetPB, FlowyError>>;
private listener?: DatabaseNotificationObserver;
constructor(public readonly viewId: string) {}
subscribe = async (callbacks: { onFieldsChanged: DatabaseNotificationCallback }) => {
subscribe = async (callbacks: { onFieldsChanged: FieldChangesetSubscribeCallback }) => {
this.notifier = new ChangeNotifier();
this.notifier?.observer.subscribe(callbacks.onFieldsChanged);
@ -41,16 +40,15 @@ export class DatabaseFieldChangesetObserver {
};
}
type FieldNotifiedValue = Result<FieldPB, FlowyError>;
export type FieldNotificationCallback = (value: FieldNotifiedValue) => void;
export type FieldSubscribeCallback = (value: Result<FieldPB, FlowyError>) => void;
export class DatabaseFieldObserver {
private _notifier?: ChangeNotifier<FieldNotifiedValue>;
private _notifier?: ChangeNotifier<Result<FieldPB, FlowyError>>;
private _listener?: DatabaseNotificationObserver;
constructor(public readonly fieldId: string) {}
subscribe = async (callbacks: { onFieldChanged: FieldNotificationCallback }) => {
subscribe = async (callbacks: { onFieldChanged: FieldSubscribeCallback }) => {
this._notifier = new ChangeNotifier();
this._notifier?.observer.subscribe(callbacks.onFieldChanged);

View File

@ -0,0 +1,149 @@
import {
DatabaseNotification,
FlowyError,
GroupPB,
GroupRowsNotificationPB,
RowPB,
} from '../../../../../services/backend';
import { ChangeNotifier } from '../../../../utils/change_notifier';
import { None, Ok, Option, Result, Some } from 'ts-results';
import { DatabaseNotificationObserver } from '../notifications/observer';
import { Log } from '../../../../utils/log';
import { DatabaseBackendService } from '../database_bd_svc';
export type GroupDataCallbacks = {
onRemoveRow: (groupId: string, rowId: string) => void;
onInsertRow: (groupId: string, row: RowPB, index?: number) => void;
onUpdateRow: (groupId: string, row: RowPB) => void;
onCreateRow: (groupId: string, row: RowPB) => void;
};
export class DatabaseGroupController {
private dataObserver: GroupDataObserver;
private callbacks?: GroupDataCallbacks;
constructor(private group: GroupPB, private databaseBackendSvc: DatabaseBackendService) {
this.dataObserver = new GroupDataObserver(group.group_id);
}
get groupId() {
return this.group.group_id;
}
get rows() {
return this.group.rows;
}
get name() {
return this.group.desc;
}
updateGroup = (group: GroupPB) => {
this.group = group;
};
rowAtIndex = (index: number): Option<RowPB> => {
if (this.group.rows.length < index) {
return None;
}
return Some(this.group.rows[index]);
};
initialize = async () => {
await this.dataObserver.subscribe({
onRowsChanged: (result) => {
if (result.ok) {
const changeset = result.val;
// Delete
changeset.deleted_rows.forEach((deletedRowId) => {
this.group.rows = this.group.rows.filter((row) => row.id !== deletedRowId);
this.callbacks?.onRemoveRow(this.group.group_id, deletedRowId);
});
// Insert
changeset.inserted_rows.forEach((insertedRow) => {
let index: number | undefined = insertedRow.index;
if (insertedRow.has_index && this.group.rows.length > insertedRow.index) {
this.group.rows.splice(index, 0, insertedRow.row);
} else {
index = undefined;
this.group.rows.push(insertedRow.row);
}
if (insertedRow.is_new) {
this.callbacks?.onCreateRow(this.group.group_id, insertedRow.row);
} else {
this.callbacks?.onInsertRow(this.group.group_id, insertedRow.row, index);
}
});
// Update
changeset.updated_rows.forEach((updatedRow) => {
const index = this.group.rows.findIndex((row) => row.id === updatedRow.id);
if (index !== -1) {
this.group.rows[index] = updatedRow;
this.callbacks?.onUpdateRow(this.group.group_id, updatedRow);
}
});
} else {
Log.error(result.val);
}
},
});
};
createRow = async () => {
return this.databaseBackendSvc.createGroupRow(this.group.group_id);
};
subscribe = (callbacks: GroupDataCallbacks) => {
this.callbacks = callbacks;
};
unsubscribe = () => {
this.callbacks = undefined;
};
dispose = async () => {
await this.dataObserver.unsubscribe();
this.callbacks = undefined;
};
}
type GroupRowsSubscribeCallback = (value: Result<GroupRowsNotificationPB, FlowyError>) => void;
class GroupDataObserver {
private notifier?: ChangeNotifier<Result<GroupRowsNotificationPB, FlowyError>>;
private listener?: DatabaseNotificationObserver;
constructor(public readonly groupId: string) {}
subscribe = async (callbacks: { onRowsChanged: GroupRowsSubscribeCallback }) => {
this.notifier = new ChangeNotifier();
this.notifier?.observer.subscribe(callbacks.onRowsChanged);
this.listener = new DatabaseNotificationObserver({
id: this.groupId,
parserHandler: (notification, result) => {
switch (notification) {
case DatabaseNotification.DidUpdateGroupRow:
if (result.ok) {
this.notifier?.notify(Ok(GroupRowsNotificationPB.deserializeBinary(result.val)));
} else {
this.notifier?.notify(result);
}
return;
default:
break;
}
},
});
await this.listener.start();
};
unsubscribe = async () => {
await this.listener?.stop();
this.notifier?.unsubscribe();
};
}

View File

@ -0,0 +1,58 @@
import { ChangeNotifier } from '../../../../utils/change_notifier';
import { Ok, Result } from 'ts-results';
import { DatabaseNotification, FlowyError, GroupChangesetPB, GroupPB } from '../../../../../services/backend';
import { DatabaseNotificationObserver } from '../notifications/observer';
export type GroupByFieldCallback = (value: Result<GroupPB[], FlowyError>) => void;
export type GroupChangesetSubscribeCallback = (value: Result<GroupChangesetPB, FlowyError>) => void;
export class DatabaseGroupObserver {
private groupByNotifier?: ChangeNotifier<Result<GroupPB[], FlowyError>>;
private groupChangesetNotifier?: ChangeNotifier<Result<GroupChangesetPB, FlowyError>>;
private listener?: DatabaseNotificationObserver;
constructor(public readonly viewId: string) {}
subscribe = async (callbacks: {
onGroupBy: GroupByFieldCallback;
onGroupChangeset: GroupChangesetSubscribeCallback;
}) => {
this.groupByNotifier = new ChangeNotifier();
this.groupByNotifier?.observer.subscribe(callbacks.onGroupBy);
this.groupChangesetNotifier = new ChangeNotifier();
this.groupChangesetNotifier?.observer.subscribe(callbacks.onGroupChangeset);
this.listener = new DatabaseNotificationObserver({
id: this.viewId,
parserHandler: (notification, result) => {
switch (notification) {
case DatabaseNotification.DidGroupByField:
if (result.ok) {
this.groupByNotifier?.notify(Ok(GroupChangesetPB.deserializeBinary(result.val).initial_groups));
} else {
this.groupByNotifier?.notify(result);
}
break;
case DatabaseNotification.DidUpdateGroups:
if (result.ok) {
this.groupChangesetNotifier?.notify(Ok(GroupChangesetPB.deserializeBinary(result.val)));
} else {
this.groupChangesetNotifier?.notify(result);
}
break;
default:
break;
}
},
});
await this.listener.start();
};
unsubscribe = async () => {
this.groupByNotifier?.unsubscribe();
this.groupChangesetNotifier?.unsubscribe();
await this.listener?.stop();
};
}

View File

@ -53,12 +53,14 @@ export class RowCache {
}
};
subscribeOnRowsChanged = (callback: (reason: RowChangedReason, cellMap?: Map<string, CellIdentifier>) => void) => {
subscribe = (callbacks: {
onRowsChanged: (reason: RowChangedReason, cellMap?: Map<string, CellIdentifier>) => void;
}) => {
return this.notifier.observer.subscribe((change) => {
if (change.rowId !== undefined) {
callback(change.reason, this._toCellMap(change.rowId, this.getFieldInfos()));
callbacks.onRowsChanged(change.reason, this._toCellMap(change.rowId, this.getFieldInfos()));
} else {
callback(change.reason);
callbacks.onRowsChanged(change.reason);
}
});
};

View File

@ -7,16 +7,17 @@ import { Subscription } from 'rxjs';
export class DatabaseViewCache {
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.subscribeOnNumOfFieldsChanged((fieldInfos) => {
fieldInfos.forEach((fieldInfo) => {
this.rowCache.onFieldUpdated(fieldInfo);
});
this.rowCache.onNumberOfFieldsUpdated(fieldInfos);
fieldController.subscribe({
onNumOfFieldsChanged: (fieldInfos) => {
fieldInfos.forEach((fieldInfo) => {
this.rowCache.onFieldUpdated(fieldInfo);
});
this.rowCache.onNumberOfFieldsUpdated(fieldInfos);
},
});
}
@ -33,12 +34,11 @@ export class DatabaseViewCache {
};
dispose = async () => {
this.fieldSubscription?.unsubscribe();
await this.rowsObserver.unsubscribe();
await this.rowCache.dispose();
};
listenOnRowsChanged = async () => {
initialize = async () => {
await this.rowsObserver.subscribe({
onRowsVisibilityChanged: (result) => {
if (result.ok) {

View File

@ -1,4 +1,158 @@
use crate::entities::parser::NotEmptyStr;
use crate::entities::{FieldIdPB, RowPB};
use flowy_derive::ProtoBuf;
use flowy_error::ErrorCode;
/// [DatabasePB] describes how many fields and blocks the grid has
#[derive(Debug, Clone, Default, ProtoBuf)]
pub struct DatabasePB {
#[pb(index = 1)]
pub id: String,
#[pb(index = 2)]
pub fields: Vec<FieldIdPB>,
#[pb(index = 3)]
pub rows: Vec<RowPB>,
}
#[derive(ProtoBuf, Default)]
pub struct CreateDatabasePayloadPB {
#[pb(index = 1)]
pub name: String,
}
#[derive(Clone, ProtoBuf, Default, Debug)]
pub struct DatabaseViewIdPB {
#[pb(index = 1)]
pub value: String,
}
impl AsRef<str> for DatabaseViewIdPB {
fn as_ref(&self) -> &str {
&self.value
}
}
#[derive(Debug, Clone, Default, ProtoBuf)]
pub struct MoveFieldPayloadPB {
#[pb(index = 1)]
pub view_id: String,
#[pb(index = 2)]
pub field_id: String,
#[pb(index = 3)]
pub from_index: i32,
#[pb(index = 4)]
pub to_index: i32,
}
#[derive(Clone)]
pub struct MoveFieldParams {
pub view_id: String,
pub field_id: String,
pub from_index: i32,
pub to_index: i32,
}
impl TryInto<MoveFieldParams> for MoveFieldPayloadPB {
type Error = ErrorCode;
fn try_into(self) -> Result<MoveFieldParams, Self::Error> {
let view_id = NotEmptyStr::parse(self.view_id).map_err(|_| ErrorCode::DatabaseViewIdIsEmpty)?;
let item_id = NotEmptyStr::parse(self.field_id).map_err(|_| ErrorCode::InvalidData)?;
Ok(MoveFieldParams {
view_id: view_id.0,
field_id: item_id.0,
from_index: self.from_index,
to_index: self.to_index,
})
}
}
#[derive(Debug, Clone, Default, ProtoBuf)]
pub struct MoveRowPayloadPB {
#[pb(index = 1)]
pub view_id: String,
#[pb(index = 2)]
pub from_row_id: String,
#[pb(index = 4)]
pub to_row_id: String,
}
pub struct MoveRowParams {
pub view_id: String,
pub from_row_id: String,
pub to_row_id: String,
}
impl TryInto<MoveRowParams> for MoveRowPayloadPB {
type Error = ErrorCode;
fn try_into(self) -> Result<MoveRowParams, Self::Error> {
let view_id = NotEmptyStr::parse(self.view_id).map_err(|_| ErrorCode::DatabaseViewIdIsEmpty)?;
let from_row_id = NotEmptyStr::parse(self.from_row_id).map_err(|_| ErrorCode::RowIdIsEmpty)?;
let to_row_id = NotEmptyStr::parse(self.to_row_id).map_err(|_| ErrorCode::RowIdIsEmpty)?;
Ok(MoveRowParams {
view_id: view_id.0,
from_row_id: from_row_id.0,
to_row_id: to_row_id.0,
})
}
}
#[derive(Debug, Clone, Default, ProtoBuf)]
pub struct MoveGroupRowPayloadPB {
#[pb(index = 1)]
pub view_id: String,
#[pb(index = 2)]
pub from_row_id: String,
#[pb(index = 3)]
pub to_group_id: String,
#[pb(index = 4, one_of)]
pub to_row_id: Option<String>,
}
pub struct MoveGroupRowParams {
pub view_id: String,
pub from_row_id: String,
pub to_group_id: String,
pub to_row_id: Option<String>,
}
impl TryInto<MoveGroupRowParams> for MoveGroupRowPayloadPB {
type Error = ErrorCode;
fn try_into(self) -> Result<MoveGroupRowParams, Self::Error> {
let view_id = NotEmptyStr::parse(self.view_id).map_err(|_| ErrorCode::DatabaseViewIdIsEmpty)?;
let from_row_id = NotEmptyStr::parse(self.from_row_id).map_err(|_| ErrorCode::RowIdIsEmpty)?;
let to_group_id =
NotEmptyStr::parse(self.to_group_id).map_err(|_| ErrorCode::GroupIdIsEmpty)?;
let to_row_id = match self.to_row_id {
None => None,
Some(to_row_id) => Some(
NotEmptyStr::parse(to_row_id)
.map_err(|_| ErrorCode::RowIdIsEmpty)?
.0,
),
};
Ok(MoveGroupRowParams {
view_id: view_id.0,
from_row_id: from_row_id.0,
to_group_id: to_group_id.0,
to_row_id,
})
}
}
#[derive(Debug, Default, ProtoBuf)]
pub struct DatabaseDescPB {
@ -14,3 +168,30 @@ pub struct RepeatedDatabaseDescPB {
#[pb(index = 1)]
pub items: Vec<DatabaseDescPB>,
}
#[derive(Debug, Clone, Default, ProtoBuf)]
pub struct DatabaseGroupIdPB {
#[pb(index = 1)]
pub view_id: String,
#[pb(index = 2)]
pub group_id: String,
}
pub struct DatabaseGroupIdParams {
pub view_id: String,
pub group_id: String,
}
impl TryInto<DatabaseGroupIdParams> for DatabaseGroupIdPB {
type Error = ErrorCode;
fn try_into(self) -> Result<DatabaseGroupIdParams, Self::Error> {
let view_id = NotEmptyStr::parse(self.view_id).map_err(|_| ErrorCode::DatabaseViewIdIsEmpty)?;
let group_id = NotEmptyStr::parse(self.group_id).map_err(|_| ErrorCode::GroupIdIsEmpty)?;
Ok(DatabaseGroupIdParams {
view_id: view_id.0,
group_id: group_id.0,
})
}
}

View File

@ -1,155 +0,0 @@
use crate::entities::parser::NotEmptyStr;
use crate::entities::{FieldIdPB, RowPB};
use flowy_derive::ProtoBuf;
use flowy_error::ErrorCode;
/// [DatabasePB] describes how many fields and blocks the grid has
#[derive(Debug, Clone, Default, ProtoBuf)]
pub struct DatabasePB {
#[pb(index = 1)]
pub id: String,
#[pb(index = 2)]
pub fields: Vec<FieldIdPB>,
#[pb(index = 3)]
pub rows: Vec<RowPB>,
}
#[derive(ProtoBuf, Default)]
pub struct CreateDatabasePayloadPB {
#[pb(index = 1)]
pub name: String,
}
#[derive(Clone, ProtoBuf, Default, Debug)]
pub struct DatabaseViewIdPB {
#[pb(index = 1)]
pub value: String,
}
impl AsRef<str> for DatabaseViewIdPB {
fn as_ref(&self) -> &str {
&self.value
}
}
#[derive(Debug, Clone, Default, ProtoBuf)]
pub struct MoveFieldPayloadPB {
#[pb(index = 1)]
pub view_id: String,
#[pb(index = 2)]
pub field_id: String,
#[pb(index = 3)]
pub from_index: i32,
#[pb(index = 4)]
pub to_index: i32,
}
#[derive(Clone)]
pub struct MoveFieldParams {
pub view_id: String,
pub field_id: String,
pub from_index: i32,
pub to_index: i32,
}
impl TryInto<MoveFieldParams> for MoveFieldPayloadPB {
type Error = ErrorCode;
fn try_into(self) -> Result<MoveFieldParams, Self::Error> {
let view_id = NotEmptyStr::parse(self.view_id).map_err(|_| ErrorCode::DatabaseViewIdIsEmpty)?;
let item_id = NotEmptyStr::parse(self.field_id).map_err(|_| ErrorCode::InvalidData)?;
Ok(MoveFieldParams {
view_id: view_id.0,
field_id: item_id.0,
from_index: self.from_index,
to_index: self.to_index,
})
}
}
#[derive(Debug, Clone, Default, ProtoBuf)]
pub struct MoveRowPayloadPB {
#[pb(index = 1)]
pub view_id: String,
#[pb(index = 2)]
pub from_row_id: String,
#[pb(index = 4)]
pub to_row_id: String,
}
pub struct MoveRowParams {
pub view_id: String,
pub from_row_id: String,
pub to_row_id: String,
}
impl TryInto<MoveRowParams> for MoveRowPayloadPB {
type Error = ErrorCode;
fn try_into(self) -> Result<MoveRowParams, Self::Error> {
let view_id = NotEmptyStr::parse(self.view_id).map_err(|_| ErrorCode::DatabaseViewIdIsEmpty)?;
let from_row_id = NotEmptyStr::parse(self.from_row_id).map_err(|_| ErrorCode::RowIdIsEmpty)?;
let to_row_id = NotEmptyStr::parse(self.to_row_id).map_err(|_| ErrorCode::RowIdIsEmpty)?;
Ok(MoveRowParams {
view_id: view_id.0,
from_row_id: from_row_id.0,
to_row_id: to_row_id.0,
})
}
}
#[derive(Debug, Clone, Default, ProtoBuf)]
pub struct MoveGroupRowPayloadPB {
#[pb(index = 1)]
pub view_id: String,
#[pb(index = 2)]
pub from_row_id: String,
#[pb(index = 3)]
pub to_group_id: String,
#[pb(index = 4, one_of)]
pub to_row_id: Option<String>,
}
pub struct MoveGroupRowParams {
pub view_id: String,
pub from_row_id: String,
pub to_group_id: String,
pub to_row_id: Option<String>,
}
impl TryInto<MoveGroupRowParams> for MoveGroupRowPayloadPB {
type Error = ErrorCode;
fn try_into(self) -> Result<MoveGroupRowParams, Self::Error> {
let view_id = NotEmptyStr::parse(self.view_id).map_err(|_| ErrorCode::DatabaseViewIdIsEmpty)?;
let from_row_id = NotEmptyStr::parse(self.from_row_id).map_err(|_| ErrorCode::RowIdIsEmpty)?;
let to_group_id =
NotEmptyStr::parse(self.to_group_id).map_err(|_| ErrorCode::GroupIdIsEmpty)?;
let to_row_id = match self.to_row_id {
None => None,
Some(to_row_id) => Some(
NotEmptyStr::parse(to_row_id)
.map_err(|_| ErrorCode::RowIdIsEmpty)?
.0,
),
};
Ok(MoveGroupRowParams {
view_id: view_id.0,
from_row_id: from_row_id.0,
to_group_id: to_group_id.0,
to_row_id,
})
}
}

View File

@ -3,7 +3,6 @@ mod cell_entities;
mod database_entities;
mod field_entities;
pub mod filter_entities;
mod grid_entities;
mod group_entities;
pub mod parser;
mod row_entities;
@ -14,9 +13,9 @@ mod view_entities;
pub use calendar_entities::*;
pub use cell_entities::*;
pub use database_entities::*;
pub use database_entities::*;
pub use field_entities::*;
pub use filter_entities::*;
pub use grid_entities::*;
pub use group_entities::*;
pub use row_entities::*;
pub use setting_entities::*;

View File

@ -538,6 +538,17 @@ pub(crate) async fn get_groups_handler(
data_result_ok(groups)
}
#[tracing::instrument(level = "trace", skip_all, err)]
pub(crate) async fn get_group_handler(
data: AFPluginData<DatabaseGroupIdPB>,
manager: AFPluginState<Arc<DatabaseManager>>,
) -> DataResult<GroupPB, FlowyError> {
let params: DatabaseGroupIdParams = data.into_inner().try_into()?;
let editor = manager.get_database_editor(&params.view_id).await?;
let group = editor.get_group(&params.view_id, &params.group_id).await?;
data_result_ok(group)
}
#[tracing::instrument(level = "debug", skip(data, manager), err)]
pub(crate) async fn create_board_card_handler(
data: AFPluginData<CreateBoardCardPayloadPB>,

View File

@ -47,7 +47,8 @@ pub fn init(database_manager: Arc<DatabaseManager>) -> AFPlugin {
.event(DatabaseEvent::CreateBoardCard, create_board_card_handler)
.event(DatabaseEvent::MoveGroup, move_group_handler)
.event(DatabaseEvent::MoveGroupRow, move_group_row_handler)
.event(DatabaseEvent::GetGroup, get_groups_handler)
.event(DatabaseEvent::GetGroups, get_groups_handler)
.event(DatabaseEvent::GetGroup, get_group_handler)
// Database
.event(DatabaseEvent::GetDatabases, get_databases_handler)
// Calendar
@ -221,7 +222,10 @@ pub enum DatabaseEvent {
UpdateDateCell = 80,
#[event(input = "DatabaseViewIdPB", output = "RepeatedGroupPB")]
GetGroup = 100,
GetGroups = 100,
#[event(input = "DatabaseGroupIdPB", output = "GroupPB")]
GetGroup = 101,
#[event(input = "CreateBoardCardPayloadPB", output = "RowPB")]
CreateBoardCard = 110,

View File

@ -207,14 +207,14 @@ impl DatabaseManager {
let create_view_editor = |database_editor: Arc<DatabaseEditor>| async move {
let user_id = user.user_id()?;
let (view_pad, view_rev_manager) = make_database_view_revision_pad(view_id, user).await?;
return DatabaseViewEditor::from_pad(
DatabaseViewEditor::from_pad(
&user_id,
database_editor.database_view_data.clone(),
database_editor.cell_data_cache.clone(),
view_rev_manager,
view_pad,
)
.await;
.await
};
let database_editor = self
@ -224,7 +224,7 @@ impl DatabaseManager {
.get(database_id)
.cloned();
return match database_editor {
match database_editor {
None => {
let mut editors_by_database_id = self.editors_by_database_id.write().await;
let db_pool = self.database_user.db_pool()?;
@ -241,7 +241,7 @@ impl DatabaseManager {
Ok(database_editor)
},
};
}
}
#[tracing::instrument(level = "trace", skip(self, pool), err)]

View File

@ -924,6 +924,11 @@ impl DatabaseEditor {
self.database_views.load_groups(view_id).await
}
#[tracing::instrument(level = "trace", skip_all, err)]
pub async fn get_group(&self, view_id: &str, group_id: &str) -> FlowyResult<GroupPB> {
self.database_views.get_group(view_id, group_id).await
}
async fn create_row_rev(&self) -> FlowyResult<RowRevision> {
let field_revs = self.database_pad.read().await.get_field_revs(None)?;
let block_id = self.block_id().await?;

View File

@ -24,7 +24,7 @@ use database_model::{
use flowy_client_sync::client_database::{
make_database_view_operations, DatabaseViewRevisionChangeset, DatabaseViewRevisionPad,
};
use flowy_error::FlowyResult;
use flowy_error::{FlowyError, FlowyResult};
use flowy_revision::RevisionManager;
use flowy_sqlite::ConnectionPool;
use flowy_task::TaskDispatcher;
@ -379,7 +379,7 @@ impl DatabaseViewEditor {
}
}
}
/// Only call once after grid view editor initialized
/// Only call once after database view editor initialized
#[tracing::instrument(level = "trace", skip(self))]
pub async fn v_load_groups(&self) -> FlowyResult<Vec<GroupPB>> {
let groups = self
@ -394,6 +394,14 @@ impl DatabaseViewEditor {
Ok(groups.into_iter().map(GroupPB::from).collect())
}
#[tracing::instrument(level = "trace", skip(self))]
pub async fn v_get_group(&self, group_id: &str) -> FlowyResult<GroupPB> {
match self.group_controller.read().await.get_group(group_id) {
None => Err(FlowyError::record_not_found().context("Can't find the group")),
Some((_, group)) => Ok(GroupPB::from(group)),
}
}
#[tracing::instrument(level = "trace", skip(self), err)]
pub async fn v_move_group(&self, params: MoveGroupParams) -> FlowyResult<()> {
self

View File

@ -1,7 +1,8 @@
#![allow(clippy::while_let_loop)]
use crate::entities::{
AlterFilterParams, AlterSortParams, CreateRowParams, DatabaseViewSettingPB, DeleteFilterParams,
DeleteGroupParams, DeleteSortParams, InsertGroupParams, MoveGroupParams, RepeatedGroupPB, RowPB,
DeleteGroupParams, DeleteSortParams, GroupPB, InsertGroupParams, MoveGroupParams,
RepeatedGroupPB, RowPB,
};
use crate::manager::DatabaseUser;
use crate::services::cell::AtomicCellDataCache;
@ -201,6 +202,11 @@ impl DatabaseViews {
Ok(RepeatedGroupPB { items: groups })
}
pub async fn get_group(&self, view_id: &str, group_id: &str) -> FlowyResult<GroupPB> {
let view_editor = self.get_view_editor(view_id).await?;
view_editor.v_get_group(group_id).await
}
pub async fn insert_or_update_group(&self, params: InsertGroupParams) -> FlowyResult<()> {
let view_editor = self.get_view_editor(&params.view_id).await?;
view_editor.v_initialize_new_group(params).await