From 8a2f5fe789a64010d25c463ef42bf45d973bd8fc Mon Sep 17 00:00:00 2001 From: "Nathan.fooo" <86001920+appflowy@users.noreply.github.com> Date: Sun, 19 Feb 2023 14:59:04 +0800 Subject: [PATCH] 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 --- .../app_flowy/lib/plugins/blank/blank.dart | 1 - .../application/field/field_controller.dart | 12 +- .../application/grid_data_controller.dart | 1 - .../grid/application/row/row_cache.dart | 2 - .../grid/application/row/row_list.dart | 17 - .../application/view/grid_view_cache.dart | 1 - .../lib/user/application/auth_service.dart | 2 +- .../user/presentation/skip_log_in_screen.dart | 2 +- .../lib/user/presentation/splash_screen.dart | 2 +- .../workspace/application/app/app_bloc.dart | 1 - .../application/app/app_service.dart | 3 - .../application/view/view_service.dart | 5 - .../test/bloc_test/board_test/util.dart | 1 - .../grid_test/filter/filter_util.dart | 1 - .../test/bloc_test/grid_test/util.dart | 1 - frontend/appflowy_tauri/.eslintignore | 9 +- frontend/appflowy_tauri/.eslintrc.cjs | 5 + frontend/appflowy_tauri/jest.config.cjs | 8 + frontend/appflowy_tauri/package.json | 11 +- .../NavigationPanel/FolderItem.hooks.ts | 23 +- .../layout/NavigationPanel/PageItem.tsx | 7 +- .../notifications/user_listener.ts | 4 +- .../effects/database/backend_service.ts | 50 +++ .../effects/database/cell/backend_service.ts | 36 ++ .../stores/effects/database/cell/cache.ts | 45 +++ .../effects/database/cell/cell_observer.ts | 40 +++ .../effects/database/cell/controller.ts | 123 +++++++ .../database/cell/controller_builder.ts | 141 ++++++++ .../effects/database/cell/data_parser.ts | 70 ++++ .../effects/database/cell/data_persistence.ts | 46 +++ .../stores/effects/database/controller.ts | 51 +++ .../effects/database/field/backend_service.ts | 87 +++++ .../effects/database/field/controller.ts | 129 +++++++ .../effects/database/field/field_observer.ts | 41 +++ .../database/notifications/observer.ts | 17 + .../database}/notifications/parser.ts | 0 .../stores/effects/database/row/cache.ts | 314 ++++++++++++++++++ .../stores/effects/database/view/cache.ts | 55 +++ .../effects/database/view/row_observer.ts | 71 ++++ .../stores/effects/folder/app/app_observer.ts | 38 +++ .../effects/folder/app/backend_service.ts | 98 ++++++ .../effects/folder/notifications/observer.ts | 17 + .../effects/folder/notifications/parser.ts | 26 ++ .../effects/folder/view/backend_service.ts | 32 ++ .../effects/folder/view/view_observer.ts | 74 +++++ .../folder/workspace/backend_service.ts | 57 ++++ .../folder/workspace/workspace_observer.ts | 46 +++ .../stores/effects/user/backend_service.ts | 82 +++++ .../stores/reducers/folders/slice.ts | 5 +- .../stores/reducers/pages/slice.ts | 11 +- .../src/appflowy_app/utils/change_notifier.ts | 17 + .../src/appflowy_app/utils/log.ts | 21 ++ .../services/backend/notifications/index.ts | 4 +- .../backend/notifications/listener.ts | 29 -- .../backend/notifications/observer.ts | 26 ++ .../services/backend/notifications/parser.ts | 24 +- .../appflowy_tauri/src/tests/helpers/init.ts | 1 + .../appflowy_tauri/src/tests/user.test.ts | 42 +++ .../appflowy_tauri/test/specs/example.e2e.ts | 14 + frontend/appflowy_tauri/test/tsconfig.json | 13 + 60 files changed, 2003 insertions(+), 109 deletions(-) create mode 100644 frontend/appflowy_tauri/jest.config.cjs create mode 100644 frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/backend_service.ts create mode 100644 frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/cell/backend_service.ts create mode 100644 frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/cell/cache.ts create mode 100644 frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/cell/cell_observer.ts create mode 100644 frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/cell/controller.ts create mode 100644 frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/cell/controller_builder.ts create mode 100644 frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/cell/data_parser.ts create mode 100644 frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/cell/data_persistence.ts create mode 100644 frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/controller.ts create mode 100644 frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/field/backend_service.ts create mode 100644 frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/field/controller.ts create mode 100644 frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/field/field_observer.ts create mode 100644 frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/notifications/observer.ts rename frontend/appflowy_tauri/src/appflowy_app/stores/{reducers/grid => effects/database}/notifications/parser.ts (100%) create mode 100644 frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/row/cache.ts create mode 100644 frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/view/cache.ts create mode 100644 frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/view/row_observer.ts create mode 100644 frontend/appflowy_tauri/src/appflowy_app/stores/effects/folder/app/app_observer.ts create mode 100644 frontend/appflowy_tauri/src/appflowy_app/stores/effects/folder/app/backend_service.ts create mode 100644 frontend/appflowy_tauri/src/appflowy_app/stores/effects/folder/notifications/observer.ts create mode 100644 frontend/appflowy_tauri/src/appflowy_app/stores/effects/folder/notifications/parser.ts create mode 100644 frontend/appflowy_tauri/src/appflowy_app/stores/effects/folder/view/backend_service.ts create mode 100644 frontend/appflowy_tauri/src/appflowy_app/stores/effects/folder/view/view_observer.ts create mode 100644 frontend/appflowy_tauri/src/appflowy_app/stores/effects/folder/workspace/backend_service.ts create mode 100644 frontend/appflowy_tauri/src/appflowy_app/stores/effects/folder/workspace/workspace_observer.ts create mode 100644 frontend/appflowy_tauri/src/appflowy_app/stores/effects/user/backend_service.ts create mode 100644 frontend/appflowy_tauri/src/appflowy_app/utils/change_notifier.ts create mode 100644 frontend/appflowy_tauri/src/appflowy_app/utils/log.ts delete mode 100644 frontend/appflowy_tauri/src/services/backend/notifications/listener.ts create mode 100644 frontend/appflowy_tauri/src/services/backend/notifications/observer.ts create mode 100644 frontend/appflowy_tauri/src/tests/helpers/init.ts create mode 100644 frontend/appflowy_tauri/src/tests/user.test.ts create mode 100644 frontend/appflowy_tauri/test/specs/example.e2e.ts create mode 100644 frontend/appflowy_tauri/test/tsconfig.json diff --git a/frontend/app_flowy/lib/plugins/blank/blank.dart b/frontend/app_flowy/lib/plugins/blank/blank.dart index ac4a6b9187..ce3f8356d7 100644 --- a/frontend/app_flowy/lib/plugins/blank/blank.dart +++ b/frontend/app_flowy/lib/plugins/blank/blank.dart @@ -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'; diff --git a/frontend/app_flowy/lib/plugins/grid/application/field/field_controller.dart b/frontend/app_flowy/lib/plugins/grid/application/field/field_controller.dart index 7c76c1d45c..811729016f 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/field/field_controller.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/field/field_controller.dart @@ -615,16 +615,16 @@ class GridFieldController { if (insertedFields.isEmpty) { return; } - final List newFields = fieldInfos; + final List 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 _updateFields(List updatedFieldPBs) { diff --git a/frontend/app_flowy/lib/plugins/grid/application/grid_data_controller.dart b/frontend/app_flowy/lib/plugins/grid/application/grid_data_controller.dart index 38c2bedd38..37fddb4ef9 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/grid_data_controller.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/grid_data_controller.dart @@ -58,7 +58,6 @@ class GridController { ); } - // Loads the rows from each block Future> openGrid() async { return _gridFFIService.openGrid().then((result) { return result.fold( diff --git a/frontend/app_flowy/lib/plugins/grid/application/row/row_cache.dart b/frontend/app_flowy/lib/plugins/grid/application/row/row_cache.dart index 1f505b3907..01113ff7c0 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/row/row_cache.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/row/row_cache.dart @@ -29,7 +29,6 @@ abstract class RowCacheDelegate { class GridRowCache { final String databaseId; - final List 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), diff --git a/frontend/app_flowy/lib/plugins/grid/application/row/row_list.dart b/frontend/app_flowy/lib/plugins/grid/application/row/row_list.dart index bd85c8c977..bd163cce58 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/row/row_list.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/row/row_list.dart @@ -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 markRowsAsInvisible(List rowIds) { - final List 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 rowIds) { _rowInfos.clear(); diff --git a/frontend/app_flowy/lib/plugins/grid/application/view/grid_view_cache.dart b/frontend/app_flowy/lib/plugins/grid/application/view/grid_view_cache.dart index b32a4e064c..6e9740a91b 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/view/grid_view_cache.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/view/grid_view_cache.dart @@ -21,7 +21,6 @@ class DatabaseViewCache { final delegate = GridRowFieldNotifierImpl(fieldController); _rowCache = GridRowCache( databaseId: databaseId, - rows: [], notifier: delegate, delegate: delegate, ); diff --git a/frontend/app_flowy/lib/user/application/auth_service.dart b/frontend/app_flowy/lib/user/application/auth_service.dart index ff82cee811..d32087c6ad 100644 --- a/frontend/app_flowy/lib/user/application/auth_service.dart +++ b/frontend/app_flowy/lib/user/application/auth_service.dart @@ -47,7 +47,7 @@ class AuthService { return UserEventSignOut().send(); } - Future> signUpWithRandomUser() { + Future> autoSignUp() { const password = "AppFlowy123@"; final uid = uuid(); final userEmail = "$uid@appflowy.io"; diff --git a/frontend/app_flowy/lib/user/presentation/skip_log_in_screen.dart b/frontend/app_flowy/lib/user/presentation/skip_log_in_screen.dart index 17b9515144..e394f98fe1 100644 --- a/frontend/app_flowy/lib/user/presentation/skip_log_in_screen.dart +++ b/frontend/app_flowy/lib/user/presentation/skip_log_in_screen.dart @@ -118,7 +118,7 @@ class _SkipLogInScreenState extends State { } Future _autoRegister(BuildContext context) async { - final result = await widget.authService.signUpWithRandomUser(); + final result = await widget.authService.autoSignUp(); result.fold( (user) { FolderEventReadCurrentWorkspace().send().then((result) { diff --git a/frontend/app_flowy/lib/user/presentation/splash_screen.dart b/frontend/app_flowy/lib/user/presentation/splash_screen.dart index 6ff452adce..e2c60447ce 100644 --- a/frontend/app_flowy/lib/user/presentation/splash_screen.dart +++ b/frontend/app_flowy/lib/user/presentation/splash_screen.dart @@ -90,7 +90,7 @@ class SplashScreen extends StatelessWidget { Future _registerIfNeeded() async { final result = await UserEventCheckUser().send(); if (!result.isLeft()) { - await getIt().signUpWithRandomUser(); + await getIt().autoSignUp(); } } } diff --git a/frontend/app_flowy/lib/workspace/application/app/app_bloc.dart b/frontend/app_flowy/lib/workspace/application/app/app_bloc.dart index f2851bf1e0..08ff21601d 100644 --- a/frontend/app_flowy/lib/workspace/application/app/app_bloc.dart +++ b/frontend/app_flowy/lib/workspace/application/app/app_bloc.dart @@ -100,7 +100,6 @@ class AppBloc extends Bloc { name: value.name, desc: value.desc ?? "", dataFormatType: value.pluginBuilder.dataFormatType, - pluginType: value.pluginBuilder.pluginType, layoutType: value.pluginBuilder.layoutType!, initialData: value.initialData, ); diff --git a/frontend/app_flowy/lib/workspace/application/app/app_service.dart b/frontend/app_flowy/lib/workspace/application/app/app_service.dart index 1a5826b4a4..31c7170a21 100644 --- a/frontend/app_flowy/lib/workspace/application/app/app_service.dart +++ b/frontend/app_flowy/lib/workspace/application/app/app_service.dart @@ -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> 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 diff --git a/frontend/app_flowy/lib/workspace/application/view/view_service.dart b/frontend/app_flowy/lib/workspace/application/view/view_service.dart index cfb986b532..7524a39e2c 100644 --- a/frontend/app_flowy/lib/workspace/application/view/view_service.dart +++ b/frontend/app_flowy/lib/workspace/application/view/view_service.dart @@ -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> readView({required String viewId}) { - final request = ViewIdPB(value: viewId); - return FolderEventReadView(request).send(); - } - Future> updateView( {required String viewId, String? name, String? desc}) { final request = UpdateViewPayloadPB.create()..viewId = viewId; diff --git a/frontend/app_flowy/test/bloc_test/board_test/util.dart b/frontend/app_flowy/test/bloc_test/board_test/util.dart index 5f1e8cdde2..18b1c54947 100644 --- a/frontend/app_flowy/test/bloc_test/board_test/util.dart +++ b/frontend/app_flowy/test/bloc_test/board_test/util.dart @@ -33,7 +33,6 @@ class AppFlowyBoardTest { appId: app.id, name: "Test Board", dataFormatType: builder.dataFormatType, - pluginType: builder.pluginType, layoutType: builder.layoutType!, ) .then((result) { diff --git a/frontend/app_flowy/test/bloc_test/grid_test/filter/filter_util.dart b/frontend/app_flowy/test/bloc_test/grid_test/filter/filter_util.dart index 123ab64dc7..9f10978cc6 100644 --- a/frontend/app_flowy/test/bloc_test/grid_test/filter/filter_util.dart +++ b/frontend/app_flowy/test/bloc_test/grid_test/filter/filter_util.dart @@ -12,7 +12,6 @@ Future createTestFilterGrid(AppFlowyGridTest gridTest) async { appId: app.id, name: "Filter Grid", dataFormatType: builder.dataFormatType, - pluginType: builder.pluginType, layoutType: builder.layoutType!, ) .then((result) { diff --git a/frontend/app_flowy/test/bloc_test/grid_test/util.dart b/frontend/app_flowy/test/bloc_test/grid_test/util.dart index 3e90577d00..26affed00b 100644 --- a/frontend/app_flowy/test/bloc_test/grid_test/util.dart +++ b/frontend/app_flowy/test/bloc_test/grid_test/util.dart @@ -168,7 +168,6 @@ class AppFlowyGridTest { appId: app.id, name: "Test Grid", dataFormatType: builder.dataFormatType, - pluginType: builder.pluginType, layoutType: builder.layoutType!, ) .then((result) { diff --git a/frontend/appflowy_tauri/.eslintignore b/frontend/appflowy_tauri/.eslintignore index fdb50c998f..e0ff674834 100644 --- a/frontend/appflowy_tauri/.eslintignore +++ b/frontend/appflowy_tauri/.eslintignore @@ -1,4 +1,7 @@ -/src/services -/src/styles +src/services +src/styles +node_modules/ +dist/ +src-tauri/ .eslintrc.cjs -node_modules \ No newline at end of file +tsconfig.json \ No newline at end of file diff --git a/frontend/appflowy_tauri/.eslintrc.cjs b/frontend/appflowy_tauri/.eslintrc.cjs index 92fbabeaab..ba23679073 100644 --- a/frontend/appflowy_tauri/.eslintrc.cjs +++ b/frontend/appflowy_tauri/.eslintrc.cjs @@ -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'], }; diff --git a/frontend/appflowy_tauri/jest.config.cjs b/frontend/appflowy_tauri/jest.config.cjs new file mode 100644 index 0000000000..b5ea02d82e --- /dev/null +++ b/frontend/appflowy_tauri/jest.config.cjs @@ -0,0 +1,8 @@ +/** @type {import('ts-jest').JestConfigWithTsJest} */ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + globals: { + window: {}, + }, +}; diff --git a/frontend/appflowy_tauri/package.json b/frontend/appflowy_tauri/package.json index d78fd52236..b0af06ca4d 100644 --- a/frontend/appflowy_tauri/package.json +++ b/frontend/appflowy_tauri/package.json @@ -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" } diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/layout/NavigationPanel/FolderItem.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/layout/NavigationPanel/FolderItem.hooks.ts index ce77e7b28e..60b3a14cbe 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/layout/NavigationPanel/FolderItem.hooks.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/layout/NavigationPanel/FolderItem.hooks.ts @@ -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 { diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/layout/NavigationPanel/PageItem.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/layout/NavigationPanel/PageItem.tsx index 80558c8e66..3873093e0c 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/layout/NavigationPanel/PageItem.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/layout/NavigationPanel/PageItem.tsx @@ -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: () = >
- {page.pageType === 'document' && } - {page.pageType === 'board' && } - {page.pageType === 'grid' && } + {page.pageType === ViewLayoutTypePB.Document && } + {page.pageType === ViewLayoutTypePB.Board && } + {page.pageType === ViewLayoutTypePB.Grid && }
{page.title}
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/user/application/notifications/user_listener.ts b/frontend/appflowy_tauri/src/appflowy_app/components/user/application/notifications/user_listener.ts index 8bd5a70d57..e5ca53dea1 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/user/application/notifications/user_listener.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/user/application/notifications/user_listener.ts @@ -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 { +export class UserNotificationListener extends AFNotificationObserver { onProfileUpdate?: OnUserProfileUpdate; onUserSignIn?: OnUserSignIn; diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/backend_service.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/backend_service.ts new file mode 100644 index 0000000000..2dfd598ff5 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/backend_service.ts @@ -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)); + }; +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/cell/backend_service.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/cell/backend_service.ts new file mode 100644 index 0000000000..aeae587faf --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/cell/backend_service.ts @@ -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 }; diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/cell/cache.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/cell/cache.ts new file mode 100644 index 0000000000..0e5f6692b4 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/cell/cache.ts @@ -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>(); + + 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(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; + } + } +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/cell/cell_observer.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/cell/cell_observer.ts new file mode 100644 index 0000000000..a021a6137d --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/cell/cell_observer.ts @@ -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; + +export type CellListenerCallback = (value: UpdateCellNotifiedValue) => void; + +export class CellObserver { + _notifier?: ChangeNotifier; + _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(); + }; +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/cell/controller.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/cell/controller.ts new file mode 100644 index 0000000000..1f55c9333c --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/cell/controller.ts @@ -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 { + _fieldBackendService: FieldBackendService; + _cellDataNotifier: CellDataNotifier; + _cellObserver: CellObserver; + _cacheKey: CellCacheKey; + + constructor( + public readonly cellIdentifier: CellIdentifier, + private readonly cellCache: CellCache, + private readonly fieldNotifier: CellFieldNotifier, + private readonly cellDataLoader: CellDataLoader, + private readonly cellDataPersistence: CellDataPersistence + ) { + 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

, 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 extends ChangeNotifier { + _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; + } +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/cell/controller_builder.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/cell/controller_builder.ts new file mode 100644 index 0000000000..3fe8036fcc --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/cell/controller_builder.ts @@ -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; + +export type CheckboxCellController = CellController; + +export type NumberCellController = CellController; + +export type SelectOptionCellController = CellController; + +export type ChecklistCellController = CellController; + +export type DateCellController = CellController; +export class CalendarData { + constructor(public readonly date: Date, public readonly time?: string) {} +} + +export type URLCellController = CellController; + +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( + 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( + 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( + 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( + 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( + 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( + this.cellIdentifier, + this.cellCache, + this._fieldNotifier, + loader, + persistence + ); + }; +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/cell/data_parser.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/cell/data_parser.ts new file mode 100644 index 0000000000..8ee30a8bd5 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/cell/data_parser.ts @@ -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 { + abstract parserData(data: Uint8Array): T | undefined; +} + +class CellDataLoader { + _service = new CellBackendService(); + + constructor( + readonly cellId: CellIdentifier, + readonly parser: CellDataParser, + 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 { + parserData(data: Uint8Array): string { + return utf8.decode(data.toString()); + } +} + +class DateCellDataParser extends CellDataParser { + parserData(data: Uint8Array): DateCellDataPB { + return DateCellDataPB.deserializeBinary(data); + } +} + +class SelectOptionCellDataParser extends CellDataParser { + parserData(data: Uint8Array): SelectOptionCellDataPB | undefined { + if (data.length === 0) { + return undefined; + } + return SelectOptionCellDataPB.deserializeBinary(data); + } +} + +class URLCellDataParser extends CellDataParser { + parserData(data: Uint8Array): URLCellDataPB | undefined { + if (data.length === 0) { + return undefined; + } + return URLCellDataPB.deserializeBinary(data); + } +} + +export { + StringCellDataParser, + DateCellDataParser, + SelectOptionCellDataParser, + URLCellDataParser, + CellDataLoader, + CellDataParser, +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/cell/data_persistence.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/cell/data_persistence.ts new file mode 100644 index 0000000000..17c8b366a2 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/cell/data_persistence.ts @@ -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 { + abstract save(data: D): Promise>; +} + +export class TextCellDataPersistence extends CellDataPersistence { + constructor(public readonly cellId: CellIdentifier) { + super(); + } + + save(data: string): Promise> { + return CellBackendService.updateCell(this.cellId, data); + } +} + +export class DateCellDataPersistence extends CellDataPersistence { + constructor(public readonly cellIdentifier: CellIdentifier) { + super(); + } + save(data: CalendarData): Promise> { + 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, + }); +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/controller.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/controller.ts new file mode 100644 index 0000000000..df3469f09c --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/controller.ts @@ -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(); + }; +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/field/backend_service.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/field/backend_service.ts new file mode 100644 index 0000000000..a0eee6df18 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/field/backend_service.ts @@ -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 { + 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); + }; +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/field/controller.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/field/controller.ts new file mode 100644 index 0000000000..86bd7bc276 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/field/controller.ts @@ -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 { + 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) {} +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/field/field_observer.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/field/field_observer.ts new file mode 100644 index 0000000000..e203a6be1e --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/field/field_observer.ts @@ -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; +export type DatabaseNotificationCallback = (value: UpdateFieldNotifiedValue) => void; + +export class DatabaseFieldObserver { + _notifier?: ChangeNotifier; + _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(); + }; +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/notifications/observer.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/notifications/observer.ts new file mode 100644 index 0000000000..317d829ad6 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/notifications/observer.ts @@ -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 { + constructor(params: { viewId?: string; parserHandler: ParserHandler; onError?: OnNotificationError }) { + const parser = new DatabaseNotificationParser({ + callback: params.parserHandler, + id: params.viewId, + onError: params.onError, + }); + super(parser); + } +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/grid/notifications/parser.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/notifications/parser.ts similarity index 100% rename from frontend/appflowy_tauri/src/appflowy_app/stores/reducers/grid/notifications/parser.ts rename to frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/notifications/parser.ts diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/row/cache.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/row/cache.ts new file mode 100644 index 0000000000..95660e9387 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/row/cache.ts @@ -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) => 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 => { + const cellIdentifierByFieldId: Map = 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 = 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(); + 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 { + _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(); + }; +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/view/cache.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/view/cache.ts new file mode 100644 index 0000000000..497e6354d2 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/view/cache.ts @@ -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); + } + }, + }); + }; +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/view/row_observer.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/view/row_observer.ts new file mode 100644 index 0000000000..c44c740bc8 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/view/row_observer.ts @@ -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; +export type RowsNotifyValue = Result; +export type ReorderRowsNotifyValue = Result; +export type ReorderSingleRowNotifyValue = Result; + +export class DatabaseViewRowsObserver { + _rowsVisibilityNotifier = new ChangeNotifier(); + _rowsNotifier = new ChangeNotifier(); + _reorderRowsNotifier = new ChangeNotifier(); + _reorderSingleRowNotifier = new ChangeNotifier(); + + _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(); + }; +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/folder/app/app_observer.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/folder/app/app_observer.ts new file mode 100644 index 0000000000..78607b4b1a --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/folder/app/app_observer.ts @@ -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; +export type AppUpdateNotifyCallback = (value: AppUpdateNotifyValue) => void; + +export class WorkspaceObserver { + _appNotifier = new ChangeNotifier(); + _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(); + }; +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/folder/app/backend_service.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/folder/app/backend_service.ts new file mode 100644 index 0000000000..54e0a4c6f9 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/folder/app/backend_service.ts @@ -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> => { + 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); + }; +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/folder/notifications/observer.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/folder/notifications/observer.ts new file mode 100644 index 0000000000..0090c7df69 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/folder/notifications/observer.ts @@ -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 { + constructor(params: { viewId?: string; parserHandler: ParserHandler; onError?: OnNotificationError }) { + const parser = new FolderNotificationParser({ + callback: params.parserHandler, + id: params.viewId, + onError: params.onError, + }); + super(parser); + } +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/folder/notifications/parser.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/folder/notifications/parser.ts new file mode 100644 index 0000000000..ee18dfcb7f --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/folder/notifications/parser.ts @@ -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 { + 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; +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/folder/view/backend_service.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/folder/view/backend_service.ts new file mode 100644 index 0000000000..fdb77a4747 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/folder/view/backend_service.ts @@ -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); + }; +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/folder/view/view_observer.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/folder/view/view_observer.ts new file mode 100644 index 0000000000..ea3ce6d077 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/folder/view/view_observer.ts @@ -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; +type UpdateViewNotifyValue = Result; +type RestoreViewNotifyValue = Result; +type MoveToTrashViewNotifyValue = Result; + +export class ViewObserver { + _deleteViewNotifier = new ChangeNotifier(); + _updateViewNotifier = new ChangeNotifier(); + _restoreViewNotifier = new ChangeNotifier(); + _moveToTashNotifier = new ChangeNotifier(); + _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(); + }; +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/folder/workspace/backend_service.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/folder/workspace/backend_service.ts new file mode 100644 index 0000000000..0d39844dbc --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/folder/workspace/backend_service.ts @@ -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); + }; +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/folder/workspace/workspace_observer.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/folder/workspace/workspace_observer.ts new file mode 100644 index 0000000000..e87bc4018d --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/folder/workspace/workspace_observer.ts @@ -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; +export type AppListNotifyCallback = (value: AppListNotifyValue) => void; +export type WorkspaceNotifyValue = Result; +export type WorkspaceNotifyCallback = (value: WorkspaceNotifyValue) => void; + +export class WorkspaceObserver { + _appListNotifier = new ChangeNotifier(); + _workspaceNotifier = new ChangeNotifier(); + _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(); + }; +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/user/backend_service.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/user/backend_service.ts new file mode 100644 index 0000000000..90ee60ee3f --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/user/backend_service.ts @@ -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 }); + }; +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/folders/slice.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/folders/slice.ts index a3541f9fbb..7065a0f0c7 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/folders/slice.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/folders/slice.ts @@ -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', diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/pages/slice.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/pages/slice.ts index aea1f78d2e..e564d9b1a0 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/pages/slice.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/pages/slice.ts @@ -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', diff --git a/frontend/appflowy_tauri/src/appflowy_app/utils/change_notifier.ts b/frontend/appflowy_tauri/src/appflowy_app/utils/change_notifier.ts new file mode 100644 index 0000000000..c3e112f80c --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/utils/change_notifier.ts @@ -0,0 +1,17 @@ +import { Subject } from 'rxjs'; + +export class ChangeNotifier { + private subject = new Subject(); + + notify(value: T) { + this.subject.next(value); + } + + get observer() { + return this.subject.asObservable(); + } + + unsubscribe = () => { + this.subject.unsubscribe(); + }; +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/utils/log.ts b/frontend/appflowy_tauri/src/appflowy_app/utils/log.ts new file mode 100644 index 0000000000..f695260397 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/utils/log.ts @@ -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); + } +} diff --git a/frontend/appflowy_tauri/src/services/backend/notifications/index.ts b/frontend/appflowy_tauri/src/services/backend/notifications/index.ts index cb9fedae5d..132155f3c8 100644 --- a/frontend/appflowy_tauri/src/services/backend/notifications/index.ts +++ b/frontend/appflowy_tauri/src/services/backend/notifications/index.ts @@ -1,2 +1,2 @@ -export * from "./listener"; -export * from "./parser"; +export * from './observer'; +export * from './parser'; diff --git a/frontend/appflowy_tauri/src/services/backend/notifications/listener.ts b/frontend/appflowy_tauri/src/services/backend/notifications/listener.ts deleted file mode 100644 index b206b58d43..0000000000 --- a/frontend/appflowy_tauri/src/services/backend/notifications/listener.ts +++ /dev/null @@ -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 { - parser?: NotificationParser | null; - private _listener?: UnlistenFn; - - protected constructor(parser?: NotificationParser) { - 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; - } -} diff --git a/frontend/appflowy_tauri/src/services/backend/notifications/observer.ts b/frontend/appflowy_tauri/src/services/backend/notifications/observer.ts new file mode 100644 index 0000000000..282886243c --- /dev/null +++ b/frontend/appflowy_tauri/src/services/backend/notifications/observer.ts @@ -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 { + parser?: NotificationParser | null; + private _listener?: UnlistenFn; + + protected constructor(parser?: NotificationParser) { + 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; + } +} diff --git a/frontend/appflowy_tauri/src/services/backend/notifications/parser.ts b/frontend/appflowy_tauri/src/services/backend/notifications/parser.ts index 1e520934fa..c74dd82b72 100644 --- a/frontend/appflowy_tauri/src/services/backend/notifications/parser.ts +++ b/frontend/appflowy_tauri/src/services/backend/notifications/parser.ts @@ -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 = (ty: T, payload: Uint8Array) => void; export declare type OnNotificationError = (error: FlowyError) => void; @@ -8,12 +7,17 @@ export declare type NotificationTyParser = (num: number) => T | null; export declare type ErrParser = (data: Uint8Array) => E; export abstract class NotificationParser { - id?: String; + id?: string; onPayload: OnNotificationPayload; onError?: OnNotificationError; tyParser: NotificationTyParser; - constructor(onPayload: OnNotificationPayload, tyParser: NotificationTyParser, id?: String, onError?: OnNotificationError) { + constructor( + onPayload: OnNotificationPayload, + tyParser: NotificationTyParser, + id?: string, + onError?: OnNotificationError + ) { this.id = id; this.onPayload = onPayload; this.onError = onError; @@ -21,19 +25,19 @@ export abstract class NotificationParser { } 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); diff --git a/frontend/appflowy_tauri/src/tests/helpers/init.ts b/frontend/appflowy_tauri/src/tests/helpers/init.ts new file mode 100644 index 0000000000..cb0ff5c3b5 --- /dev/null +++ b/frontend/appflowy_tauri/src/tests/helpers/init.ts @@ -0,0 +1 @@ +export {}; diff --git a/frontend/appflowy_tauri/src/tests/user.test.ts b/frontend/appflowy_tauri/src/tests/user.test.ts new file mode 100644 index 0000000000..d757e87e99 --- /dev/null +++ b/frontend/appflowy_tauri/src/tests/user.test.ts @@ -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); + }); +}); diff --git a/frontend/appflowy_tauri/test/specs/example.e2e.ts b/frontend/appflowy_tauri/test/specs/example.e2e.ts new file mode 100644 index 0000000000..be4a485a82 --- /dev/null +++ b/frontend/appflowy_tauri/test/specs/example.e2e.ts @@ -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!') + }) +}) + diff --git a/frontend/appflowy_tauri/test/tsconfig.json b/frontend/appflowy_tauri/test/tsconfig.json new file mode 100644 index 0000000000..ddef7c61c3 --- /dev/null +++ b/frontend/appflowy_tauri/test/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "moduleResolution": "node", + "module": "ESNext", + "types": [ + "node", + "@wdio/globals/types", + "expect-webdriverio", + "@wdio/mocha-framework" + ], + "target": "es2022" + } +} \ No newline at end of file