mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
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:
parent
7c3a823078
commit
8a2f5fe789
@ -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';
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -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(
|
||||
|
@ -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),
|
||||
|
@ -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();
|
||||
|
||||
|
@ -21,7 +21,6 @@ class DatabaseViewCache {
|
||||
final delegate = GridRowFieldNotifierImpl(fieldController);
|
||||
_rowCache = GridRowCache(
|
||||
databaseId: databaseId,
|
||||
rows: [],
|
||||
notifier: delegate,
|
||||
delegate: delegate,
|
||||
);
|
||||
|
@ -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";
|
||||
|
@ -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) {
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
);
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
|
@ -33,7 +33,6 @@ class AppFlowyBoardTest {
|
||||
appId: app.id,
|
||||
name: "Test Board",
|
||||
dataFormatType: builder.dataFormatType,
|
||||
pluginType: builder.pluginType,
|
||||
layoutType: builder.layoutType!,
|
||||
)
|
||||
.then((result) {
|
||||
|
@ -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) {
|
||||
|
@ -168,7 +168,6 @@ class AppFlowyGridTest {
|
||||
appId: app.id,
|
||||
name: "Test Grid",
|
||||
dataFormatType: builder.dataFormatType,
|
||||
pluginType: builder.pluginType,
|
||||
layoutType: builder.layoutType!,
|
||||
)
|
||||
.then((result) {
|
||||
|
@ -1,4 +1,7 @@
|
||||
/src/services
|
||||
/src/styles
|
||||
src/services
|
||||
src/styles
|
||||
node_modules/
|
||||
dist/
|
||||
src-tauri/
|
||||
.eslintrc.cjs
|
||||
node_modules
|
||||
tsconfig.json
|
@ -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'],
|
||||
};
|
||||
|
8
frontend/appflowy_tauri/jest.config.cjs
Normal file
8
frontend/appflowy_tauri/jest.config.cjs
Normal file
@ -0,0 +1,8 @@
|
||||
/** @type {import('ts-jest').JestConfigWithTsJest} */
|
||||
module.exports = {
|
||||
preset: 'ts-jest',
|
||||
testEnvironment: 'node',
|
||||
globals: {
|
||||
window: {},
|
||||
},
|
||||
};
|
@ -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"
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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));
|
||||
};
|
||||
}
|
@ -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 };
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
};
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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
|
||||
);
|
||||
};
|
||||
}
|
@ -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,
|
||||
};
|
@ -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,
|
||||
});
|
||||
}
|
@ -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();
|
||||
};
|
||||
}
|
@ -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);
|
||||
};
|
||||
}
|
@ -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) {}
|
||||
}
|
@ -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();
|
||||
};
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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();
|
||||
};
|
||||
}
|
@ -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);
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
@ -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();
|
||||
};
|
||||
}
|
@ -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();
|
||||
};
|
||||
}
|
@ -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);
|
||||
};
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
};
|
@ -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);
|
||||
};
|
||||
}
|
@ -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();
|
||||
};
|
||||
}
|
@ -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);
|
||||
};
|
||||
}
|
@ -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();
|
||||
};
|
||||
}
|
@ -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 });
|
||||
};
|
||||
}
|
@ -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',
|
||||
|
@ -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',
|
||||
|
@ -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();
|
||||
};
|
||||
}
|
21
frontend/appflowy_tauri/src/appflowy_app/utils/log.ts
Normal file
21
frontend/appflowy_tauri/src/appflowy_app/utils/log.ts
Normal 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);
|
||||
}
|
||||
}
|
@ -1,2 +1,2 @@
|
||||
export * from "./listener";
|
||||
export * from "./parser";
|
||||
export * from './observer';
|
||||
export * from './parser';
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
|
1
frontend/appflowy_tauri/src/tests/helpers/init.ts
Normal file
1
frontend/appflowy_tauri/src/tests/helpers/init.ts
Normal file
@ -0,0 +1 @@
|
||||
export {};
|
42
frontend/appflowy_tauri/src/tests/user.test.ts
Normal file
42
frontend/appflowy_tauri/src/tests/user.test.ts
Normal 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);
|
||||
});
|
||||
});
|
14
frontend/appflowy_tauri/test/specs/example.e2e.ts
Normal file
14
frontend/appflowy_tauri/test/specs/example.e2e.ts
Normal 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!')
|
||||
})
|
||||
})
|
||||
|
13
frontend/appflowy_tauri/test/tsconfig.json
Normal file
13
frontend/appflowy_tauri/test/tsconfig.json
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"moduleResolution": "node",
|
||||
"module": "ESNext",
|
||||
"types": [
|
||||
"node",
|
||||
"@wdio/globals/types",
|
||||
"expect-webdriverio",
|
||||
"@wdio/mocha-framework"
|
||||
],
|
||||
"target": "es2022"
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user