Feat/tauri database effects (#1863)

* feat: config database view effects

* chore: add tests

* chore: config jest

* chore: config jest windows

* ci: wanrings

* chore: config folder effect
This commit is contained in:
Nathan.fooo 2023-02-19 14:59:04 +08:00 committed by GitHub
parent 7c3a823078
commit 8a2f5fe789
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
60 changed files with 2003 additions and 109 deletions

View File

@ -2,7 +2,6 @@ import 'package:app_flowy/workspace/presentation/home/home_stack.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flutter/material.dart';
import 'package:app_flowy/generated/locale_keys.g.dart';
import 'package:app_flowy/startup/plugin/plugin.dart';

View File

@ -615,16 +615,16 @@ class GridFieldController {
if (insertedFields.isEmpty) {
return;
}
final List<FieldInfo> newFields = fieldInfos;
final List<FieldInfo> newFieldInfos = fieldInfos;
for (final indexField in insertedFields) {
final gridField = FieldInfo(field: indexField.field_1);
if (newFields.length > indexField.index) {
newFields.insert(indexField.index, gridField);
final fieldInfo = FieldInfo(field: indexField.field_1);
if (newFieldInfos.length > indexField.index) {
newFieldInfos.insert(indexField.index, fieldInfo);
} else {
newFields.add(gridField);
newFieldInfos.add(fieldInfo);
}
}
_fieldNotifier?.fieldInfos = newFields;
_fieldNotifier?.fieldInfos = newFieldInfos;
}
List<FieldInfo> _updateFields(List<FieldPB> updatedFieldPBs) {

View File

@ -58,7 +58,6 @@ class GridController {
);
}
// Loads the rows from each block
Future<Either<Unit, FlowyError>> openGrid() async {
return _gridFFIService.openGrid().then((result) {
return result.fold(

View File

@ -29,7 +29,6 @@ abstract class RowCacheDelegate {
class GridRowCache {
final String databaseId;
final List<RowPB> rows;
/// _rows containers the current block's rows
/// Use List to reverse the order of the GridRow.
@ -48,7 +47,6 @@ class GridRowCache {
GridRowCache({
required this.databaseId,
required this.rows,
required RowChangesetNotifierForward notifier,
required RowCacheDelegate delegate,
}) : _cellCache = GridCellCache(databaseId: databaseId),

View File

@ -1,7 +1,5 @@
import 'dart:collection';
import 'package:appflowy_backend/protobuf/flowy-database/row_entities.pb.dart';
import 'row_cache.dart';
class RowList {
@ -134,21 +132,6 @@ class RowList {
return updatedIndexs;
}
List<DeletedIndex> markRowsAsInvisible(List<String> rowIds) {
final List<DeletedIndex> deletedRows = [];
for (final rowId in rowIds) {
final rowInfo = _rowInfoByRowId[rowId];
if (rowInfo != null) {
final index = _rowInfos.indexOf(rowInfo);
if (index != -1) {
deletedRows.add(DeletedIndex(index: index, rowInfo: rowInfo));
}
}
}
return deletedRows;
}
void reorderWithRowIds(List<String> rowIds) {
_rowInfos.clear();

View File

@ -21,7 +21,6 @@ class DatabaseViewCache {
final delegate = GridRowFieldNotifierImpl(fieldController);
_rowCache = GridRowCache(
databaseId: databaseId,
rows: [],
notifier: delegate,
delegate: delegate,
);

View File

@ -47,7 +47,7 @@ class AuthService {
return UserEventSignOut().send();
}
Future<Either<UserProfilePB, FlowyError>> signUpWithRandomUser() {
Future<Either<UserProfilePB, FlowyError>> autoSignUp() {
const password = "AppFlowy123@";
final uid = uuid();
final userEmail = "$uid@appflowy.io";

View File

@ -118,7 +118,7 @@ class _SkipLogInScreenState extends State<SkipLogInScreen> {
}
Future<void> _autoRegister(BuildContext context) async {
final result = await widget.authService.signUpWithRandomUser();
final result = await widget.authService.autoSignUp();
result.fold(
(user) {
FolderEventReadCurrentWorkspace().send().then((result) {

View File

@ -90,7 +90,7 @@ class SplashScreen extends StatelessWidget {
Future<void> _registerIfNeeded() async {
final result = await UserEventCheckUser().send();
if (!result.isLeft()) {
await getIt<AuthService>().signUpWithRandomUser();
await getIt<AuthService>().autoSignUp();
}
}
}

View File

@ -100,7 +100,6 @@ class AppBloc extends Bloc<AppEvent, AppState> {
name: value.name,
desc: value.desc ?? "",
dataFormatType: value.pluginBuilder.dataFormatType,
pluginType: value.pluginBuilder.pluginType,
layoutType: value.pluginBuilder.layoutType!,
initialData: value.initialData,
);

View File

@ -8,8 +8,6 @@ import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/app.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:app_flowy/startup/plugin/plugin.dart';
class AppService {
Future<Either<AppPB, FlowyError>> readApp({required String appId}) {
final payload = AppIdPB.create()..value = appId;
@ -22,7 +20,6 @@ class AppService {
required String name,
String? desc,
required ViewDataFormatPB dataFormatType,
required PluginType pluginType,
required ViewLayoutTypePB layoutType,
/// The initial data should be the JSON of the doucment

View File

@ -5,11 +5,6 @@ import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
class ViewService {
Future<Either<ViewPB, FlowyError>> readView({required String viewId}) {
final request = ViewIdPB(value: viewId);
return FolderEventReadView(request).send();
}
Future<Either<ViewPB, FlowyError>> updateView(
{required String viewId, String? name, String? desc}) {
final request = UpdateViewPayloadPB.create()..viewId = viewId;

View File

@ -33,7 +33,6 @@ class AppFlowyBoardTest {
appId: app.id,
name: "Test Board",
dataFormatType: builder.dataFormatType,
pluginType: builder.pluginType,
layoutType: builder.layoutType!,
)
.then((result) {

View File

@ -12,7 +12,6 @@ Future<GridTestContext> createTestFilterGrid(AppFlowyGridTest gridTest) async {
appId: app.id,
name: "Filter Grid",
dataFormatType: builder.dataFormatType,
pluginType: builder.pluginType,
layoutType: builder.layoutType!,
)
.then((result) {

View File

@ -168,7 +168,6 @@ class AppFlowyGridTest {
appId: app.id,
name: "Test Grid",
dataFormatType: builder.dataFormatType,
pluginType: builder.pluginType,
layoutType: builder.layoutType!,
)
.then((result) {

View File

@ -1,4 +1,7 @@
/src/services
/src/styles
src/services
src/styles
node_modules/
dist/
src-tauri/
.eslintrc.cjs
node_modules
tsconfig.json

View File

@ -1,4 +1,5 @@
module.exports = {
// https://eslint.org/docs/latest/use/configure/configuration-files
env: {
browser: true,
es6: true,
@ -9,6 +10,7 @@ module.exports = {
parserOptions: {
project: 'tsconfig.json',
sourceType: 'module',
tsconfigRootDir: __dirname,
},
plugins: ['@typescript-eslint'],
rules: {
@ -22,6 +24,8 @@ module.exports = {
'@typescript-eslint/prefer-for-of': 'warn',
'@typescript-eslint/triple-slash-reference': 'error',
'@typescript-eslint/unified-signatures': 'warn',
'no-shadow': 'off',
'@typescript-eslint/no-shadow': 'warn',
'constructor-super': 'error',
eqeqeq: ['error', 'always'],
'no-cond-assign': 'error',
@ -51,4 +55,5 @@ module.exports = {
'no-void': 'off',
'prefer-const': 'warn',
},
ignorePatterns: ['src/**/*.test.ts'],
};

View File

@ -0,0 +1,8 @@
/** @type {import('ts-jest').JestConfigWithTsJest} */
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
globals: {
window: {},
},
};

View File

@ -10,12 +10,14 @@
"format": "prettier --write .",
"test:code": "eslint --max-warnings=0 --ext .js,.ts,.tsx .",
"test:prettier": "yarn prettier --list-different src",
"tauri:dev": "tauri dev"
"tauri:dev": "tauri dev",
"test": "jest"
},
"dependencies": {
"@reduxjs/toolkit": "^1.9.2",
"@tauri-apps/api": "^1.2.0",
"google-protobuf": "^3.21.2",
"jest": "^29.4.3",
"nanoid": "^4.0.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
@ -23,14 +25,18 @@
"react-router-dom": "^6.8.0",
"react18-input-otp": "^1.1.2",
"redux": "^4.2.1",
"ts-results": "^3.3.0"
"rxjs": "^7.8.0",
"ts-results": "^3.3.0",
"utf8": "^3.0.0"
},
"devDependencies": {
"@tauri-apps/cli": "^1.2.2",
"@types/google-protobuf": "^3.15.6",
"@types/jest": "^29.4.0",
"@types/node": "^18.7.10",
"@types/react": "^18.0.15",
"@types/react-dom": "^18.0.6",
"@types/utf8": "^3.0.1",
"@typescript-eslint/eslint-plugin": "^5.51.0",
"@typescript-eslint/parser": "^5.51.0",
"@vitejs/plugin-react": "^3.0.0",
@ -41,6 +47,7 @@
"prettier": "^2.8.3",
"prettier-plugin-tailwindcss": "^0.2.2",
"tailwindcss": "^3.2.4",
"ts-jest": "^29.0.5",
"typescript": "^4.6.4",
"vite": "^4.0.0"
}

View File

@ -3,6 +3,7 @@ import { useState } from 'react';
import { useAppDispatch } from '../../../stores/store';
import { nanoid } from 'nanoid';
import { pagesActions } from '../../../stores/reducers/pages/slice';
import { ViewLayoutTypePB } from '../../../../services/backend';
export const useFolderEvents = (folder: IFolder) => {
const appDispatch = useAppDispatch();
@ -54,17 +55,33 @@ export const useFolderEvents = (folder: IFolder) => {
const onAddNewDocumentPage = () => {
closePopup();
appDispatch(pagesActions.addPage({ folderId: folder.id, pageType: 'document', title: 'New Page 1', id: nanoid(6) }));
appDispatch(
pagesActions.addPage({
folderId: folder.id,
pageType: ViewLayoutTypePB.Document,
title: 'New Page 1',
id: nanoid(6),
})
);
};
const onAddNewBoardPage = () => {
closePopup();
appDispatch(pagesActions.addPage({ folderId: folder.id, pageType: 'board', title: 'New Board 1', id: nanoid(6) }));
appDispatch(
pagesActions.addPage({
folderId: folder.id,
pageType: ViewLayoutTypePB.Board,
title: 'New Board 1',
id: nanoid(6),
})
);
};
const onAddNewGridPage = () => {
closePopup();
appDispatch(pagesActions.addPage({ folderId: folder.id, pageType: 'grid', title: 'New Grid 1', id: nanoid(6) }));
appDispatch(
pagesActions.addPage({ folderId: folder.id, pageType: ViewLayoutTypePB.Grid, title: 'New Grid 1', id: nanoid(6) })
);
};
return {

View File

@ -7,6 +7,7 @@ import { IPage } from '../../../stores/reducers/pages/slice';
import { Button } from '../../_shared/Button';
import { usePageEvents } from './PageItem.hooks';
import { RenamePopup } from './RenamePopup';
import { ViewLayoutTypePB } from '../../../../services/backend/models/flowy-folder/view';
export const PageItem = ({ page, onPageClick }: { page: IPage; onPageClick: () => void }) => {
const {
@ -29,9 +30,9 @@ export const PageItem = ({ page, onPageClick }: { page: IPage; onPageClick: () =
>
<div className={'flex min-w-0 flex-1 items-center'}>
<div className={'ml-1 mr-1 h-[16px] w-[16px]'}>
{page.pageType === 'document' && <DocumentSvg></DocumentSvg>}
{page.pageType === 'board' && <BoardSvg></BoardSvg>}
{page.pageType === 'grid' && <GridSvg></GridSvg>}
{page.pageType === ViewLayoutTypePB.Document && <DocumentSvg></DocumentSvg>}
{page.pageType === ViewLayoutTypePB.Board && <BoardSvg></BoardSvg>}
{page.pageType === ViewLayoutTypePB.Grid && <GridSvg></GridSvg>}
</div>
<span className={'ml-2 min-w-0 flex-1 overflow-hidden overflow-ellipsis whitespace-nowrap'}>{page.title}</span>
</div>

View File

@ -1,11 +1,11 @@
import { UserNotification, UserProfilePB } from '../../../../../services/backend';
import { AFNotificationListener, OnNotificationError } from '../../../../../services/backend/notifications';
import { AFNotificationObserver, OnNotificationError } from '../../../../../services/backend/notifications';
import { UserNotificationParser } from './parser';
declare type OnUserProfileUpdate = (userProfile: UserProfilePB) => void;
declare type OnUserSignIn = (userProfile: UserProfilePB) => void;
export class UserNotificationListener extends AFNotificationListener<UserNotification> {
export class UserNotificationListener extends AFNotificationObserver<UserNotification> {
onProfileUpdate?: OnUserProfileUpdate;
onUserSignIn?: OnUserSignIn;

View File

@ -0,0 +1,50 @@
import {
DatabaseEventCreateRow,
DatabaseEventGetDatabase,
DatabaseEventGetFields,
} from '../../../../services/backend/events/flowy-database/event';
import { DatabaseIdPB } from '../../../../services/backend/models/flowy-database';
import { CreateRowPayloadPB } from '../../../../services/backend/models/flowy-database/row_entities';
import {
GetFieldPayloadPB,
RepeatedFieldIdPB,
FieldIdPB,
} from '../../../../services/backend/models/flowy-database/field_entities';
import { ViewIdPB } from '../../../../services/backend/models/flowy-folder/view';
import { FolderEventCloseView } from '../../../../services/backend/events/flowy-folder';
export class DatabaseBackendService {
viewId: string;
constructor(viewId: string) {
this.viewId = viewId;
}
openDatabase = async () => {
const payload = DatabaseIdPB.fromObject({
value: this.viewId,
});
return DatabaseEventGetDatabase(payload);
};
closeDatabase = async () => {
const payload = ViewIdPB.fromObject({ value: this.viewId });
return FolderEventCloseView(payload);
};
createRow = async (rowId?: string) => {
const props = { database_id: this.viewId, start_row_id: rowId ?? undefined };
const payload = CreateRowPayloadPB.fromObject(props);
return DatabaseEventCreateRow(payload);
};
getFields = async (fieldIds?: FieldIdPB[]) => {
const payload = GetFieldPayloadPB.fromObject({ database_id: this.viewId });
if (!fieldIds) {
payload.field_ids = RepeatedFieldIdPB.fromObject({ items: fieldIds });
}
return DatabaseEventGetFields(payload).then((result) => result.map((value) => value.items));
};
}

View File

@ -0,0 +1,36 @@
import { DatabaseEventGetCell, DatabaseEventUpdateCell } from '../../../../../services/backend/events/flowy-database';
import { CellChangesetPB, CellIdPB } from '../../../../../services/backend/models/flowy-database/cell_entities';
import { FieldType } from '../../../../../services/backend/models/flowy-database/field_entities';
class CellIdentifier {
constructor(
public readonly viewId: string,
public readonly rowId: string,
public readonly fieldId: string,
public readonly fieldType: FieldType
) {}
}
class CellBackendService {
static updateCell = async (cellId: CellIdentifier, data: string) => {
const payload = CellChangesetPB.fromObject({
database_id: cellId.viewId,
field_id: cellId.fieldId,
row_id: cellId.rowId,
type_cell_data: data,
});
return DatabaseEventUpdateCell(payload);
};
getCell = async (cellId: CellIdentifier) => {
const payload = CellIdPB.fromObject({
database_id: cellId.viewId,
field_id: cellId.fieldId,
row_id: cellId.rowId,
});
return DatabaseEventGetCell(payload);
};
}
export { CellBackendService, CellIdentifier };

View File

@ -0,0 +1,45 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
export class CellCacheKey {
constructor(public readonly fieldId: string, public readonly rowId: string) {}
}
export class CellCache {
_cellDataByFieldId = new Map<string, Map<string, any>>();
constructor(public readonly databaseId: string) {}
remove = (key: CellCacheKey) => {
const inner = this._cellDataByFieldId.get(key.fieldId);
if (inner !== undefined) {
inner.delete(key.rowId);
}
};
removeWithFieldId = (fieldId: string) => {
this._cellDataByFieldId.delete(fieldId);
};
insert = (key: CellCacheKey, value: any) => {
let inner = this._cellDataByFieldId.get(key.fieldId);
if (inner === undefined) {
inner = this._cellDataByFieldId.set(key.fieldId, new Map());
}
inner.set(key.rowId, value);
};
get<T>(key: CellCacheKey): T | null {
const inner = this._cellDataByFieldId.get(key.fieldId);
if (inner === undefined) {
return null;
} else {
const value = inner.get(key.rowId);
if (typeof value === typeof undefined || typeof value === typeof null) {
return null;
}
if (value satisfies T) {
return value as T;
}
return null;
}
}
}

View File

@ -0,0 +1,40 @@
import { Err, Ok, Result } from 'ts-results';
import { ChangeNotifier } from '../../../../utils/change_notifier';
import { DatabaseNotificationObserver } from '../notifications/observer';
import { FlowyError } from '../../../../../services/backend/models/flowy-error';
import { DatabaseNotification } from '../../../../../services/backend';
type UpdateCellNotifiedValue = Result<void, FlowyError>;
export type CellListenerCallback = (value: UpdateCellNotifiedValue) => void;
export class CellObserver {
_notifier?: ChangeNotifier<UpdateCellNotifiedValue>;
_listener?: DatabaseNotificationObserver;
constructor(public readonly rowId: string, public readonly fieldId: string) {}
subscribe = (callbacks: { onCellChanged: CellListenerCallback }) => {
this._notifier = new ChangeNotifier();
this._notifier?.observer.subscribe(callbacks.onCellChanged);
this._listener = new DatabaseNotificationObserver({
viewId: this.rowId + ':' + this.fieldId,
parserHandler: (notification) => {
switch (notification) {
case DatabaseNotification.DidUpdateCell:
this._notifier?.notify(Ok.EMPTY);
return;
default:
break;
}
},
onError: (error) => this._notifier?.notify(Err(error)),
});
return undefined;
};
unsubscribe = async () => {
this._notifier?.unsubscribe();
await this._listener?.stop();
};
}

View File

@ -0,0 +1,123 @@
import { CellIdentifier } from './backend_service';
import { CellCache, CellCacheKey } from './cache';
import { FieldController } from '../field/controller';
import { CellDataLoader } from './data_parser';
import { CellDataPersistence } from './data_persistence';
import { FieldBackendService, TypeOptionParser } from '../field/backend_service';
import { ChangeNotifier } from '../../../../utils/change_notifier';
import { CellObserver } from './cell_observer';
import { Log } from '../../../../utils/log';
import { Err, Ok } from 'ts-results';
export abstract class CellFieldNotifier {
abstract subscribeOnFieldChanged(callback: () => void): void;
}
export class CellController<T, D> {
_fieldBackendService: FieldBackendService;
_cellDataNotifier: CellDataNotifier<T | null>;
_cellObserver: CellObserver;
_cacheKey: CellCacheKey;
constructor(
public readonly cellIdentifier: CellIdentifier,
private readonly cellCache: CellCache,
private readonly fieldNotifier: CellFieldNotifier,
private readonly cellDataLoader: CellDataLoader<T>,
private readonly cellDataPersistence: CellDataPersistence<D>
) {
this._fieldBackendService = new FieldBackendService(cellIdentifier.viewId, cellIdentifier.fieldId);
this._cacheKey = new CellCacheKey(cellIdentifier.rowId, cellIdentifier.fieldId);
this._cellDataNotifier = new CellDataNotifier(cellCache.get(this._cacheKey));
this._cellObserver = new CellObserver(cellIdentifier.rowId, cellIdentifier.fieldId);
}
subscribeChanged = (callbacks: { onCellChanged: (value: T | null) => void; onFieldChanged?: () => void }) => {
this._cellObserver.subscribe({
/// 1.Listen on user edit event and load the new cell data if needed.
/// For example:
/// user input: 12
/// cell display: $12
onCellChanged: async () => {
this.cellCache.remove(this._cacheKey);
await this._loadCellData();
},
});
/// 2.Listen on the field event and load the cell data if needed.
this.fieldNotifier.subscribeOnFieldChanged(async () => {
//
callbacks.onFieldChanged?.();
/// reloadOnFieldChanged should be true if you need to load the data when the corresponding field is changed
/// For example:
/// ¥12 -> $12
if (this.cellDataLoader.reloadOnFieldChanged) {
await this._loadCellData();
}
});
this._cellDataNotifier.observer.subscribe((cellData) => {
callbacks.onCellChanged(cellData);
});
};
getTypeOption = async <P extends TypeOptionParser<PD>, PD>(parser: P) => {
const result = await this._fieldBackendService.getTypeOptionData(this.cellIdentifier.fieldType);
if (result.ok) {
return Ok(parser.fromBuffer(result.val.type_option_data));
} else {
return Err(result.val);
}
};
saveCellData = async (data: D) => {
const result = await this.cellDataPersistence.save(data);
if (result.err) {
Log.error(result.val);
}
};
_loadCellData = () => {
return this.cellDataLoader.loadData().then((result) => {
if (result.ok && result.val !== undefined) {
this.cellCache.insert(this._cacheKey, result.val);
this._cellDataNotifier.cellData = result.val;
} else {
this.cellCache.remove(this._cacheKey);
this._cellDataNotifier.cellData = null;
}
});
};
}
export class CellFieldNotifierImpl extends CellFieldNotifier {
constructor(private readonly fieldController: FieldController) {
super();
}
subscribeOnFieldChanged(callback: () => void): void {
this.fieldController.subscribeOnFieldsChanged(callback);
}
}
class CellDataNotifier<T> extends ChangeNotifier<T | null> {
_cellData: T | null;
constructor(cellData: T) {
super();
this._cellData = cellData;
}
set cellData(data: T | null) {
if (this._cellData !== data) {
this._cellData = data;
this.notify(this._cellData);
}
}
get cellData(): T | null {
return this._cellData;
}
}

View File

@ -0,0 +1,141 @@
import {
DateCellDataPB,
FieldType,
SelectOptionCellDataPB,
URLCellDataPB,
} from '../../../../../services/backend/models/flowy-database';
import { CellIdentifier } from './backend_service';
import { CellController, CellFieldNotifierImpl } from './controller';
import {
CellDataLoader,
DateCellDataParser,
SelectOptionCellDataParser,
StringCellDataParser,
URLCellDataParser,
} from './data_parser';
import { CellCache } from './cache';
import { FieldController } from '../field/controller';
import { DateCellDataPersistence, TextCellDataPersistence } from './data_persistence';
export type TextCellController = CellController<string, string>;
export type CheckboxCellController = CellController<string, string>;
export type NumberCellController = CellController<string, string>;
export type SelectOptionCellController = CellController<SelectOptionCellDataPB, string>;
export type ChecklistCellController = CellController<SelectOptionCellDataPB, string>;
export type DateCellController = CellController<DateCellDataPB, CalendarData>;
export class CalendarData {
constructor(public readonly date: Date, public readonly time?: string) {}
}
export type URLCellController = CellController<URLCellDataPB, string>;
export class CellControllerBuilder {
_fieldNotifier: CellFieldNotifierImpl;
constructor(
public readonly cellIdentifier: CellIdentifier,
public readonly cellCache: CellCache,
public readonly fieldController: FieldController
) {
this._fieldNotifier = new CellFieldNotifierImpl(this.fieldController);
}
build = () => {
switch (this.cellIdentifier.fieldType) {
case FieldType.Checkbox:
return this.makeCheckboxCellController();
case FieldType.RichText:
return this.makeTextCellController();
case FieldType.Number:
return this.makeNumberCellController();
case FieldType.DateTime:
return this.makeDateCellController();
case FieldType.URL:
return this.makeURLCellController();
case FieldType.SingleSelect:
case FieldType.MultiSelect:
case FieldType.Checklist:
return this.makeSelectOptionCellController();
}
};
makeSelectOptionCellController = (): SelectOptionCellController => {
const loader = new CellDataLoader(this.cellIdentifier, new SelectOptionCellDataParser(), true);
const persistence = new TextCellDataPersistence(this.cellIdentifier);
return new CellController<SelectOptionCellDataPB, string>(
this.cellIdentifier,
this.cellCache,
this._fieldNotifier,
loader,
persistence
);
};
makeURLCellController = (): URLCellController => {
const loader = new CellDataLoader(this.cellIdentifier, new URLCellDataParser());
const persistence = new TextCellDataPersistence(this.cellIdentifier);
return new CellController<URLCellDataPB, string>(
this.cellIdentifier,
this.cellCache,
this._fieldNotifier,
loader,
persistence
);
};
makeDateCellController = (): DateCellController => {
const loader = new CellDataLoader(this.cellIdentifier, new DateCellDataParser(), true);
const persistence = new DateCellDataPersistence(this.cellIdentifier);
return new CellController<DateCellDataPB, CalendarData>(
this.cellIdentifier,
this.cellCache,
this._fieldNotifier,
loader,
persistence
);
};
makeNumberCellController = (): NumberCellController => {
const loader = new CellDataLoader(this.cellIdentifier, new StringCellDataParser(), true);
const persistence = new TextCellDataPersistence(this.cellIdentifier);
return new CellController<string, string>(
this.cellIdentifier,
this.cellCache,
this._fieldNotifier,
loader,
persistence
);
};
makeTextCellController = (): TextCellController => {
const loader = new CellDataLoader(this.cellIdentifier, new StringCellDataParser());
const persistence = new TextCellDataPersistence(this.cellIdentifier);
return new CellController<string, string>(
this.cellIdentifier,
this.cellCache,
this._fieldNotifier,
loader,
persistence
);
};
makeCheckboxCellController = (): CheckboxCellController => {
const loader = new CellDataLoader(this.cellIdentifier, new StringCellDataParser());
const persistence = new TextCellDataPersistence(this.cellIdentifier);
return new CellController<string, string>(
this.cellIdentifier,
this.cellCache,
this._fieldNotifier,
loader,
persistence
);
};
}

View File

@ -0,0 +1,70 @@
import utf8 from 'utf8';
import { CellBackendService, CellIdentifier } from './backend_service';
import { DateCellDataPB } from '../../../../../services/backend/models/flowy-database/date_type_option_entities';
import { SelectOptionCellDataPB } from '../../../../../services/backend/models/flowy-database/select_type_option';
import { URLCellDataPB } from '../../../../../services/backend/models/flowy-database/url_type_option_entities';
import { Err, Ok } from 'ts-results';
import { Log } from '../../../../utils/log';
abstract class CellDataParser<T> {
abstract parserData(data: Uint8Array): T | undefined;
}
class CellDataLoader<T> {
_service = new CellBackendService();
constructor(
readonly cellId: CellIdentifier,
readonly parser: CellDataParser<T>,
public readonly reloadOnFieldChanged: boolean = false
) {}
loadData = async () => {
const result = await this._service.getCell(this.cellId);
if (result.ok) {
return Ok(this.parser.parserData(result.val.data));
} else {
Log.error(result.err);
return Err(result.err);
}
};
}
class StringCellDataParser extends CellDataParser<string> {
parserData(data: Uint8Array): string {
return utf8.decode(data.toString());
}
}
class DateCellDataParser extends CellDataParser<DateCellDataPB> {
parserData(data: Uint8Array): DateCellDataPB {
return DateCellDataPB.deserializeBinary(data);
}
}
class SelectOptionCellDataParser extends CellDataParser<SelectOptionCellDataPB | undefined> {
parserData(data: Uint8Array): SelectOptionCellDataPB | undefined {
if (data.length === 0) {
return undefined;
}
return SelectOptionCellDataPB.deserializeBinary(data);
}
}
class URLCellDataParser extends CellDataParser<URLCellDataPB | undefined> {
parserData(data: Uint8Array): URLCellDataPB | undefined {
if (data.length === 0) {
return undefined;
}
return URLCellDataPB.deserializeBinary(data);
}
}
export {
StringCellDataParser,
DateCellDataParser,
SelectOptionCellDataParser,
URLCellDataParser,
CellDataLoader,
CellDataParser,
};

View File

@ -0,0 +1,46 @@
import { Result } from 'ts-results';
import { FlowyError } from '../../../../../services/backend/models/flowy-error';
import { CellBackendService, CellIdentifier } from './backend_service';
import { CalendarData } from './controller_builder';
import { DateChangesetPB } from '../../../../../services/backend/models/flowy-database/date_type_option_entities';
import { CellIdPB } from '../../../../../services/backend/models/flowy-database/cell_entities';
import { DatabaseEventUpdateDateCell } from '../../../../../services/backend/events/flowy-database';
export abstract class CellDataPersistence<D> {
abstract save(data: D): Promise<Result<void, FlowyError>>;
}
export class TextCellDataPersistence extends CellDataPersistence<string> {
constructor(public readonly cellId: CellIdentifier) {
super();
}
save(data: string): Promise<Result<void, FlowyError>> {
return CellBackendService.updateCell(this.cellId, data);
}
}
export class DateCellDataPersistence extends CellDataPersistence<CalendarData> {
constructor(public readonly cellIdentifier: CellIdentifier) {
super();
}
save(data: CalendarData): Promise<Result<void, FlowyError>> {
const payload = DateChangesetPB.fromObject({ cell_path: _makeCellPath(this.cellIdentifier) });
payload.date = data.date.getUTCMilliseconds.toString();
payload.is_utc = true;
if (data.time !== undefined) {
payload.time = data.time;
}
return DatabaseEventUpdateDateCell(payload);
}
}
function _makeCellPath(cellIdentifier: CellIdentifier): CellIdPB {
return CellIdPB.fromObject({
database_id: cellIdentifier.viewId,
field_id: cellIdentifier.fieldId,
row_id: cellIdentifier.rowId,
});
}

View File

@ -0,0 +1,51 @@
import { DatabaseBackendService } from './backend_service';
import { FieldController, FieldInfo } from './field/controller';
import { DatabaseViewCache } from './view/cache';
import { DatabasePB } from '../../../../services/backend/models/flowy-database/grid_entities';
import { RowChangedReason, RowInfo } from './row/cache';
import { Err } from 'ts-results';
export type SubscribeCallback = {
onViewChanged: (data: DatabasePB) => void;
onRowsChanged: (rowInfos: RowInfo[], reason: RowChangedReason) => void;
onFieldsChanged: (fieldInfos: FieldInfo[]) => void;
};
export class DatabaseController {
_backendService: DatabaseBackendService;
_fieldController: FieldController;
_databaseViewCache: DatabaseViewCache;
_callback?: SubscribeCallback;
constructor(public readonly viewId: string) {
this._backendService = new DatabaseBackendService(viewId);
this._fieldController = new FieldController(viewId);
this._databaseViewCache = new DatabaseViewCache(viewId, this._fieldController);
}
subscribe = (callbacks: SubscribeCallback) => {
this._callback = callbacks;
this._fieldController.subscribeOnFieldsChanged(callbacks.onFieldsChanged);
};
open = async () => {
const result = await this._backendService.openDatabase();
if (result.ok) {
const database: DatabasePB = result.val;
this._callback?.onViewChanged(database);
this._databaseViewCache.initializeWithRows(database.rows);
return await this._fieldController.loadFields(database.fields);
} else {
return Err(result.val);
}
};
createRow = async () => {
return this._backendService.createRow();
};
dispose = async () => {
await this._backendService.closeDatabase();
await this._fieldController.dispose();
};
}

View File

@ -0,0 +1,87 @@
import {
DeleteFieldPayloadPB,
DuplicateFieldPayloadPB,
FieldChangesetPB,
FieldType,
TypeOptionChangesetPB,
TypeOptionPathPB,
} from '../../../../../services/backend/models/flowy-database/field_entities';
import {
DatabaseEventDeleteField,
DatabaseEventDuplicateField,
DatabaseEventGetTypeOption,
DatabaseEventUpdateField,
DatabaseEventUpdateFieldTypeOption,
} from '../../../../../services/backend/events/flowy-database';
export abstract class TypeOptionParser<T> {
abstract fromBuffer(buffer: Uint8Array): T;
}
export class FieldBackendService {
constructor(public readonly databaseId: string, public readonly fieldId: string) {}
updateField = (data: {
name?: string;
fieldType: FieldType;
frozen?: boolean;
visibility?: boolean;
width?: number;
}) => {
const payload = FieldChangesetPB.fromObject({ database_id: this.databaseId, field_id: this.fieldId });
if (data.name !== undefined) {
payload.name = data.name;
}
if (data.fieldType !== undefined) {
payload.field_type = data.fieldType;
}
if (data.frozen !== undefined) {
payload.frozen = data.frozen;
}
if (data.visibility !== undefined) {
payload.visibility = data.visibility;
}
if (data.width !== undefined) {
payload.width = data.width;
}
return DatabaseEventUpdateField(payload);
};
updateTypeOption = (typeOptionData: Uint8Array) => {
const payload = TypeOptionChangesetPB.fromObject({
database_id: this.databaseId,
field_id: this.fieldId,
type_option_data: typeOptionData,
});
return DatabaseEventUpdateFieldTypeOption(payload);
};
deleteField = () => {
const payload = DeleteFieldPayloadPB.fromObject({ database_id: this.databaseId, field_id: this.fieldId });
return DatabaseEventDeleteField(payload);
};
duplicateField = () => {
const payload = DuplicateFieldPayloadPB.fromObject({ database_id: this.databaseId, field_id: this.fieldId });
return DatabaseEventDuplicateField(payload);
};
getTypeOptionData = (fieldType: FieldType) => {
const payload = TypeOptionPathPB.fromObject({
database_id: this.databaseId,
field_id: this.fieldId,
field_type: fieldType,
});
return DatabaseEventGetTypeOption(payload);
};
}

View File

@ -0,0 +1,129 @@
import { Log } from '../../../../utils/log';
import { DatabaseBackendService } from '../backend_service';
import { DatabaseFieldObserver } from './field_observer';
import { FieldIdPB, FieldPB, IndexFieldPB } from '../../../../../services/backend/models/flowy-database/field_entities';
import { ChangeNotifier } from '../../../../utils/change_notifier';
export class FieldController {
_fieldListener: DatabaseFieldObserver;
_backendService: DatabaseBackendService;
_fieldNotifier = new FieldNotifier([]);
constructor(public readonly viewId: string) {
this._backendService = new DatabaseBackendService(viewId);
this._fieldListener = new DatabaseFieldObserver(viewId);
this._listenOnFieldChanges();
}
dispose = async () => {
this._fieldNotifier.unsubscribe();
await this._fieldListener.unsubscribe();
};
get fieldInfos(): readonly FieldInfo[] {
return this._fieldNotifier.fieldInfos;
}
getField = (fieldId: string): FieldInfo | undefined => {
return this._fieldNotifier.fieldInfos.find((element) => element.field.id === fieldId);
};
loadFields = async (fieldIds: FieldIdPB[]) => {
const result = await this._backendService.getFields(fieldIds);
if (result.ok) {
this._fieldNotifier.fieldInfos = result.val.map((field) => new FieldInfo(field));
}
};
subscribeOnFieldsChanged = (callback: (fieldInfos: FieldInfo[]) => void) => {
return this._fieldNotifier.observer.subscribe((fieldInfos) => {
callback(fieldInfos);
});
};
_listenOnFieldChanges = () => {
this._fieldListener.subscribe({
onFieldsChanged: (result) => {
if (result.ok) {
const changeset = result.val;
this._deleteFields(changeset.deleted_fields);
this._insertFields(changeset.inserted_fields);
this._updateFields(changeset.updated_fields);
} else {
Log.error(result.val);
}
},
});
};
_deleteFields = (deletedFields: FieldIdPB[]) => {
if (deletedFields.length === 0) {
return;
}
const deletedFieldIds = deletedFields.map((field) => field.field_id);
const predicate = (element: FieldInfo) => {
!deletedFieldIds.includes(element.field.id);
};
const newFieldInfos = [...this.fieldInfos];
newFieldInfos.filter(predicate);
this._fieldNotifier.fieldInfos = newFieldInfos;
};
_insertFields = (insertedFields: IndexFieldPB[]) => {
if (insertedFields.length === 0) {
return;
}
const newFieldInfos = [...this.fieldInfos];
insertedFields.forEach((insertedField) => {
const fieldInfo = new FieldInfo(insertedField.field);
if (newFieldInfos.length > insertedField.index) {
newFieldInfos.splice(insertedField.index, 0, fieldInfo);
} else {
newFieldInfos.push(fieldInfo);
}
});
this._fieldNotifier.fieldInfos = newFieldInfos;
};
_updateFields = (updatedFields: FieldPB[]) => {
if (updatedFields.length === 0) {
return;
}
const newFieldInfos = [...this.fieldInfos];
updatedFields.forEach((updatedField) => {
newFieldInfos.map((element) => {
if (element.field.id === updatedField.id) {
return updatedField;
} else {
return element;
}
});
});
this._fieldNotifier.fieldInfos = newFieldInfos;
};
}
class FieldNotifier extends ChangeNotifier<FieldInfo[]> {
constructor(private _fieldInfos: FieldInfo[]) {
super();
}
set fieldInfos(newFieldInfos: FieldInfo[]) {
if (this._fieldInfos !== newFieldInfos) {
this._fieldInfos = newFieldInfos;
this.notify(this._fieldInfos);
}
}
/// Return a readonly list
get fieldInfos(): FieldInfo[] {
return this._fieldInfos;
}
}
export class FieldInfo {
constructor(public readonly field: FieldPB) {}
}

View File

@ -0,0 +1,41 @@
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 { ChangeNotifier } from '../../../../utils/change_notifier';
import { DatabaseNotificationObserver } from '../notifications/observer';
type UpdateFieldNotifiedValue = Result<DatabaseFieldChangesetPB, FlowyError>;
export type DatabaseNotificationCallback = (value: UpdateFieldNotifiedValue) => void;
export class DatabaseFieldObserver {
_notifier?: ChangeNotifier<UpdateFieldNotifiedValue>;
_listener?: DatabaseNotificationObserver;
constructor(public readonly databaseId: string) {}
subscribe = (callbacks: { onFieldsChanged: DatabaseNotificationCallback }) => {
this._notifier = new ChangeNotifier();
this._notifier?.observer.subscribe(callbacks.onFieldsChanged);
this._listener = new DatabaseNotificationObserver({
viewId: this.databaseId,
parserHandler: (notification, payload) => {
switch (notification) {
case DatabaseNotification.DidUpdateFields:
this._notifier?.notify(Ok(DatabaseFieldChangesetPB.deserializeBinary(payload)));
return;
default:
break;
}
},
onError: (error) => this._notifier?.notify(Err(error)),
});
return undefined;
};
unsubscribe = async () => {
this._notifier?.unsubscribe();
await this._listener?.stop();
};
}

View File

@ -0,0 +1,17 @@
import { DatabaseNotification } from '../../../../../services/backend/models/flowy-database/notification';
import { OnNotificationError } from '../../../../../services/backend/notifications';
import { AFNotificationObserver } from '../../../../../services/backend/notifications/observer';
import { DatabaseNotificationParser } from './parser';
export type ParserHandler = (notification: DatabaseNotification, payload: Uint8Array) => void;
export class DatabaseNotificationObserver extends AFNotificationObserver<DatabaseNotification> {
constructor(params: { viewId?: string; parserHandler: ParserHandler; onError?: OnNotificationError }) {
const parser = new DatabaseNotificationParser({
callback: params.parserHandler,
id: params.viewId,
onError: params.onError,
});
super(parser);
}
}

View File

@ -0,0 +1,314 @@
import { RowPB, InsertedRowPB, UpdatedRowPB } from '../../../../../services/backend/models/flowy-database/row_entities';
import { ChangeNotifier } from '../../../../utils/change_notifier';
import { FieldInfo } from '../field/controller';
import { CellCache, CellCacheKey } from '../cell/cache';
import {
ViewRowsChangesetPB,
ViewRowsVisibilityChangesetPB,
} from '../../../../../services/backend/models/flowy-database/view_entities';
import { CellIdentifier } from '../cell/backend_service';
import { ReorderSingleRowPB } from '../../../../../services/backend/models/flowy-database/sort_entities';
export class RowCache {
_rowList: RowList;
_cellCache: CellCache;
_notifier: RowChangeNotifier;
constructor(public readonly viewId: string, private readonly getFieldInfos: () => readonly FieldInfo[]) {
this._rowList = new RowList();
this._cellCache = new CellCache(viewId);
this._notifier = new RowChangeNotifier();
}
get rows(): readonly RowInfo[] {
return this._rowList.rows;
}
subscribeOnRowsChanged = (callback: (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()));
} else {
callback(change.reason);
}
});
};
onFieldUpdated = (fieldInfo: FieldInfo) => {
// Remove the cell data if the corresponding field was changed
this._cellCache.removeWithFieldId(fieldInfo.field.id);
};
onNumberOfFieldsUpdated = () => {
this._notifier.withChange(RowChangedReason.FieldDidChanged);
};
initializeRows = (rows: RowPB[]) => {
rows.forEach((rowPB) => {
this._rowList.push(this._toRowInfo(rowPB));
});
};
applyRowsChanged = (changeset: ViewRowsChangesetPB) => {
this._deleteRows(changeset.deleted_rows);
this._insertRows(changeset.inserted_rows);
this._updateRows(changeset.updated_rows);
};
applyRowsVisibility = (changeset: ViewRowsVisibilityChangesetPB) => {
this._hideRows(changeset.invisible_rows);
this._displayRows(changeset.visible_rows);
};
applyReorderRows = (rowIds: string[]) => {
this._rowList.reorderByRowIds(rowIds);
this._notifier.withChange(RowChangedReason.ReorderRows);
};
applyReorderSingleRow = (reorderRow: ReorderSingleRowPB) => {
const rowInfo = this._rowList.getRow(reorderRow.row_id);
if (rowInfo !== undefined) {
this._rowList.move({ rowId: reorderRow.row_id, fromIndex: reorderRow.old_index, toIndex: reorderRow.new_index });
this._notifier.withChange(RowChangedReason.ReorderSingleRow, reorderRow.row_id);
}
};
_deleteRows = (rowIds: string[]) => {
rowIds.forEach((rowId) => {
const deletedRow = this._rowList.remove(rowId);
if (deletedRow !== undefined) {
this._notifier.withChange(RowChangedReason.Delete, deletedRow.rowInfo.row.id);
}
});
};
_insertRows = (rows: InsertedRowPB[]) => {
rows.forEach((insertedRow) => {
const rowInfo = this._toRowInfo(insertedRow.row);
const insertedIndex = this._rowList.insert(insertedRow.index, rowInfo);
if (insertedIndex !== undefined) {
this._notifier.withChange(RowChangedReason.Insert, insertedIndex.rowId);
}
});
};
_updateRows = (updatedRows: UpdatedRowPB[]) => {
if (updatedRows.length === 0) {
return;
}
const rowInfos: RowInfo[] = [];
updatedRows.forEach((updatedRow) => {
updatedRow.field_ids.forEach((fieldId) => {
const key = new CellCacheKey(fieldId, updatedRow.row.id);
this._cellCache.remove(key);
});
rowInfos.push(this._toRowInfo(updatedRow.row));
});
const updatedIndexs = this._rowList.insertRows(rowInfos);
updatedIndexs.forEach((row) => {
this._notifier.withChange(RowChangedReason.Update, row.rowId);
});
};
_hideRows = (rowIds: string[]) => {
rowIds.forEach((rowId) => {
const deletedRow = this._rowList.remove(rowId);
if (deletedRow !== undefined) {
this._notifier.withChange(RowChangedReason.Delete, deletedRow.rowInfo.row.id);
}
});
};
_displayRows = (insertedRows: InsertedRowPB[]) => {
insertedRows.forEach((insertedRow) => {
const insertedIndex = this._rowList.insert(insertedRow.index, this._toRowInfo(insertedRow.row));
if (insertedIndex !== undefined) {
this._notifier.withChange(RowChangedReason.Insert, insertedIndex.rowId);
}
});
};
dispose = async () => {
this._notifier.dispose();
};
_toRowInfo = (rowPB: RowPB) => {
return new RowInfo(this.viewId, this.getFieldInfos(), rowPB);
};
_toCellMap = (rowId: string, fieldInfos: readonly FieldInfo[]): Map<string, CellIdentifier> => {
const cellIdentifierByFieldId: Map<string, CellIdentifier> = new Map();
fieldInfos.forEach((fieldInfo) => {
const identifier = new CellIdentifier(this.viewId, rowId, fieldInfo.field.id, fieldInfo.field.field_type);
cellIdentifierByFieldId.set(fieldInfo.field.id, identifier);
});
return cellIdentifierByFieldId;
};
}
class RowList {
_rowInfos: RowInfo[] = [];
_rowInfoByRowId: Map<string, RowInfo> = new Map();
get rows(): readonly RowInfo[] {
return this._rowInfos;
}
getRow = (rowId: string) => {
return this._rowInfoByRowId.get(rowId);
};
getRowWithIndex = (rowId: string): { rowInfo: RowInfo; index: number } | undefined => {
const rowInfo = this._rowInfoByRowId.get(rowId);
if (rowInfo !== undefined) {
const index = this._rowInfos.indexOf(rowInfo, 0);
return { rowInfo: rowInfo, index: index };
}
return undefined;
};
indexOfRow = (rowId: string): number => {
const rowInfo = this._rowInfoByRowId.get(rowId);
if (rowInfo !== undefined) {
return this._rowInfos.indexOf(rowInfo, 0);
}
return -1;
};
push = (rowInfo: RowInfo) => {
const index = this.indexOfRow(rowInfo.row.id);
if (index !== -1) {
this._rowInfos.splice(index, 1, rowInfo);
} else {
this._rowInfos.push(rowInfo);
}
this._rowInfoByRowId.set(rowInfo.row.id, rowInfo);
};
remove = (rowId: string): DeletedRow | undefined => {
const result = this.getRowWithIndex(rowId);
if (result !== undefined) {
this._rowInfoByRowId.delete(result.rowInfo.row.id);
this._rowInfos.splice(result.index, 1);
return new DeletedRow(result.index, result.rowInfo);
} else {
return undefined;
}
};
insert = (index: number, newRowInfo: RowInfo): InsertedRow | undefined => {
const rowId = newRowInfo.row.id;
// Calibrate where to insert
let insertedIndex = index;
if (this._rowInfos.length <= insertedIndex) {
insertedIndex = this._rowInfos.length;
}
const result = this.getRowWithIndex(rowId);
if (result !== undefined) {
// remove the old row info
this._rowInfos.splice(result.index, 1);
// insert the new row info to the insertedIndex
this._rowInfos.splice(insertedIndex, 0, newRowInfo);
this._rowInfoByRowId.set(rowId, newRowInfo);
return undefined;
} else {
this._rowInfos.splice(insertedIndex, 0, newRowInfo);
this._rowInfoByRowId.set(rowId, newRowInfo);
return new InsertedRow(insertedIndex, rowId);
}
};
insertRows = (rowInfos: RowInfo[]) => {
const map = new Map<string, InsertedRow>();
rowInfos.forEach((rowInfo) => {
const index = this.indexOfRow(rowInfo.row.id);
if (index !== -1) {
this._rowInfos.splice(index, 1, rowInfo);
this._rowInfoByRowId.set(rowInfo.row.id, rowInfo);
map.set(rowInfo.row.id, new InsertedRow(index, rowInfo.row.id));
}
});
return map;
};
move = (params: { rowId: string; fromIndex: number; toIndex: number }) => {
const currentIndex = this.indexOfRow(params.rowId);
if (currentIndex !== -1 && currentIndex !== params.toIndex) {
const rowInfo = this.remove(params.rowId)?.rowInfo;
if (rowInfo !== undefined) {
this.insert(params.toIndex, rowInfo);
}
}
};
reorderByRowIds = (rowIds: string[]) => {
// remove all the elements
this._rowInfos = [];
rowIds.forEach((rowId) => {
const rowInfo = this._rowInfoByRowId.get(rowId);
if (rowInfo !== undefined) {
this._rowInfos.push(rowInfo);
}
});
};
includes = (rowId: string): boolean => {
return this._rowInfoByRowId.has(rowId);
};
}
export class RowInfo {
constructor(
public readonly databaseId: string,
public readonly fieldInfos: readonly FieldInfo[],
public readonly row: RowPB
) {}
}
export class DeletedRow {
constructor(public readonly index: number, public readonly rowInfo: RowInfo) {}
}
export class InsertedRow {
constructor(public readonly index: number, public readonly rowId: string) {}
}
export class RowChanged {
constructor(public readonly reason: RowChangedReason, public readonly rowId?: string) {}
}
// eslint-disable-next-line no-shadow
export enum RowChangedReason {
Insert,
Delete,
Update,
Initial,
FieldDidChanged,
ReorderRows,
ReorderSingleRow,
}
export class RowChangeNotifier extends ChangeNotifier<RowChanged> {
_currentChanged = new RowChanged(RowChangedReason.Initial);
withChange = (reason: RowChangedReason, rowId?: string) => {
const newChange = new RowChanged(reason, rowId);
if (this._currentChanged !== newChange) {
this._currentChanged = newChange;
this.notify(this._currentChanged);
}
};
dispose = () => {
this.unsubscribe();
};
}

View File

@ -0,0 +1,55 @@
import { DatabaseViewRowsObserver } from './row_observer';
import { RowCache, RowChangedReason } from '../row/cache';
import { FieldController } from '../field/controller';
import { RowPB } from '../../../../../services/backend/models/flowy-database/row_entities';
export class DatabaseViewCache {
_rowsObserver: DatabaseViewRowsObserver;
_rowCache: RowCache;
constructor(public readonly viewId: string, fieldController: FieldController) {
this._rowsObserver = new DatabaseViewRowsObserver(viewId);
this._rowCache = new RowCache(viewId, () => fieldController.fieldInfos);
this._listenOnRowsChanged();
}
initializeWithRows = (rows: RowPB[]) => {
this._rowCache.initializeRows(rows);
};
subscribeOnRowsChanged = (onRowsChanged: (reason: RowChangedReason) => void) => {
return this._rowCache.subscribeOnRowsChanged((reason) => {
onRowsChanged(reason);
});
};
dispose = async () => {
await this._rowsObserver.unsubscribe();
await this._rowCache.dispose();
};
_listenOnRowsChanged = () => {
this._rowsObserver.subscribe({
onRowsVisibilityChanged: (result) => {
if (result.ok) {
this._rowCache.applyRowsVisibility(result.val);
}
},
onNumberOfRowsChanged: (result) => {
if (result.ok) {
this._rowCache.applyRowsChanged(result.val);
}
},
onReorderRows: (result) => {
if (result.ok) {
this._rowCache.applyReorderRows(result.val);
}
},
onReorderSingleRow: (result) => {
if (result.ok) {
this._rowCache.applyReorderSingleRow(result.val);
}
},
});
};
}

View File

@ -0,0 +1,71 @@
import { Ok, Result } from 'ts-results';
import {
DatabaseNotification,
ReorderAllRowsPB,
ReorderSingleRowPB,
} from '../../../../../services/backend/events/flowy-database';
import {
ViewRowsChangesetPB,
ViewRowsVisibilityChangesetPB,
} from '../../../../../services/backend/models/flowy-database/view_entities';
import { FlowyError } from '../../../../../services/backend/models/flowy-error/errors';
import { ChangeNotifier } from '../../../../utils/change_notifier';
import { DatabaseNotificationObserver } from '../notifications/observer';
export type RowsVisibilityNotifyValue = Result<ViewRowsVisibilityChangesetPB, FlowyError>;
export type RowsNotifyValue = Result<ViewRowsChangesetPB, FlowyError>;
export type ReorderRowsNotifyValue = Result<string[], FlowyError>;
export type ReorderSingleRowNotifyValue = Result<ReorderSingleRowPB, FlowyError>;
export class DatabaseViewRowsObserver {
_rowsVisibilityNotifier = new ChangeNotifier<RowsVisibilityNotifyValue>();
_rowsNotifier = new ChangeNotifier<RowsNotifyValue>();
_reorderRowsNotifier = new ChangeNotifier<ReorderRowsNotifyValue>();
_reorderSingleRowNotifier = new ChangeNotifier<ReorderSingleRowNotifyValue>();
_listener?: DatabaseNotificationObserver;
constructor(public readonly viewId: string) {}
subscribe = (callbacks: {
onRowsVisibilityChanged?: (value: RowsVisibilityNotifyValue) => void;
onNumberOfRowsChanged?: (value: RowsNotifyValue) => void;
onReorderRows?: (value: ReorderRowsNotifyValue) => void;
onReorderSingleRow?: (value: ReorderSingleRowNotifyValue) => void;
}) => {
//
this._rowsVisibilityNotifier.observer.subscribe(callbacks.onRowsVisibilityChanged);
this._rowsNotifier.observer.subscribe(callbacks.onNumberOfRowsChanged);
this._reorderRowsNotifier.observer.subscribe(callbacks.onReorderRows);
this._reorderSingleRowNotifier.observer.subscribe(callbacks.onReorderSingleRow);
this._listener = new DatabaseNotificationObserver({
viewId: this.viewId,
parserHandler: (notification, payload) => {
switch (notification) {
case DatabaseNotification.DidUpdateViewRowsVisibility:
this._rowsVisibilityNotifier.notify(Ok(ViewRowsVisibilityChangesetPB.deserializeBinary(payload)));
break;
case DatabaseNotification.DidUpdateViewRows:
this._rowsNotifier.notify(Ok(ViewRowsChangesetPB.deserializeBinary(payload)));
break;
case DatabaseNotification.DidReorderRows:
this._reorderRowsNotifier.notify(Ok(ReorderAllRowsPB.deserializeBinary(payload).row_orders));
break;
case DatabaseNotification.DidReorderSingleRow:
this._reorderSingleRowNotifier.notify(Ok(ReorderSingleRowPB.deserializeBinary(payload)));
break;
default:
break;
}
},
});
};
unsubscribe = async () => {
this._rowsVisibilityNotifier.unsubscribe();
this._reorderRowsNotifier.unsubscribe();
this._rowsNotifier.unsubscribe();
this._reorderSingleRowNotifier.unsubscribe();
await this._listener?.stop();
};
}

View File

@ -0,0 +1,38 @@
import { Ok, Result } from 'ts-results';
import { AppPB, FolderNotification } from '../../../../../services/backend';
import { FlowyError } from '../../../../../services/backend/models/flowy-error';
import { ChangeNotifier } from '../../../../utils/change_notifier';
import { FolderNotificationObserver } from '../notifications/observer';
export type AppUpdateNotifyValue = Result<AppPB, FlowyError>;
export type AppUpdateNotifyCallback = (value: AppUpdateNotifyValue) => void;
export class WorkspaceObserver {
_appNotifier = new ChangeNotifier<AppUpdateNotifyValue>();
_listener?: FolderNotificationObserver;
constructor(public readonly appId: string) {}
subscribe = (callbacks: { onAppChanged: AppUpdateNotifyCallback }) => {
this._appNotifier?.observer.subscribe(callbacks.onAppChanged);
this._listener = new FolderNotificationObserver({
viewId: this.appId,
parserHandler: (notification, payload) => {
switch (notification) {
case FolderNotification.DidUpdateWorkspaceApps:
this._appNotifier?.notify(Ok(AppPB.deserializeBinary(payload)));
break;
default:
break;
}
},
});
return undefined;
};
unsubscribe = async () => {
this._appNotifier.unsubscribe();
await this._listener?.stop();
};
}

View File

@ -0,0 +1,98 @@
import {
FolderEventCreateView,
FolderEventDeleteApp,
FolderEventDeleteView,
FolderEventMoveItem,
FolderEventReadApp,
FolderEventUpdateApp,
ViewDataFormatPB,
ViewLayoutTypePB,
} from '../../../../../services/backend/events/flowy-folder';
import { AppIdPB, UpdateAppPayloadPB } from '../../../../../services/backend/models/flowy-folder/app';
import {
CreateViewPayloadPB,
RepeatedViewIdPB,
ViewPB,
MoveFolderItemPayloadPB,
MoveFolderItemType,
} from '../../../../../services/backend/models/flowy-folder/view';
import { FlowyError } from '../../../../../services/backend/models/flowy-error/errors';
import { None, Result, Some } from 'ts-results';
export class AppBackendService {
constructor(public readonly appId: string) {}
getApp = () => {
const payload = AppIdPB.fromObject({ value: this.appId });
return FolderEventReadApp(payload);
};
createView = (params: {
name: string;
desc?: string;
dataFormatType: ViewDataFormatPB;
layoutType: ViewLayoutTypePB;
/// The initial data should be the JSON of the doucment
/// For example: {"document":{"type":"editor","children":[]}}
initialData?: string;
}) => {
const encoder = new TextEncoder();
const payload = CreateViewPayloadPB.fromObject({
belong_to_id: this.appId,
name: params.name,
desc: params.desc || '',
data_format: params.dataFormatType,
layout: params.layoutType,
initial_data: encoder.encode(params.initialData || ''),
});
return FolderEventCreateView(payload);
};
getAllViews = (): Promise<Result<ViewPB[], FlowyError>> => {
const payload = AppIdPB.fromObject({ value: this.appId });
return FolderEventReadApp(payload).then((result) => {
return result.map((app) => app.belongings.items);
});
};
getView = async (viewId: string) => {
const result = await this.getAllViews();
if (result.ok) {
const target = result.val.find((view) => view.id === viewId);
if (target !== undefined) {
return Some(target);
} else {
return None;
}
} else {
return None;
}
};
update = (params: { name: string }) => {
const payload = UpdateAppPayloadPB.fromObject({ app_id: this.appId, name: params.name });
return FolderEventUpdateApp(payload);
};
delete = () => {
const payload = AppIdPB.fromObject({ value: this.appId });
return FolderEventDeleteApp(payload);
};
deleteView = (viewId: string) => {
const payload = RepeatedViewIdPB.fromObject({ items: [viewId] });
return FolderEventDeleteView(payload);
};
moveView = (params: { view_id: string; fromIndex: number; toIndex: number }) => {
const payload = MoveFolderItemPayloadPB.fromObject({
item_id: params.view_id,
from: params.fromIndex,
to: params.toIndex,
ty: MoveFolderItemType.MoveView,
});
return FolderEventMoveItem(payload);
};
}

View File

@ -0,0 +1,17 @@
import { OnNotificationError } from '../../../../../services/backend/notifications';
import { AFNotificationObserver } from '../../../../../services/backend/notifications/observer';
import { FolderNotificationParser } from './parser';
import { FolderNotification } from '../../../../../services/backend/models/flowy-folder/notification';
export type ParserHandler = (notification: FolderNotification, payload: Uint8Array) => void;
export class FolderNotificationObserver extends AFNotificationObserver<FolderNotification> {
constructor(params: { viewId?: string; parserHandler: ParserHandler; onError?: OnNotificationError }) {
const parser = new FolderNotificationParser({
callback: params.parserHandler,
id: params.viewId,
onError: params.onError,
});
super(parser);
}
}

View File

@ -0,0 +1,26 @@
import { NotificationParser, OnNotificationError } from '../../../../../services/backend/notifications';
import { FolderNotification } from '../../../../../services/backend/models/flowy-folder/notification';
declare type FolderNotificationCallback = (ty: FolderNotification, payload: Uint8Array) => void;
export class FolderNotificationParser extends NotificationParser<FolderNotification> {
constructor(params: { id?: string; callback: FolderNotificationCallback; onError?: OnNotificationError }) {
super(
params.callback,
(ty) => {
const notification = FolderNotification[ty];
if (isFolderNotification(notification)) {
return FolderNotification[notification];
} else {
return FolderNotification.Unknown;
}
},
params.id,
params.onError
);
}
}
const isFolderNotification = (notification: string): notification is keyof typeof FolderNotification => {
return Object.values(FolderNotification).indexOf(notification) !== -1;
};

View File

@ -0,0 +1,32 @@
import { UpdateViewPayloadPB, RepeatedViewIdPB, ViewPB } from '../../../../../services/backend/models/flowy-folder/view';
import {
FolderEventDeleteView,
FolderEventDuplicateView,
FolderEventUpdateView,
} from '../../../../../services/backend/events/flowy-folder';
export class ViewBackendService {
constructor(public readonly viewId: string) {}
update = (params: { name?: string; desc?: string }) => {
const payload = UpdateViewPayloadPB.fromObject({ view_id: this.viewId });
if (params.name !== undefined) {
payload.name = params.name;
}
if (params.desc !== undefined) {
payload.desc = params.desc;
}
return FolderEventUpdateView(payload);
};
delete = () => {
const payload = RepeatedViewIdPB.fromObject({ items: [this.viewId] });
return FolderEventDeleteView(payload);
};
duplicate = (view: ViewPB) => {
return FolderEventDuplicateView(view);
};
}

View File

@ -0,0 +1,74 @@
import { Ok, Result } from 'ts-results';
import { FlowyError } from '../../../../../services/backend/models/flowy-error/errors';
import { DeletedViewPB, FolderNotification, ViewPB } from '../../../../../services/backend/models/flowy-folder';
import { ChangeNotifier } from '../../../../utils/change_notifier';
import { FolderNotificationObserver } from '../notifications/observer';
type DeleteViewNotifyValue = Result<ViewPB, FlowyError>;
type UpdateViewNotifyValue = Result<ViewPB, FlowyError>;
type RestoreViewNotifyValue = Result<ViewPB, FlowyError>;
type MoveToTrashViewNotifyValue = Result<DeletedViewPB, FlowyError>;
export class ViewObserver {
_deleteViewNotifier = new ChangeNotifier<DeleteViewNotifyValue>();
_updateViewNotifier = new ChangeNotifier<UpdateViewNotifyValue>();
_restoreViewNotifier = new ChangeNotifier<RestoreViewNotifyValue>();
_moveToTashNotifier = new ChangeNotifier<MoveToTrashViewNotifyValue>();
_listener?: FolderNotificationObserver;
constructor(public readonly viewId: string) {}
subscribe = (callbacks: {
onViewUpdate?: (value: UpdateViewNotifyValue) => void;
onViewDelete?: (value: DeleteViewNotifyValue) => void;
onViewRestored?: (value: RestoreViewNotifyValue) => void;
onViewMoveToTrash?: (value: MoveToTrashViewNotifyValue) => void;
}) => {
if (callbacks.onViewDelete !== undefined) {
this._deleteViewNotifier.observer.subscribe(callbacks.onViewDelete);
}
if (callbacks.onViewUpdate !== undefined) {
this._updateViewNotifier.observer.subscribe(callbacks.onViewUpdate);
}
if (callbacks.onViewRestored !== undefined) {
this._restoreViewNotifier.observer.subscribe(callbacks.onViewRestored);
}
if (callbacks.onViewMoveToTrash !== undefined) {
this._moveToTashNotifier.observer.subscribe(callbacks.onViewMoveToTrash);
}
this._listener = new FolderNotificationObserver({
viewId: this.viewId,
parserHandler: (notification, payload) => {
switch (notification) {
case FolderNotification.DidUpdateView:
this._updateViewNotifier.notify(Ok(ViewPB.deserializeBinary(payload)));
break;
case FolderNotification.DidDeleteView:
this._deleteViewNotifier.notify(Ok(ViewPB.deserializeBinary(payload)));
break;
case FolderNotification.DidRestoreView:
this._restoreViewNotifier.notify(Ok(ViewPB.deserializeBinary(payload)));
break;
case FolderNotification.DidMoveViewToTrash:
this._moveToTashNotifier.notify(Ok(DeletedViewPB.deserializeBinary(payload)));
break;
default:
break;
}
},
});
return undefined;
};
unsubscribe = async () => {
this._deleteViewNotifier.unsubscribe();
this._updateViewNotifier.unsubscribe();
this._restoreViewNotifier.unsubscribe();
this._moveToTashNotifier.unsubscribe();
await this._listener?.stop();
};
}

View File

@ -0,0 +1,57 @@
import { Err, Ok } from 'ts-results';
import {
FolderEventCreateApp,
FolderEventMoveItem,
FolderEventReadWorkspaceApps,
FolderEventReadWorkspaces,
} from '../../../../../services/backend/events/flowy-folder';
import { CreateAppPayloadPB } from '../../../../../services/backend/models/flowy-folder/app';
import { WorkspaceIdPB } from '../../../../../services/backend/models/flowy-folder/workspace';
import assert from 'assert';
import { FlowyError } from '../../../../../services/backend/models/flowy-error/errors';
import { MoveFolderItemPayloadPB } from '../../../../../services/backend/models/flowy-folder/view';
export class WorkspaceBackendService {
constructor(public readonly workspaceId: string) {}
createApp = (params: { name: string; desc?: string }) => {
const payload = CreateAppPayloadPB.fromObject({
workspace_id: this.workspaceId,
name: params.name,
desc: params.desc || '',
});
return FolderEventCreateApp(payload);
};
getWorkspace = () => {
const payload = WorkspaceIdPB.fromObject({ value: this.workspaceId });
return FolderEventReadWorkspaces(payload).then((result) => {
if (result.ok) {
const workspaces = result.val.items;
if (workspaces.length === 0) {
return Err(FlowyError.fromObject({ msg: 'workspace not found' }));
} else {
assert(workspaces.length === 1);
return Ok(workspaces[0]);
}
} else {
return Err(result.val);
}
});
};
getApps = () => {
const payload = WorkspaceIdPB.fromObject({ value: this.workspaceId });
return FolderEventReadWorkspaceApps(payload).then((result) => result.map((val) => val.items));
};
moveApp = (params: { appId: string; fromIndex: number; toIndex: number }) => {
const payload = MoveFolderItemPayloadPB.fromObject({
item_id: params.appId,
from: params.fromIndex,
to: params.toIndex,
});
return FolderEventMoveItem(payload);
};
}

View File

@ -0,0 +1,46 @@
import { Ok, Result } from 'ts-results';
import { AppPB, FolderNotification, RepeatedAppPB, WorkspacePB } from '../../../../../services/backend';
import { FlowyError } from '../../../../../services/backend/models/flowy-error';
import { ChangeNotifier } from '../../../../utils/change_notifier';
import { FolderNotificationObserver } from '../notifications/observer';
export type AppListNotifyValue = Result<AppPB[], FlowyError>;
export type AppListNotifyCallback = (value: AppListNotifyValue) => void;
export type WorkspaceNotifyValue = Result<WorkspacePB, FlowyError>;
export type WorkspaceNotifyCallback = (value: WorkspaceNotifyValue) => void;
export class WorkspaceObserver {
_appListNotifier = new ChangeNotifier<AppListNotifyValue>();
_workspaceNotifier = new ChangeNotifier<WorkspaceNotifyValue>();
_listener?: FolderNotificationObserver;
constructor(public readonly workspaceId: string) {}
subscribe = (callbacks: { onAppListChanged: AppListNotifyCallback; onWorkspaceChanged: WorkspaceNotifyCallback }) => {
this._appListNotifier?.observer.subscribe(callbacks.onAppListChanged);
this._workspaceNotifier?.observer.subscribe(callbacks.onWorkspaceChanged);
this._listener = new FolderNotificationObserver({
viewId: this.workspaceId,
parserHandler: (notification, payload) => {
switch (notification) {
case FolderNotification.DidUpdateWorkspace:
this._workspaceNotifier?.notify(Ok(WorkspacePB.deserializeBinary(payload)));
break;
case FolderNotification.DidUpdateWorkspaceApps:
this._appListNotifier?.notify(Ok(RepeatedAppPB.deserializeBinary(payload).items));
break;
default:
break;
}
},
});
return undefined;
};
unsubscribe = async () => {
this._appListNotifier.unsubscribe();
this._workspaceNotifier.unsubscribe();
await this._listener?.stop();
};
}

View File

@ -0,0 +1,82 @@
import { nanoid } from '@reduxjs/toolkit';
import {
UserEventGetUserProfile,
UserEventSignIn,
UserEventSignOut,
UserEventSignUp,
UserEventUpdateUserProfile,
} from '../../../../services/backend/events/flowy-user';
import { SignInPayloadPB, SignUpPayloadPB } from '../../../../services/backend/models/flowy-user/auth';
import { UpdateUserProfilePayloadPB } from '../../../../services/backend/models/flowy-user/user_profile';
import { WorkspaceIdPB, CreateWorkspacePayloadPB } from '../../../../services/backend/models/flowy-folder/workspace';
import {
FolderEventCreateWorkspace,
FolderEventOpenWorkspace,
FolderEventReadWorkspaces,
} from '../../../../services/backend/events/flowy-folder';
export class UserBackendService {
constructor(public readonly userId: string) {}
getUserProfile = () => {
return UserEventGetUserProfile();
};
updateUserProfile = (params: { name?: string; password?: string; email?: string; openAIKey?: string }) => {
const payload = UpdateUserProfilePayloadPB.fromObject({ id: this.userId });
if (params.name !== undefined) {
payload.name = params.name;
}
if (params.password !== undefined) {
payload.password = params.password;
}
if (params.email !== undefined) {
payload.email = params.email;
}
// if (params.openAIKey !== undefined) {
// }
return UserEventUpdateUserProfile(payload);
};
getWorkspaces = () => {
const payload = WorkspaceIdPB.fromObject({});
return FolderEventReadWorkspaces(payload);
};
openWorkspace = (workspaceId: string) => {
const payload = WorkspaceIdPB.fromObject({ value: workspaceId });
return FolderEventOpenWorkspace(payload);
};
createWorkspace = (params: { name: string; desc: string }) => {
const payload = CreateWorkspacePayloadPB.fromObject({ name: params.name, desc: params.desc });
return FolderEventCreateWorkspace(payload);
};
signOut = () => {
return UserEventSignOut();
};
}
export class AuthBackendService {
signIn = (params: { email: string; password: string }) => {
const payload = SignInPayloadPB.fromObject({ email: params.email, password: params.password });
return UserEventSignIn(payload);
};
signUp = (params: { name: string; email: string; password: string }) => {
const payload = SignUpPayloadPB.fromObject({ name: params.name, email: params.email, password: params.password });
return UserEventSignUp(payload);
};
signOut = () => {
return UserEventSignOut();
};
autoSignUp = () => {
const password = 'AppFlowy123@';
const email = nanoid(4) + '@appflowy.io';
return this.signUp({ name: 'Me', email: email, password: password });
};
}

View File

@ -5,10 +5,7 @@ export interface IFolder {
title: string;
}
const initialState: IFolder[] = [
{ id: 'getting_started', title: 'Getting Started' },
{ id: 'my_folder', title: 'My Folder' },
];
const initialState: IFolder[] = [];
export const foldersSlice = createSlice({
name: 'folders',

View File

@ -1,19 +1,14 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
export type PageType = 'document' | 'grid' | 'board';
import { ViewLayoutTypePB } from '../../../../services/backend';
export interface IPage {
id: string;
title: string;
pageType: PageType;
pageType: ViewLayoutTypePB;
folderId: string;
}
const initialState: IPage[] = [
{ id: 'welcome_page', title: 'Welcome', pageType: 'document', folderId: 'getting_started' },
{ id: 'first_page', title: 'First Page', pageType: 'document', folderId: 'my_folder' },
{ id: 'second_page', title: 'Second Page', pageType: 'document', folderId: 'my_folder' },
];
const initialState: IPage[] = [];
export const pagesSlice = createSlice({
name: 'pages',

View File

@ -0,0 +1,17 @@
import { Subject } from 'rxjs';
export class ChangeNotifier<T> {
private subject = new Subject<T>();
notify(value: T) {
this.subject.next(value);
}
get observer() {
return this.subject.asObservable();
}
unsubscribe = () => {
this.subject.unsubscribe();
};
}

View File

@ -0,0 +1,21 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
export class Log {
static error(msg?: any) {
console.log(msg);
}
static info(msg?: any) {
console.log(msg);
}
static debug(msg?: any) {
console.log(msg);
}
static trace(msg?: any) {
console.log(msg);
}
static warn(msg?: any) {
console.log(msg);
}
}

View File

@ -1,2 +1,2 @@
export * from "./listener";
export * from "./parser";
export * from './observer';
export * from './parser';

View File

@ -1,29 +0,0 @@
import { listen, UnlistenFn } from "@tauri-apps/api/event";
import { FlowyError } from "../models/flowy-error";
import { SubscribeObject } from "../models/flowy-notification";
import { NotificationParser } from "./parser";
declare type OnError = (error: FlowyError) => void;
export abstract class AFNotificationListener<T> {
parser?: NotificationParser<T> | null;
private _listener?: UnlistenFn;
protected constructor(parser?: NotificationParser<T>) {
this.parser = parser;
}
async start() {
this._listener = await listen("af-notification", (notification) => {
let object = SubscribeObject.fromObject(notification.payload as {});
this.parser?.parse(object);
});
}
async stop() {
if (this._listener != null) {
this._listener();
}
this.parser = null;
}
}

View File

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

View File

@ -1,6 +1,5 @@
import { Ok, Err, Result } from "ts-results/result";
import { FlowyError } from "../models/flowy-error";
import { SubscribeObject } from "../models/flowy-notification";
import { FlowyError } from '../models/flowy-error';
import { SubscribeObject } from '../models/flowy-notification';
export declare type OnNotificationPayload<T> = (ty: T, payload: Uint8Array) => void;
export declare type OnNotificationError = (error: FlowyError) => void;
@ -8,12 +7,17 @@ export declare type NotificationTyParser<T> = (num: number) => T | null;
export declare type ErrParser<E> = (data: Uint8Array) => E;
export abstract class NotificationParser<T> {
id?: String;
id?: string;
onPayload: OnNotificationPayload<T>;
onError?: OnNotificationError;
tyParser: NotificationTyParser<T>;
constructor(onPayload: OnNotificationPayload<T>, tyParser: NotificationTyParser<T>, id?: String, onError?: OnNotificationError) {
constructor(
onPayload: OnNotificationPayload<T>,
tyParser: NotificationTyParser<T>,
id?: string,
onError?: OnNotificationError
) {
this.id = id;
this.onPayload = onPayload;
this.onError = onError;
@ -21,19 +25,19 @@ export abstract class NotificationParser<T> {
}
parse(subject: SubscribeObject) {
if (typeof this.id !== "undefined" && this.id.length == 0) {
if (subject.id != this.id) {
if (typeof this.id !== 'undefined' && this.id.length === 0) {
if (subject.id !== this.id) {
return;
}
}
let ty = this.tyParser(subject.ty);
if (ty == null) {
const ty = this.tyParser(subject.ty);
if (ty === null) {
return;
}
if (subject.has_error) {
let error = FlowyError.deserializeBinary(subject.error);
const error = FlowyError.deserializeBinary(subject.error);
this.onError?.(error);
} else {
this.onPayload(ty, subject.payload);

View File

@ -0,0 +1 @@
export {};

View File

@ -0,0 +1,42 @@
import { AuthBackendService, UserBackendService } from '../appflowy_app/stores/effects/user/backend_service';
import { randomFillSync } from 'crypto';
import { nanoid } from '@reduxjs/toolkit';
beforeAll(() => {
//@ts-ignore
window.crypto = {
// @ts-ignore
getRandomValues: function (buffer) {
// @ts-ignore
return randomFillSync(buffer);
},
};
});
describe('User backend service', () => {
it('sign up', async () => {
const service = new AuthBackendService();
const result = await service.autoSignUp();
expect(result.ok).toBeTruthy;
});
it('sign in', async () => {
const authService = new AuthBackendService();
const email = nanoid(4) + '@appflowy.io';
const password = nanoid(10);
const signUpResult = await authService.signUp({ name: 'nathan', email: email, password: password });
expect(signUpResult.ok).toBeTruthy;
const signInResult = await authService.signIn({ email: email, password: password });
expect(signInResult.ok).toBeTruthy;
});
it('get user profile', async () => {
const service = new AuthBackendService();
const result = await service.autoSignUp();
const userProfile = result.unwrap();
const userService = new UserBackendService(userProfile.id);
expect((await userService.getUserProfile()).unwrap()).toBe(userProfile);
});
});

View File

@ -0,0 +1,14 @@
describe('My Login application', () => {
it('should login with valid credentials', async () => {
await browser.url(`https://the-internet.herokuapp.com/login`)
await $('#username').setValue('tomsmith')
await $('#password').setValue('SuperSecretPassword!')
await $('button[type="submit"]').click()
await expect($('#flash')).toBeExisting()
await expect($('#flash')).toHaveTextContaining(
'You logged into a secure area!')
})
})

View File

@ -0,0 +1,13 @@
{
"compilerOptions": {
"moduleResolution": "node",
"module": "ESNext",
"types": [
"node",
"@wdio/globals/types",
"expect-webdriverio",
"@wdio/mocha-framework"
],
"target": "es2022"
}
}