From 8e22ef223007a3d74fba03d64919c9d0b7f58245 Mon Sep 17 00:00:00 2001 From: "Nathan.fooo" <86001920+appflowy@users.noreply.github.com> Date: Sun, 5 Mar 2023 16:13:06 +0800 Subject: [PATCH 1/8] chore: observer app change (#1922) --- .../NavigationPanel/FolderItem.hooks.ts | 34 +++++++++++++++++-- .../NavigationPanel/NavigationPanel.hooks.ts | 3 -- .../stores/effects/folder/app/app_observer.ts | 5 ++- .../stores/reducers/pages/slice.ts | 8 +++++ 4 files changed, 42 insertions(+), 8 deletions(-) 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 4fea0c973b..81f762c298 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 @@ -2,10 +2,11 @@ import { foldersActions, IFolder } from '../../../stores/reducers/folders/slice' import { useEffect, useState } from 'react'; import { useAppDispatch, useAppSelector } from '../../../stores/store'; import { IPage, pagesActions } from '../../../stores/reducers/pages/slice'; -import { ViewLayoutTypePB } from '../../../../services/backend'; +import { AppPB, ViewLayoutTypePB } from '../../../../services/backend'; import { AppBackendService } from '../../../stores/effects/folder/app/app_bd_svc'; import { WorkspaceBackendService } from '../../../stores/effects/folder/workspace/workspace_bd_svc'; import { useError } from '../../error/Error.hooks'; +import { AppObserver } from '../../../stores/effects/folder/app/app_observer'; const initialFolderHeight = 40; const initialPageHeight = 40; @@ -13,19 +14,48 @@ const animationDuration = 500; export const useFolderEvents = (folder: IFolder, pages: IPage[]) => { const appDispatch = useAppDispatch(); + const workspace = useAppSelector((state) => state.workspace); + // Actions const [showPages, setShowPages] = useState(false); const [showFolderOptions, setShowFolderOptions] = useState(false); const [showNewPageOptions, setShowNewPageOptions] = useState(false); const [showRenamePopup, setShowRenamePopup] = useState(false); + // UI configurations const [folderHeight, setFolderHeight] = useState(`${initialFolderHeight}px`); - const workspace = useAppSelector((state) => state.workspace); + // Observers + const appObserver = new AppObserver(folder.id); + // Backend services const appBackendService = new AppBackendService(folder.id); const workspaceBackendService = new WorkspaceBackendService(workspace.id || ''); + + // Error const error = useError(); + + useEffect(() => { + void appObserver.subscribe({ + onAppChanged: (change) => { + if (change.ok) { + const app: AppPB = change.val; + const updatedPages: IPage[] = app.belongings.items.map((view) => ({ + id: view.id, + folderId: view.app_id, + pageType: view.layout, + title: view.name, + })); + appDispatch(pagesActions.didReceivePages(updatedPages)); + } + }, + }); + return () => { + // Unsubscribe when the component is unmounted. + void appObserver.unsubscribe(); + }; + }, []); + useEffect(() => { if (showPages) { setFolderHeight(`${initialFolderHeight + pages.length * initialPageHeight}px`); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/layout/NavigationPanel/NavigationPanel.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/layout/NavigationPanel/NavigationPanel.hooks.ts index 26c8d40011..07de52e2b1 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/layout/NavigationPanel/NavigationPanel.hooks.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/layout/NavigationPanel/NavigationPanel.hooks.ts @@ -56,13 +56,10 @@ export const useNavigationPanelHooks = function () { return { width, - folders, pages, - navigate, onPageClick, - onCollapseNavigationClick, onFixNavigationClick, navigationPanelFixed, 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 index 377541fc3a..1af76beffc 100644 --- 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 @@ -3,11 +3,10 @@ import { AppPB, FlowyError, FolderNotification } from '../../../../../services/b import { ChangeNotifier } from '../../../../utils/change_notifier'; import { FolderNotificationObserver } from '../notifications/observer'; -export type AppUpdateNotifyValue = Result; -export type AppUpdateNotifyCallback = (value: AppUpdateNotifyValue) => void; +export type AppUpdateNotifyCallback = (value: Result) => void; export class AppObserver { - _appNotifier = new ChangeNotifier(); + _appNotifier = new ChangeNotifier>(); _listener?: FolderNotificationObserver; constructor(public readonly appId: string) {} 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 cb09fdbc58..330eeb5968 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 @@ -14,6 +14,14 @@ export const pagesSlice = createSlice({ name: 'pages', initialState: initialState, reducers: { + didReceivePages(state, action: PayloadAction) { + action.payload.forEach((updatedPage) => { + const index = state.findIndex((page) => page.id === updatedPage.id); + if (index !== -1) { + state.splice(index, 1, updatedPage); + } + }); + }, addPage(state, action: PayloadAction) { state.push(action.payload); }, From 7e7cee4bf477a705ddc1b38822b00cbbf7a8469a Mon Sep 17 00:00:00 2001 From: "Nathan.fooo" <86001920+appflowy@users.noreply.github.com> Date: Sun, 5 Mar 2023 16:26:27 +0800 Subject: [PATCH 2/8] chore: add tauri database group test (#1924) * chore: add tauri database group test * chore: add more tests * chore: enable run all tests * chore: rename test folder --- .../application/database_service.dart | 4 +- .../database_view/application/defines.dart | 2 +- .../application/field/field_service.dart | 2 +- .../application/filter/filter_service.dart | 2 +- .../application/row/row_service.dart | 2 +- .../application/setting/setting_service.dart | 2 +- .../application/sort/sort_service.dart | 2 +- .../board/application/group_controller.dart | 26 +- .../appflowy_tauri/src/appflowy_app/App.tsx | 2 +- .../components/TestApiButton/TestGrid.tsx | 334 ----------------- .../DatabaseTestHelper.ts | 45 ++- .../{TestApiButton => tests}/TestAPI.tsx | 19 +- .../TestApiButton.tsx | 0 .../components/tests/TestGrid.tsx | 349 ++++++++++++++++++ .../components/tests/TestGroup.tsx | 150 ++++++++ .../effects/database/database_bd_svc.ts | 44 +++ .../effects/database/database_controller.ts | 131 ++++++- .../database/field/field_controller.ts | 16 +- .../effects/database/field/field_observer.ts | 14 +- .../database/group/group_controller.ts | 149 ++++++++ .../effects/database/group/group_observer.ts | 58 +++ .../stores/effects/database/row/row_cache.ts | 8 +- .../database/view/database_view_cache.ts | 16 +- .../src/entities/database_entities.rs | 181 +++++++++ .../src/entities/grid_entities.rs | 155 -------- .../flowy-database/src/entities/mod.rs | 3 +- .../flowy-database/src/event_handler.rs | 11 + .../rust-lib/flowy-database/src/event_map.rs | 8 +- .../rust-lib/flowy-database/src/manager.rs | 8 +- .../src/services/database/database_editor.rs | 5 + .../src/services/database_view/editor.rs | 12 +- .../services/database_view/editor_manager.rs | 8 +- 32 files changed, 1187 insertions(+), 581 deletions(-) delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/TestApiButton/TestGrid.tsx rename frontend/appflowy_tauri/src/appflowy_app/components/{TestApiButton => tests}/DatabaseTestHelper.ts (76%) rename frontend/appflowy_tauri/src/appflowy_app/components/{TestApiButton => tests}/TestAPI.tsx (59%) rename frontend/appflowy_tauri/src/appflowy_app/components/{TestApiButton => tests}/TestApiButton.tsx (100%) create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/tests/TestGrid.tsx create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/tests/TestGroup.tsx create mode 100644 frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/group/group_controller.ts create mode 100644 frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/group/group_observer.ts delete mode 100644 frontend/rust-lib/flowy-database/src/entities/grid_entities.rs diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/database_service.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/database_service.dart index 9f56d1f443..1dcf433d01 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/application/database_service.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/database_service.dart @@ -1,9 +1,9 @@ +import 'package:appflowy_backend/protobuf/flowy-database/database_entities.pb.dart'; import 'package:dartz/dartz.dart'; import 'package:appflowy_backend/dispatch/dispatch.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-database/field_entities.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/grid_entities.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-database/group.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-database/row_entities.pb.dart'; @@ -60,6 +60,6 @@ class DatabaseBackendService { Future> loadGroups() { final payload = DatabaseViewIdPB(value: viewId); - return DatabaseEventGetGroup(payload).send(); + return DatabaseEventGetGroups(payload).send(); } } diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/defines.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/defines.dart index 1e5beccbc2..d724028e63 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/application/defines.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/defines.dart @@ -1,6 +1,6 @@ import 'dart:collection'; -import 'package:appflowy_backend/protobuf/flowy-database/grid_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database/database_entities.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; import '../grid/presentation/widgets/filter/filter_info.dart'; diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/field/field_service.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/field/field_service.dart index c005870310..86f8983d21 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/application/field/field_service.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/field/field_service.dart @@ -1,8 +1,8 @@ +import 'package:appflowy_backend/protobuf/flowy-database/database_entities.pb.dart'; import 'package:dartz/dartz.dart'; import 'package:appflowy_backend/dispatch/dispatch.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-database/field_entities.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/grid_entities.pb.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; part 'field_service.freezed.dart'; diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/filter/filter_service.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/filter/filter_service.dart index aaea678ece..86cdb513a5 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/application/filter/filter_service.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/filter/filter_service.dart @@ -1,3 +1,4 @@ +import 'package:appflowy_backend/protobuf/flowy-database/database_entities.pb.dart'; import 'package:dartz/dartz.dart'; import 'package:appflowy_backend/dispatch/dispatch.dart'; import 'package:appflowy_backend/log.dart'; @@ -6,7 +7,6 @@ import 'package:appflowy_backend/protobuf/flowy-database/checkbox_filter.pbserve import 'package:appflowy_backend/protobuf/flowy-database/checklist_filter.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-database/date_filter.pbserver.dart'; import 'package:appflowy_backend/protobuf/flowy-database/field_entities.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/grid_entities.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-database/number_filter.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-database/select_option_filter.pbserver.dart'; import 'package:appflowy_backend/protobuf/flowy-database/setting_entities.pb.dart'; diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/row/row_service.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/row/row_service.dart index 58553c8b87..d0e74186e7 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/application/row/row_service.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/row/row_service.dart @@ -1,7 +1,7 @@ +import 'package:appflowy_backend/protobuf/flowy-database/database_entities.pb.dart'; import 'package:dartz/dartz.dart'; import 'package:appflowy_backend/dispatch/dispatch.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/grid_entities.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-database/group_changeset.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-database/row_entities.pb.dart'; diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/setting/setting_service.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/setting/setting_service.dart index 54bdb85e48..63ff0449cb 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/application/setting/setting_service.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/setting/setting_service.dart @@ -1,8 +1,8 @@ +import 'package:appflowy_backend/protobuf/flowy-database/database_entities.pb.dart'; import 'package:dartz/dartz.dart'; import 'package:appflowy_backend/dispatch/dispatch.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-database/field_entities.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/grid_entities.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-database/group.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-database/setting_entities.pb.dart'; diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/sort/sort_service.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/sort/sort_service.dart index f7b4999878..530dfb11c2 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/application/sort/sort_service.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/sort/sort_service.dart @@ -1,9 +1,9 @@ +import 'package:appflowy_backend/protobuf/flowy-database/database_entities.pb.dart'; import 'package:dartz/dartz.dart'; import 'package:appflowy_backend/dispatch/dispatch.dart'; import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-database/field_entities.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/grid_entities.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-database/setting_entities.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-database/sort_entities.pb.dart'; diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/board/application/group_controller.dart b/frontend/appflowy_flutter/lib/plugins/database_view/board/application/group_controller.dart index 8fd1368df1..edc4160b39 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/board/application/group_controller.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/board/application/group_controller.dart @@ -68,9 +68,8 @@ class GroupController { if (index != -1) { group.rows[index] = updatedRow; + delegate.updateRow(group, updatedRow); } - - delegate.updateRow(group, updatedRow); } }, (err) => Log.error(err), @@ -78,29 +77,6 @@ class GroupController { }); } - // GroupChangesetPB _transformChangeset(GroupChangesetPB changeset) { - // final insertedRows = changeset.insertedRows - // .where( - // (delete) => !changeset.deletedRows.contains(delete.row.id), - // ) - // .toList(); - - // final deletedRows = changeset.deletedRows - // .where((deletedRowId) => - // changeset.insertedRows - // .indexWhere((insert) => insert.row.id == deletedRowId) == - // -1) - // .toList(); - - // return changeset.rebuild((rebuildChangeset) { - // rebuildChangeset.insertedRows.clear(); - // rebuildChangeset.insertedRows.addAll(insertedRows); - - // rebuildChangeset.deletedRows.clear(); - // rebuildChangeset.deletedRows.addAll(deletedRows); - // }); - // } - Future dispose() async { _listener.stop(); } diff --git a/frontend/appflowy_tauri/src/appflowy_app/App.tsx b/frontend/appflowy_tauri/src/appflowy_app/App.tsx index efb4736264..ffdc433918 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/App.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/App.tsx @@ -12,7 +12,7 @@ import { SignUpPage } from './views/SignUpPage'; import { ConfirmAccountPage } from './views/ConfirmAccountPage'; import { ErrorHandlerPage } from './components/error/ErrorHandlerPage'; import initializeI18n from './stores/i18n/initializeI18n'; -import { TestAPI } from './components/TestApiButton/TestAPI'; +import { TestAPI } from './components/tests/TestAPI'; import { GetStarted } from './components/auth/GetStarted/GetStarted'; initializeI18n(); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/TestApiButton/TestGrid.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/TestApiButton/TestGrid.tsx deleted file mode 100644 index 6b1eb26c2f..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/components/TestApiButton/TestGrid.tsx +++ /dev/null @@ -1,334 +0,0 @@ -import React from 'react'; -import { - FieldType, - NumberFormat, - NumberTypeOptionPB, - SelectOptionCellDataPB, - SingleSelectTypeOptionPB, - ViewLayoutTypePB, -} from '../../../services/backend'; -import { Log } from '../../utils/log'; -import { - assertFieldName, - assertNumberOfFields, - assertNumberOfRows, - assertTextCell, - createTestDatabaseView, - editTextCell, - findFirstFieldInfoWithFieldType, - makeMultiSelectCellController, - makeSingleSelectCellController, - makeTextCellController, - openTestDatabase, -} from './DatabaseTestHelper'; -import { - SelectOptionBackendService, - SelectOptionCellBackendService, -} from '../../stores/effects/database/cell/select_option_bd_svc'; -import { TypeOptionController } from '../../stores/effects/database/field/type_option/type_option_controller'; -import { None, Some } from 'ts-results'; -import { RowBackendService } from '../../stores/effects/database/row/row_bd_svc'; -import { - makeNumberTypeOptionContext, - makeSingleSelectTypeOptionContext, -} from '../../stores/effects/database/field/type_option/type_option_context'; - -export const TestCreateGrid = () => { - async function createBuildInGrid() { - const view = await createTestDatabaseView(ViewLayoutTypePB.Grid); - const databaseController = await openTestDatabase(view.id); - databaseController.subscribe({ - onViewChanged: (databasePB) => { - Log.debug('Did receive database:' + databasePB); - }, - // onRowsChanged: async (rows) => { - // if (rows.length !== 3) { - // throw Error('Expected number of rows is 3, but receive ' + rows.length); - // } - // }, - onFieldsChanged: (fields) => { - if (fields.length !== 3) { - throw Error('Expected number of fields is 3, but receive ' + fields.length); - } - }, - }); - await databaseController.open().then((result) => result.unwrap()); - await databaseController.dispose(); - } - - return TestButton('Test create build-in grid', createBuildInGrid); -}; - -export const TestEditCell = () => { - async function testGridRow() { - const view = await createTestDatabaseView(ViewLayoutTypePB.Grid); - const databaseController = await openTestDatabase(view.id); - await databaseController.open().then((result) => result.unwrap()); - - for (const [index, row] of databaseController.databaseViewCache.rowInfos.entries()) { - const cellContent = index.toString(); - const fieldInfo = findFirstFieldInfoWithFieldType(row, FieldType.RichText).unwrap(); - await editTextCell(fieldInfo.field.id, row, databaseController, cellContent); - await assertTextCell(fieldInfo.field.id, row, databaseController, cellContent); - } - } - - return TestButton('Test editing cell', testGridRow); -}; - -export const TestCreateRow = () => { - async function testCreateRow() { - const view = await createTestDatabaseView(ViewLayoutTypePB.Grid); - const databaseController = await openTestDatabase(view.id); - await databaseController.open().then((result) => result.unwrap()); - await assertNumberOfRows(view.id, 3); - - // Create a row from a DatabaseController or create using the RowBackendService - await databaseController.createRow(); - await assertNumberOfRows(view.id, 4); - await databaseController.dispose(); - } - - return TestButton('Test create row', testCreateRow); -}; -export const TestDeleteRow = () => { - async function testDeleteRow() { - const view = await createTestDatabaseView(ViewLayoutTypePB.Grid); - const databaseController = await openTestDatabase(view.id); - await databaseController.open().then((result) => result.unwrap()); - - const rows = databaseController.databaseViewCache.rowInfos; - const svc = new RowBackendService(view.id); - await svc.deleteRow(rows[0].row.id); - await assertNumberOfRows(view.id, 2); - - // Wait the databaseViewCache get the change notification and - // update the rows. - await new Promise((resolve) => setTimeout(resolve, 200)); - if (databaseController.databaseViewCache.rowInfos.length !== 2) { - throw Error('The number of rows is not match'); - } - await databaseController.dispose(); - } - - return TestButton('Test delete row', testDeleteRow); -}; -export const TestCreateSelectOptionInCell = () => { - async function testCreateOptionInCell() { - const view = await createTestDatabaseView(ViewLayoutTypePB.Grid); - const databaseController = await openTestDatabase(view.id); - await databaseController.open().then((result) => result.unwrap()); - for (const [index, row] of databaseController.databaseViewCache.rowInfos.entries()) { - if (index === 0) { - const fieldInfo = findFirstFieldInfoWithFieldType(row, FieldType.SingleSelect).unwrap(); - const cellController = await makeSingleSelectCellController(fieldInfo.field.id, row, databaseController).then( - (result) => result.unwrap() - ); - await cellController.subscribeChanged({ - onCellChanged: (value) => { - if (value.some) { - const option: SelectOptionCellDataPB = value.unwrap(); - console.log(option); - } - }, - }); - const backendSvc = new SelectOptionCellBackendService(cellController.cellIdentifier); - await backendSvc.createOption({ name: 'option' + index }); - await cellController.dispose(); - } - } - await databaseController.dispose(); - } - - return TestButton('Test create a select option in cell', testCreateOptionInCell); -}; - -export const TestGetSingleSelectFieldData = () => { - async function testGetSingleSelectFieldData() { - const view = await createTestDatabaseView(ViewLayoutTypePB.Grid); - const databaseController = await openTestDatabase(view.id); - await databaseController.open().then((result) => result.unwrap()); - - // Find the single select column - const singleSelect = databaseController.fieldController.fieldInfos.find( - (fieldInfo) => fieldInfo.field.field_type === FieldType.SingleSelect - )!; - const typeOptionController = new TypeOptionController(view.id, Some(singleSelect)); - const singleSelectTypeOptionContext = makeSingleSelectTypeOptionContext(typeOptionController); - - // Create options - const singleSelectTypeOptionPB: SingleSelectTypeOptionPB = await singleSelectTypeOptionContext - .getTypeOption() - .then((result) => result.unwrap()); - const backendSvc = new SelectOptionBackendService(view.id, singleSelect.field.id); - const option1 = await backendSvc.createOption({ name: 'Task 1' }).then((result) => result.unwrap()); - singleSelectTypeOptionPB.options.splice(0, 0, option1); - const option2 = await backendSvc.createOption({ name: 'Task 2' }).then((result) => result.unwrap()); - singleSelectTypeOptionPB.options.splice(0, 0, option2); - const option3 = await backendSvc.createOption({ name: 'Task 3' }).then((result) => result.unwrap()); - singleSelectTypeOptionPB.options.splice(0, 0, option3); - await singleSelectTypeOptionContext.setTypeOption(singleSelectTypeOptionPB); - - // Read options - const options = singleSelectTypeOptionPB.options; - console.log(options); - - await databaseController.dispose(); - } - - return TestButton('Test get single-select column data', testGetSingleSelectFieldData); -}; - -export const TestSwitchFromSingleSelectToNumber = () => { - async function testSwitchFromSingleSelectToNumber() { - const view = await createTestDatabaseView(ViewLayoutTypePB.Grid); - const databaseController = await openTestDatabase(view.id); - await databaseController.open().then((result) => result.unwrap()); - - // Find the single select column - const singleSelect = databaseController.fieldController.fieldInfos.find( - (fieldInfo) => fieldInfo.field.field_type === FieldType.SingleSelect - )!; - const typeOptionController = new TypeOptionController(view.id, Some(singleSelect)); - await typeOptionController.switchToField(FieldType.Number); - - // Check the number type option - const numberTypeOptionContext = makeNumberTypeOptionContext(typeOptionController); - const numberTypeOption: NumberTypeOptionPB = await numberTypeOptionContext - .getTypeOption() - .then((result) => result.unwrap()); - const format: NumberFormat = numberTypeOption.format; - if (format !== NumberFormat.Num) { - throw Error('The default format should be number'); - } - - await databaseController.dispose(); - } - - return TestButton('Test switch from single-select to number column', testSwitchFromSingleSelectToNumber); -}; - -export const TestSwitchFromMultiSelectToText = () => { - async function testSwitchFromMultiSelectToRichText() { - const view = await createTestDatabaseView(ViewLayoutTypePB.Grid); - const databaseController = await openTestDatabase(view.id); - await databaseController.open().then((result) => result.unwrap()); - - // Create multi-select field - const typeOptionController = new TypeOptionController(view.id, None, FieldType.MultiSelect); - await typeOptionController.initialize(); - - // Insert options to first row - const row = databaseController.databaseViewCache.rowInfos[0]; - const multiSelectField = typeOptionController.getFieldInfo(); - // const multiSelectField = findFirstFieldInfoWithFieldType(row, FieldType.MultiSelect).unwrap(); - const selectOptionCellController = await makeMultiSelectCellController( - multiSelectField.field.id, - row, - databaseController - ).then((result) => result.unwrap()); - const backendSvc = new SelectOptionCellBackendService(selectOptionCellController.cellIdentifier); - await backendSvc.createOption({ name: 'A' }); - await backendSvc.createOption({ name: 'B' }); - await backendSvc.createOption({ name: 'C' }); - - const selectOptionCellData = await selectOptionCellController.getCellData().then((result) => result.unwrap()); - if (selectOptionCellData.options.length !== 3) { - throw Error('The options should equal to 3'); - } - - if (selectOptionCellData.select_options.length !== 3) { - throw Error('The selected options should equal to 3'); - } - await selectOptionCellController.dispose(); - - // Switch to RichText field type - await typeOptionController.switchToField(FieldType.RichText).then((result) => result.unwrap()); - if (typeOptionController.fieldType !== FieldType.RichText) { - throw Error('The field type should be text'); - } - - const textCellController = await makeTextCellController(multiSelectField.field.id, row, databaseController).then( - (result) => result.unwrap() - ); - const cellContent = await textCellController.getCellData(); - if (cellContent.unwrap() !== 'A,B,C') { - throw Error('The cell content should be A,B,C, but receive: ' + cellContent.unwrap()); - } - - await databaseController.dispose(); - } - - return TestButton('Test switch from multi-select to text column', testSwitchFromMultiSelectToRichText); -}; - -export const TestEditField = () => { - async function testEditField() { - const view = await createTestDatabaseView(ViewLayoutTypePB.Grid); - const databaseController = await openTestDatabase(view.id); - await databaseController.open().then((result) => result.unwrap()); - const fieldInfos = databaseController.fieldController.fieldInfos; - - // Modify the name of the field - const firstFieldInfo = fieldInfos[0]; - const controller = new TypeOptionController(view.id, Some(firstFieldInfo)); - await controller.initialize(); - const newName = 'hello world'; - await controller.setFieldName(newName); - - await assertFieldName(view.id, firstFieldInfo.field.id, firstFieldInfo.field.field_type, newName); - await databaseController.dispose(); - } - - return TestButton('Test edit the column name', testEditField); -}; - -export const TestCreateNewField = () => { - async function testCreateNewField() { - const view = await createTestDatabaseView(ViewLayoutTypePB.Grid); - const databaseController = await openTestDatabase(view.id); - await databaseController.open().then((result) => result.unwrap()); - await assertNumberOfFields(view.id, 3); - - // Modify the name of the field - const controller = new TypeOptionController(view.id, None); - await controller.initialize(); - await assertNumberOfFields(view.id, 4); - await databaseController.dispose(); - } - - return TestButton('Test create a new column', testCreateNewField); -}; - -export const TestDeleteField = () => { - async function testDeleteField() { - const view = await createTestDatabaseView(ViewLayoutTypePB.Grid); - const databaseController = await openTestDatabase(view.id); - await databaseController.open().then((result) => result.unwrap()); - - // Modify the name of the field. - // The fieldInfos[0] is the primary field by default, we can't delete it. - // So let choose the second fieldInfo. - const fieldInfo = databaseController.fieldController.fieldInfos[1]; - const controller = new TypeOptionController(view.id, Some(fieldInfo)); - await controller.initialize(); - await assertNumberOfFields(view.id, 3); - await controller.deleteField(); - await assertNumberOfFields(view.id, 2); - await databaseController.dispose(); - } - - return TestButton('Test delete a new column', testDeleteField); -}; - -const TestButton = (title: string, onClick: () => void) => { - return ( - -
- -
-
- ); -}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/TestApiButton/DatabaseTestHelper.ts b/frontend/appflowy_tauri/src/appflowy_app/components/tests/DatabaseTestHelper.ts similarity index 76% rename from frontend/appflowy_tauri/src/appflowy_app/components/TestApiButton/DatabaseTestHelper.ts rename to frontend/appflowy_tauri/src/appflowy_app/components/tests/DatabaseTestHelper.ts index 1eecc6f70a..19a05272c4 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/TestApiButton/DatabaseTestHelper.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/tests/DatabaseTestHelper.ts @@ -1,4 +1,10 @@ -import { FieldType, ViewLayoutTypePB, ViewPB, WorkspaceSettingPB } from '../../../services/backend'; +import { + FieldType, + SingleSelectTypeOptionPB, + ViewLayoutTypePB, + ViewPB, + WorkspaceSettingPB, +} from '../../../services/backend'; import { FolderEventReadCurrentWorkspace } from '../../../services/backend/events/flowy-folder'; import { AppBackendService } from '../../stores/effects/folder/app/app_bd_svc'; import { DatabaseController } from '../../stores/effects/database/database_controller'; @@ -14,6 +20,10 @@ import { import { None, Option, Some } from 'ts-results'; import { TypeOptionBackendService } from '../../stores/effects/database/field/type_option/type_option_bd_svc'; import { DatabaseBackendService } from '../../stores/effects/database/database_bd_svc'; +import { FieldInfo } from '../../stores/effects/database/field/field_controller'; +import { TypeOptionController } from '../../stores/effects/database/field/type_option/type_option_controller'; +import { makeSingleSelectTypeOptionContext } from '../../stores/effects/database/field/type_option/type_option_context'; +import { SelectOptionBackendService } from '../../stores/effects/database/cell/select_option_bd_svc'; // Create a database view for specific layout type // Do not use it production code. Just for testing @@ -168,3 +178,36 @@ export async function assertNumberOfRows(viewId: string, expected: number) { throw Error('Expect number of rows:' + expected + 'but receive:' + databasePB.rows.length); } } + +export async function assertNumberOfRowsInGroup(viewId: string, groupId: string, expected: number) { + const svc = new DatabaseBackendService(viewId); + await svc.openDatabase(); + + const group = await svc.getGroup(groupId).then((result) => result.unwrap()); + if (group.rows.length !== expected) { + throw Error('Expect number of rows in group:' + expected + 'but receive:' + group.rows.length); + } +} + +export async function createSingleSelectOptions(viewId: string, fieldInfo: FieldInfo, optionNames: string[]) { + assert(fieldInfo.field.field_type === FieldType.SingleSelect, 'Only work on single select'); + const typeOptionController = new TypeOptionController(viewId, Some(fieldInfo)); + const singleSelectTypeOptionContext = makeSingleSelectTypeOptionContext(typeOptionController); + const singleSelectTypeOptionPB: SingleSelectTypeOptionPB = await singleSelectTypeOptionContext + .getTypeOption() + .then((result) => result.unwrap()); + + const backendSvc = new SelectOptionBackendService(viewId, fieldInfo.field.id); + for (const optionName of optionNames) { + const option = await backendSvc.createOption({ name: optionName }).then((result) => result.unwrap()); + singleSelectTypeOptionPB.options.splice(0, 0, option); + } + await singleSelectTypeOptionContext.setTypeOption(singleSelectTypeOptionPB); + return singleSelectTypeOptionContext; +} + +export function assert(condition: boolean, msg?: string) { + if (!condition) { + throw Error(msg); + } +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/TestApiButton/TestAPI.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/tests/TestAPI.tsx similarity index 59% rename from frontend/appflowy_tauri/src/appflowy_app/components/TestApiButton/TestAPI.tsx rename to frontend/appflowy_tauri/src/appflowy_app/components/tests/TestAPI.tsx index e7057042f1..c86960c881 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/TestApiButton/TestAPI.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/tests/TestAPI.tsx @@ -1,5 +1,6 @@ import React from 'react'; import { + RunAllGridTests, TestCreateGrid, TestCreateNewField, TestCreateRow, @@ -12,12 +13,21 @@ import { TestSwitchFromMultiSelectToText, TestSwitchFromSingleSelectToNumber, } from './TestGrid'; +import { + TestCreateKanbanBoard, + TestCreateKanbanBoardColumn, + TestCreateKanbanBoardRowInNoStatusGroup, + TestAllKanbanTests, + TestMoveKanbanBoardColumn, + TestMoveKanbanBoardRow, +} from './TestGroup'; export const TestAPI = () => { return (
    - {/**/} + {/**/} + @@ -29,6 +39,13 @@ export const TestAPI = () => { + {/*kanban board */} + + + + + +
); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/TestApiButton/TestApiButton.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/tests/TestApiButton.tsx similarity index 100% rename from frontend/appflowy_tauri/src/appflowy_app/components/TestApiButton/TestApiButton.tsx rename to frontend/appflowy_tauri/src/appflowy_app/components/tests/TestApiButton.tsx diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/tests/TestGrid.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/tests/TestGrid.tsx new file mode 100644 index 0000000000..075c7d04e9 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/tests/TestGrid.tsx @@ -0,0 +1,349 @@ +import React from 'react'; +import { + FieldType, + NumberFormat, + NumberTypeOptionPB, + SelectOptionCellDataPB, + ViewLayoutTypePB, +} from '../../../services/backend'; +import { Log } from '../../utils/log'; +import { + assertFieldName, + assertNumberOfFields, + assertNumberOfRows, + assertTextCell, + createSingleSelectOptions, + createTestDatabaseView, + editTextCell, + findFirstFieldInfoWithFieldType, + makeMultiSelectCellController, + makeSingleSelectCellController, + makeTextCellController, + openTestDatabase, +} from './DatabaseTestHelper'; +import { SelectOptionCellBackendService } from '../../stores/effects/database/cell/select_option_bd_svc'; +import { TypeOptionController } from '../../stores/effects/database/field/type_option/type_option_controller'; +import { None, Some } from 'ts-results'; +import { RowBackendService } from '../../stores/effects/database/row/row_bd_svc'; +import { makeNumberTypeOptionContext } from '../../stores/effects/database/field/type_option/type_option_context'; + +export const RunAllGridTests = () => { + async function run() { + await createBuildInGrid(); + await testEditGridRow(); + await testCreateRow(); + await testDeleteRow(); + await testCreateOptionInCell(); + await testGetSingleSelectFieldData(); + await testSwitchFromSingleSelectToNumber(); + await testSwitchFromMultiSelectToRichText(); + await testEditField(); + await testCreateNewField(); + await testDeleteField(); + } + + return ( + +
+ +
+
+ ); +}; + +async function createBuildInGrid() { + const view = await createTestDatabaseView(ViewLayoutTypePB.Grid); + const databaseController = await openTestDatabase(view.id); + databaseController.subscribe({ + onViewChanged: (databasePB) => { + Log.debug('Did receive database:' + databasePB); + }, + // onRowsChanged: async (rows) => { + // if (rows.length !== 3) { + // throw Error('Expected number of rows is 3, but receive ' + rows.length); + // } + // }, + onFieldsChanged: (fields) => { + if (fields.length !== 3) { + throw Error('Expected number of fields is 3, but receive ' + fields.length); + } + }, + }); + await databaseController.open().then((result) => result.unwrap()); + await databaseController.dispose(); +} + +async function testEditGridRow() { + const view = await createTestDatabaseView(ViewLayoutTypePB.Grid); + const databaseController = await openTestDatabase(view.id); + await databaseController.open().then((result) => result.unwrap()); + + for (const [index, row] of databaseController.databaseViewCache.rowInfos.entries()) { + const cellContent = index.toString(); + const fieldInfo = findFirstFieldInfoWithFieldType(row, FieldType.RichText).unwrap(); + await editTextCell(fieldInfo.field.id, row, databaseController, cellContent); + await assertTextCell(fieldInfo.field.id, row, databaseController, cellContent); + } +} + +async function testCreateRow() { + const view = await createTestDatabaseView(ViewLayoutTypePB.Grid); + const databaseController = await openTestDatabase(view.id); + await databaseController.open().then((result) => result.unwrap()); + await assertNumberOfRows(view.id, 3); + + // Create a row from a DatabaseController or create using the RowBackendService + await databaseController.createRow(); + await assertNumberOfRows(view.id, 4); + await databaseController.dispose(); +} + +async function testDeleteRow() { + const view = await createTestDatabaseView(ViewLayoutTypePB.Grid); + const databaseController = await openTestDatabase(view.id); + await databaseController.open().then((result) => result.unwrap()); + + const rows = databaseController.databaseViewCache.rowInfos; + const svc = new RowBackendService(view.id); + await svc.deleteRow(rows[0].row.id); + await assertNumberOfRows(view.id, 2); + + // Wait the databaseViewCache get the change notification and + // update the rows. + await new Promise((resolve) => setTimeout(resolve, 200)); + if (databaseController.databaseViewCache.rowInfos.length !== 2) { + throw Error('The number of rows is not match'); + } + await databaseController.dispose(); +} + +async function testCreateOptionInCell() { + const view = await createTestDatabaseView(ViewLayoutTypePB.Grid); + const databaseController = await openTestDatabase(view.id); + await databaseController.open().then((result) => result.unwrap()); + for (const [index, row] of databaseController.databaseViewCache.rowInfos.entries()) { + if (index === 0) { + const fieldInfo = findFirstFieldInfoWithFieldType(row, FieldType.SingleSelect).unwrap(); + const cellController = await makeSingleSelectCellController(fieldInfo.field.id, row, databaseController).then( + (result) => result.unwrap() + ); + await cellController.subscribeChanged({ + onCellChanged: (value) => { + if (value.some) { + const option: SelectOptionCellDataPB = value.unwrap(); + console.log(option); + } + }, + }); + const backendSvc = new SelectOptionCellBackendService(cellController.cellIdentifier); + await backendSvc.createOption({ name: 'option' + index }); + await cellController.dispose(); + } + } + await databaseController.dispose(); +} + +async function testGetSingleSelectFieldData() { + const view = await createTestDatabaseView(ViewLayoutTypePB.Grid); + const databaseController = await openTestDatabase(view.id); + await databaseController.open().then((result) => result.unwrap()); + + // Find the single select column + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const singleSelect = databaseController.fieldController.fieldInfos.find( + (fieldInfo) => fieldInfo.field.field_type === FieldType.SingleSelect + )!; + + // Create options + const singleSelectTypeOptionContext = await createSingleSelectOptions(view.id, singleSelect, [ + 'Task 1', + 'Task 2', + 'Task 3', + ]); + + // Read options + const options = await singleSelectTypeOptionContext.getTypeOption().then((result) => result.unwrap()); + console.log(options); + + await databaseController.dispose(); +} + +async function testSwitchFromSingleSelectToNumber() { + const view = await createTestDatabaseView(ViewLayoutTypePB.Grid); + const databaseController = await openTestDatabase(view.id); + await databaseController.open().then((result) => result.unwrap()); + + // Find the single select column + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const singleSelect = databaseController.fieldController.fieldInfos.find( + (fieldInfo) => fieldInfo.field.field_type === FieldType.SingleSelect + )!; + const typeOptionController = new TypeOptionController(view.id, Some(singleSelect)); + await typeOptionController.switchToField(FieldType.Number); + + // Check the number type option + const numberTypeOptionContext = makeNumberTypeOptionContext(typeOptionController); + const numberTypeOption: NumberTypeOptionPB = await numberTypeOptionContext + .getTypeOption() + .then((result) => result.unwrap()); + const format: NumberFormat = numberTypeOption.format; + if (format !== NumberFormat.Num) { + throw Error('The default format should be number'); + } + + await databaseController.dispose(); +} + +async function testSwitchFromMultiSelectToRichText() { + const view = await createTestDatabaseView(ViewLayoutTypePB.Grid); + const databaseController = await openTestDatabase(view.id); + await databaseController.open().then((result) => result.unwrap()); + + // Create multi-select field + const typeOptionController = new TypeOptionController(view.id, None, FieldType.MultiSelect); + await typeOptionController.initialize(); + + // Insert options to first row + const row = databaseController.databaseViewCache.rowInfos[0]; + const multiSelectField = typeOptionController.getFieldInfo(); + // const multiSelectField = findFirstFieldInfoWithFieldType(row, FieldType.MultiSelect).unwrap(); + const selectOptionCellController = await makeMultiSelectCellController( + multiSelectField.field.id, + row, + databaseController + ).then((result) => result.unwrap()); + const backendSvc = new SelectOptionCellBackendService(selectOptionCellController.cellIdentifier); + await backendSvc.createOption({ name: 'A' }); + await backendSvc.createOption({ name: 'B' }); + await backendSvc.createOption({ name: 'C' }); + + const selectOptionCellData = await selectOptionCellController.getCellData().then((result) => result.unwrap()); + if (selectOptionCellData.options.length !== 3) { + throw Error('The options should equal to 3'); + } + + if (selectOptionCellData.select_options.length !== 3) { + throw Error('The selected options should equal to 3'); + } + await selectOptionCellController.dispose(); + + // Switch to RichText field type + await typeOptionController.switchToField(FieldType.RichText).then((result) => result.unwrap()); + if (typeOptionController.fieldType !== FieldType.RichText) { + throw Error('The field type should be text'); + } + + const textCellController = await makeTextCellController(multiSelectField.field.id, row, databaseController).then( + (result) => result.unwrap() + ); + const cellContent = await textCellController.getCellData(); + if (cellContent.unwrap() !== 'A,B,C') { + throw Error('The cell content should be A,B,C, but receive: ' + cellContent.unwrap()); + } + + await databaseController.dispose(); +} + +async function testEditField() { + const view = await createTestDatabaseView(ViewLayoutTypePB.Grid); + const databaseController = await openTestDatabase(view.id); + await databaseController.open().then((result) => result.unwrap()); + const fieldInfos = databaseController.fieldController.fieldInfos; + + // Modify the name of the field + const firstFieldInfo = fieldInfos[0]; + const controller = new TypeOptionController(view.id, Some(firstFieldInfo)); + await controller.initialize(); + const newName = 'hello world'; + await controller.setFieldName(newName); + + await new Promise((resolve) => setTimeout(resolve, 200)); + await assertFieldName(view.id, firstFieldInfo.field.id, firstFieldInfo.field.field_type, newName); + await databaseController.dispose(); +} + +async function testCreateNewField() { + const view = await createTestDatabaseView(ViewLayoutTypePB.Grid); + const databaseController = await openTestDatabase(view.id); + await databaseController.open().then((result) => result.unwrap()); + await assertNumberOfFields(view.id, 3); + + // Modify the name of the field + const controller = new TypeOptionController(view.id, None); + await controller.initialize(); + await assertNumberOfFields(view.id, 4); + await databaseController.dispose(); +} + +async function testDeleteField() { + const view = await createTestDatabaseView(ViewLayoutTypePB.Grid); + const databaseController = await openTestDatabase(view.id); + await databaseController.open().then((result) => result.unwrap()); + + // Modify the name of the field. + // The fieldInfos[0] is the primary field by default, we can't delete it. + // So let choose the second fieldInfo. + const fieldInfo = databaseController.fieldController.fieldInfos[1]; + const controller = new TypeOptionController(view.id, Some(fieldInfo)); + await controller.initialize(); + await assertNumberOfFields(view.id, 3); + await controller.deleteField(); + await assertNumberOfFields(view.id, 2); + await databaseController.dispose(); +} + +export const TestCreateGrid = () => { + return TestButton('Test create build-in grid', createBuildInGrid); +}; + +export const TestEditCell = () => { + return TestButton('Test editing cell', testEditGridRow); +}; + +export const TestCreateRow = () => { + return TestButton('Test create row', testCreateRow); +}; +export const TestDeleteRow = () => { + return TestButton('Test delete row', testDeleteRow); +}; +export const TestCreateSelectOptionInCell = () => { + return TestButton('Test create a select option in cell', testCreateOptionInCell); +}; + +export const TestGetSingleSelectFieldData = () => { + return TestButton('Test get single-select column data', testGetSingleSelectFieldData); +}; + +export const TestSwitchFromSingleSelectToNumber = () => { + return TestButton('Test switch from single-select to number column', testSwitchFromSingleSelectToNumber); +}; + +export const TestSwitchFromMultiSelectToText = () => { + return TestButton('Test switch from multi-select to text column', testSwitchFromMultiSelectToRichText); +}; + +export const TestEditField = () => { + return TestButton('Test edit the column name', testEditField); +}; + +export const TestCreateNewField = () => { + return TestButton('Test create a new column', testCreateNewField); +}; + +export const TestDeleteField = () => { + return TestButton('Test delete a new column', testDeleteField); +}; + +export const TestButton = (title: string, onClick: () => void) => { + return ( + +
+ +
+
+ ); +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/tests/TestGroup.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/tests/TestGroup.tsx new file mode 100644 index 0000000000..a88065f253 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/tests/TestGroup.tsx @@ -0,0 +1,150 @@ +import { + assert, + assertNumberOfRowsInGroup, + createSingleSelectOptions, + createTestDatabaseView, + openTestDatabase, +} from './DatabaseTestHelper'; +import { FieldType, ViewLayoutTypePB } from '../../../services/backend'; +import React from 'react'; + +export const TestAllKanbanTests = () => { + async function run() { + await createBuildInBoard(); + await createKanbanBoardRow(); + await moveKanbanBoardRow(); + await createKanbanBoardColumn(); + await createColumnInBoard(); + } + + return ( + +
+ +
+
+ ); +}; + +async function createBuildInBoard() { + const view = await createTestDatabaseView(ViewLayoutTypePB.Board); + const databaseController = await openTestDatabase(view.id); + databaseController.subscribe({ + onGroupByField: (groups) => { + console.log(groups); + if (groups.length !== 4) { + throw Error('The build-in board should have 4 groups'); + } + + assert(groups[0].rows.length === 0, 'The no status group should have 0 rows'); + assert(groups[1].rows.length === 3, 'The first group should have 3 rows'); + assert(groups[2].rows.length === 0, 'The second group should have 0 rows'); + assert(groups[3].rows.length === 0, 'The third group should have 0 rows'); + }, + }); + await databaseController.open().then((result) => result.unwrap()); + await databaseController.dispose(); +} + +async function createKanbanBoardRow() { + const view = await createTestDatabaseView(ViewLayoutTypePB.Board); + const databaseController = await openTestDatabase(view.id); + await databaseController.open().then((result) => result.unwrap()); + + // Create row in no status group + const noStatusGroup = databaseController.groups.getValue()[0]; + await noStatusGroup.createRow().then((result) => result.unwrap()); + await assertNumberOfRowsInGroup(view.id, noStatusGroup.groupId, 1); + + await databaseController.dispose(); +} + +async function moveKanbanBoardRow() { + const view = await createTestDatabaseView(ViewLayoutTypePB.Board); + const databaseController = await openTestDatabase(view.id); + await databaseController.open().then((result) => result.unwrap()); + + // Create row in no status group + const firstGroup = databaseController.groups.getValue()[1]; + const secondGroup = databaseController.groups.getValue()[2]; + const row = firstGroup.rowAtIndex(0).unwrap(); + await databaseController.moveRow(row.id, secondGroup.groupId); + + assert(firstGroup.rows.length === 2); + await assertNumberOfRowsInGroup(view.id, firstGroup.groupId, 2); + + assert(secondGroup.rows.length === 1); + await assertNumberOfRowsInGroup(view.id, secondGroup.groupId, 1); + + await databaseController.dispose(); +} + +async function createKanbanBoardColumn() { + const view = await createTestDatabaseView(ViewLayoutTypePB.Board); + const databaseController = await openTestDatabase(view.id); + await databaseController.open().then((result) => result.unwrap()); + + // Create row in no status group + const firstGroup = databaseController.groups.getValue()[1]; + const secondGroup = databaseController.groups.getValue()[2]; + await databaseController.moveGroup(firstGroup.groupId, secondGroup.groupId); + + assert(databaseController.groups.getValue()[1].groupId === secondGroup.groupId); + assert(databaseController.groups.getValue()[2].groupId === firstGroup.groupId); + await databaseController.dispose(); +} + +async function createColumnInBoard() { + const view = await createTestDatabaseView(ViewLayoutTypePB.Board); + const databaseController = await openTestDatabase(view.id); + await databaseController.open().then((result) => result.unwrap()); + + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const singleSelect = databaseController.fieldController.fieldInfos.find( + (fieldInfo) => fieldInfo.field.field_type === FieldType.SingleSelect + )!; + + // Create a option which will cause creating a new group + const name = 'New column'; + await createSingleSelectOptions(view.id, singleSelect, [name]); + + // Wait the backend posting the notification to update the groups + await new Promise((resolve) => setTimeout(resolve, 200)); + assert(databaseController.groups.value.length === 5, 'expect number of groups is 5'); + assert(databaseController.groups.value[4].name === name, 'expect the last group name is ' + name); + await databaseController.dispose(); +} + +export const TestCreateKanbanBoard = () => { + return TestButton('Test create build-in board', createBuildInBoard); +}; + +export const TestCreateKanbanBoardRowInNoStatusGroup = () => { + return TestButton('Test create row in build-in kanban board', createKanbanBoardRow); +}; + +export const TestMoveKanbanBoardRow = () => { + return TestButton('Test move row in build-in kanban board', moveKanbanBoardRow); +}; + +export const TestMoveKanbanBoardColumn = () => { + return TestButton('Test move column in build-in kanban board', createKanbanBoardColumn); +}; + +export const TestCreateKanbanBoardColumn = () => { + return TestButton('Test create column in build-in kanban board', createColumnInBoard); +}; + +export const TestButton = (title: string, onClick: () => void) => { + return ( + +
+ +
+
+ ); +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/database_bd_svc.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/database_bd_svc.ts index 19f956564d..f2e8c9eb2a 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/database_bd_svc.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/database_bd_svc.ts @@ -1,7 +1,16 @@ import { + CreateBoardCardPayloadPB, + DatabaseEventCreateBoardCard, DatabaseEventCreateRow, DatabaseEventGetDatabase, DatabaseEventGetFields, + DatabaseEventGetGroup, + DatabaseEventGetGroups, + DatabaseEventMoveGroup, + DatabaseEventMoveGroupRow, + DatabaseGroupIdPB, + MoveGroupPayloadPB, + MoveGroupRowPayloadPB, } from '../../../../services/backend/events/flowy-database'; import { GetFieldPayloadPB, @@ -37,6 +46,31 @@ export class DatabaseBackendService { return DatabaseEventCreateRow(payload); }; + createGroupRow = async (groupId: string, startRowId?: string) => { + const payload = CreateBoardCardPayloadPB.fromObject({ view_id: this.viewId, group_id: groupId }); + if (startRowId !== undefined) { + payload.start_row_id = startRowId; + } + return DatabaseEventCreateBoardCard(payload); + }; + + moveRow = (rowId: string, groupId?: string) => { + const payload = MoveGroupRowPayloadPB.fromObject({ view_id: this.viewId, from_row_id: rowId }); + if (groupId !== undefined) { + payload.to_group_id = groupId; + } + return DatabaseEventMoveGroupRow(payload); + }; + + moveGroup = (fromGroupId: string, toGroupId: string) => { + const payload = MoveGroupPayloadPB.fromObject({ + view_id: this.viewId, + from_group_id: fromGroupId, + to_group_id: toGroupId, + }); + return DatabaseEventMoveGroup(payload); + }; + getFields = async (fieldIds?: FieldIdPB[]) => { const payload = GetFieldPayloadPB.fromObject({ view_id: this.viewId }); @@ -46,4 +80,14 @@ export class DatabaseBackendService { return DatabaseEventGetFields(payload).then((result) => result.map((value) => value.items)); }; + + getGroup = (groupId: string) => { + const payload = DatabaseGroupIdPB.fromObject({ view_id: this.viewId, group_id: groupId }); + return DatabaseEventGetGroup(payload); + }; + + loadGroups = () => { + const payload = DatabaseViewIdPB.fromObject({ value: this.viewId }); + return DatabaseEventGetGroups(payload); + }; } diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/database_controller.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/database_controller.ts index 819818dae8..e47119c49a 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/database_controller.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/database_controller.ts @@ -1,55 +1,150 @@ import { DatabaseBackendService } from './database_bd_svc'; import { FieldController, FieldInfo } from './field/field_controller'; import { DatabaseViewCache } from './view/database_view_cache'; -import { DatabasePB } from '../../../../services/backend'; +import { DatabasePB, GroupPB } from '../../../../services/backend'; import { RowChangedReason, RowInfo } from './row/row_cache'; -import { Err, Ok } from 'ts-results'; +import { Err } from 'ts-results'; +import { DatabaseGroupController } from './group/group_controller'; +import { BehaviorSubject } from 'rxjs'; +import { DatabaseGroupObserver } from './group/group_observer'; +import { Log } from '../../../utils/log'; -export type SubscribeCallbacks = { +export type DatabaseSubscriberCallbacks = { onViewChanged?: (data: DatabasePB) => void; onRowsChanged?: (rowInfos: readonly RowInfo[], reason: RowChangedReason) => void; onFieldsChanged?: (fieldInfos: readonly FieldInfo[]) => void; + onGroupByField?: (groups: GroupPB[]) => void; + + onNumOfGroupChanged?: { + onUpdateGroup: (value: GroupPB[]) => void; + onDeleteGroup: (value: GroupPB[]) => void; + onInsertGroup: (value: GroupPB[]) => void; + }; }; export class DatabaseController { - private backendService: DatabaseBackendService; + private readonly backendService: DatabaseBackendService; fieldController: FieldController; databaseViewCache: DatabaseViewCache; - private _callback?: SubscribeCallbacks; + private _callback?: DatabaseSubscriberCallbacks; + public groups: BehaviorSubject; + private groupsObserver: DatabaseGroupObserver; constructor(public readonly viewId: string) { this.backendService = new DatabaseBackendService(viewId); this.fieldController = new FieldController(viewId); this.databaseViewCache = new DatabaseViewCache(viewId, this.fieldController); + this.groups = new BehaviorSubject([]); + this.groupsObserver = new DatabaseGroupObserver(viewId); } - subscribe = (callbacks: SubscribeCallbacks) => { + subscribe = (callbacks: DatabaseSubscriberCallbacks) => { this._callback = callbacks; - this.fieldController.subscribeOnNumOfFieldsChanged(callbacks.onFieldsChanged); - this.databaseViewCache.getRowCache().subscribeOnRowsChanged((reason) => { - this._callback?.onRowsChanged?.(this.databaseViewCache.rowInfos, reason); + this.fieldController.subscribe({ onNumOfFieldsChanged: callbacks.onFieldsChanged }); + this.databaseViewCache.getRowCache().subscribe({ + onRowsChanged: (reason) => { + this._callback?.onRowsChanged?.(this.databaseViewCache.rowInfos, reason); + }, }); }; open = async () => { - const result = await this.backendService.openDatabase(); - if (result.ok) { - const database: DatabasePB = result.val; - this._callback?.onViewChanged?.(database); + const openDatabaseResult = await this.backendService.openDatabase(); + if (openDatabaseResult.ok) { + const database: DatabasePB = openDatabaseResult.val; + await this.databaseViewCache.initialize(); + await this.fieldController.initialize(); + + // subscriptions + await this.subscribeOnGroupsChanged(); + + // load database initial data await this.fieldController.loadFields(database.fields); - await this.databaseViewCache.listenOnRowsChanged(); - await this.fieldController.listenOnFieldChanges(); + const loadGroupResult = await this.loadGroup(); + this.databaseViewCache.initializeWithRows(database.rows); - return Ok.EMPTY; + + this._callback?.onViewChanged?.(database); + return loadGroupResult; } else { - return Err(result.val); + return Err(openDatabaseResult.val); } }; - createRow = async () => { + createRow = () => { return this.backendService.createRow(); }; + moveRow = (rowId: string, groupId: string) => { + return this.backendService.moveRow(rowId, groupId); + }; + + moveGroup = (fromGroupId: string, toGroupId: string) => { + return this.backendService.moveGroup(fromGroupId, toGroupId); + }; + + private loadGroup = async () => { + const result = await this.backendService.loadGroups(); + if (result.ok) { + const groups = result.val.items; + await this.initialGroups(groups); + } + return result; + }; + + private initialGroups = async (groups: GroupPB[]) => { + this.groups.getValue().forEach((controller) => { + void controller.dispose(); + }); + + const controllers: DatabaseGroupController[] = []; + for (const groupPB of groups) { + const controller = new DatabaseGroupController(groupPB, this.backendService); + await controller.initialize(); + controllers.push(controller); + } + this.groups.next(controllers); + this.groups.value; + }; + + private subscribeOnGroupsChanged = async () => { + await this.groupsObserver.subscribe({ + onGroupBy: async (result) => { + if (result.ok) { + await this.initialGroups(result.val); + } + }, + onGroupChangeset: (result) => { + if (result.err) { + Log.error(result.val); + return; + } + const changeset = result.val; + let existControllers = [...this.groups.getValue()]; + for (const deleteId of changeset.deleted_groups) { + existControllers = existControllers.filter((c) => c.groupId !== deleteId); + } + + for (const update of changeset.update_groups) { + const index = existControllers.findIndex((c) => c.groupId === update.group_id); + if (index !== -1) { + existControllers[index].updateGroup(update); + } + } + + for (const insert of changeset.inserted_groups) { + const controller = new DatabaseGroupController(insert.group, this.backendService); + if (insert.index > existControllers.length) { + existControllers.push(controller); + } else { + existControllers.splice(insert.index, 0, controller); + } + } + this.groups.next(existControllers); + }, + }); + }; + dispose = async () => { await this.backendService.closeDatabase(); await this.fieldController.dispose(); diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/field/field_controller.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/field/field_controller.ts index 05ab179325..317c5d3370 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/field/field_controller.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/field/field_controller.ts @@ -6,17 +6,17 @@ import { ChangeNotifier } from '../../../../utils/change_notifier'; export class FieldController { private backendService: DatabaseBackendService; - private numOfFieldsObserver: DatabaseFieldChangesetObserver; + private fieldChangesetObserver: DatabaseFieldChangesetObserver; private numOfFieldsNotifier = new NumOfFieldsNotifier([]); constructor(public readonly viewId: string) { this.backendService = new DatabaseBackendService(viewId); - this.numOfFieldsObserver = new DatabaseFieldChangesetObserver(viewId); + this.fieldChangesetObserver = new DatabaseFieldChangesetObserver(viewId); } dispose = async () => { this.numOfFieldsNotifier.unsubscribe(); - await this.numOfFieldsObserver.unsubscribe(); + await this.fieldChangesetObserver.unsubscribe(); }; get fieldInfos(): readonly FieldInfo[] { @@ -36,14 +36,14 @@ export class FieldController { } }; - subscribeOnNumOfFieldsChanged = (callback?: (fieldInfos: readonly FieldInfo[]) => void) => { - return this.numOfFieldsNotifier.observer.subscribe((fieldInfos) => { - callback?.(fieldInfos); + subscribe = (callbacks: { onNumOfFieldsChanged?: (fieldInfos: readonly FieldInfo[]) => void}) => { + this.numOfFieldsNotifier.observer.subscribe((fieldInfos) => { + callbacks.onNumOfFieldsChanged?.(fieldInfos); }); }; - listenOnFieldChanges = async () => { - await this.numOfFieldsObserver.subscribe({ + initialize = async () => { + await this.fieldChangesetObserver.subscribe({ onFieldsChanged: (result) => { if (result.ok) { const changeset = result.val; 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 index 9d7daec7df..ea30c5021e 100644 --- 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 @@ -3,16 +3,15 @@ import { DatabaseNotification, DatabaseFieldChangesetPB, FlowyError, FieldPB } f import { ChangeNotifier } from '../../../../utils/change_notifier'; import { DatabaseNotificationObserver } from '../notifications/observer'; -type UpdateFieldNotifiedValue = Result; -export type DatabaseNotificationCallback = (value: UpdateFieldNotifiedValue) => void; +export type FieldChangesetSubscribeCallback = (value: Result) => void; export class DatabaseFieldChangesetObserver { - private notifier?: ChangeNotifier; + private notifier?: ChangeNotifier>; private listener?: DatabaseNotificationObserver; constructor(public readonly viewId: string) {} - subscribe = async (callbacks: { onFieldsChanged: DatabaseNotificationCallback }) => { + subscribe = async (callbacks: { onFieldsChanged: FieldChangesetSubscribeCallback }) => { this.notifier = new ChangeNotifier(); this.notifier?.observer.subscribe(callbacks.onFieldsChanged); @@ -41,16 +40,15 @@ export class DatabaseFieldChangesetObserver { }; } -type FieldNotifiedValue = Result; -export type FieldNotificationCallback = (value: FieldNotifiedValue) => void; +export type FieldSubscribeCallback = (value: Result) => void; export class DatabaseFieldObserver { - private _notifier?: ChangeNotifier; + private _notifier?: ChangeNotifier>; private _listener?: DatabaseNotificationObserver; constructor(public readonly fieldId: string) {} - subscribe = async (callbacks: { onFieldChanged: FieldNotificationCallback }) => { + subscribe = async (callbacks: { onFieldChanged: FieldSubscribeCallback }) => { this._notifier = new ChangeNotifier(); this._notifier?.observer.subscribe(callbacks.onFieldChanged); diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/group/group_controller.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/group/group_controller.ts new file mode 100644 index 0000000000..395bdebb38 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/group/group_controller.ts @@ -0,0 +1,149 @@ +import { + DatabaseNotification, + FlowyError, + GroupPB, + GroupRowsNotificationPB, + RowPB, +} from '../../../../../services/backend'; +import { ChangeNotifier } from '../../../../utils/change_notifier'; +import { None, Ok, Option, Result, Some } from 'ts-results'; +import { DatabaseNotificationObserver } from '../notifications/observer'; +import { Log } from '../../../../utils/log'; +import { DatabaseBackendService } from '../database_bd_svc'; + +export type GroupDataCallbacks = { + onRemoveRow: (groupId: string, rowId: string) => void; + onInsertRow: (groupId: string, row: RowPB, index?: number) => void; + onUpdateRow: (groupId: string, row: RowPB) => void; + + onCreateRow: (groupId: string, row: RowPB) => void; +}; + +export class DatabaseGroupController { + private dataObserver: GroupDataObserver; + private callbacks?: GroupDataCallbacks; + + constructor(private group: GroupPB, private databaseBackendSvc: DatabaseBackendService) { + this.dataObserver = new GroupDataObserver(group.group_id); + } + + get groupId() { + return this.group.group_id; + } + + get rows() { + return this.group.rows; + } + + get name() { + return this.group.desc; + } + + updateGroup = (group: GroupPB) => { + this.group = group; + }; + + rowAtIndex = (index: number): Option => { + if (this.group.rows.length < index) { + return None; + } + return Some(this.group.rows[index]); + }; + + initialize = async () => { + await this.dataObserver.subscribe({ + onRowsChanged: (result) => { + if (result.ok) { + const changeset = result.val; + // Delete + changeset.deleted_rows.forEach((deletedRowId) => { + this.group.rows = this.group.rows.filter((row) => row.id !== deletedRowId); + this.callbacks?.onRemoveRow(this.group.group_id, deletedRowId); + }); + + // Insert + changeset.inserted_rows.forEach((insertedRow) => { + let index: number | undefined = insertedRow.index; + if (insertedRow.has_index && this.group.rows.length > insertedRow.index) { + this.group.rows.splice(index, 0, insertedRow.row); + } else { + index = undefined; + this.group.rows.push(insertedRow.row); + } + + if (insertedRow.is_new) { + this.callbacks?.onCreateRow(this.group.group_id, insertedRow.row); + } else { + this.callbacks?.onInsertRow(this.group.group_id, insertedRow.row, index); + } + }); + + // Update + changeset.updated_rows.forEach((updatedRow) => { + const index = this.group.rows.findIndex((row) => row.id === updatedRow.id); + if (index !== -1) { + this.group.rows[index] = updatedRow; + this.callbacks?.onUpdateRow(this.group.group_id, updatedRow); + } + }); + } else { + Log.error(result.val); + } + }, + }); + }; + + createRow = async () => { + return this.databaseBackendSvc.createGroupRow(this.group.group_id); + }; + + subscribe = (callbacks: GroupDataCallbacks) => { + this.callbacks = callbacks; + }; + + unsubscribe = () => { + this.callbacks = undefined; + }; + + dispose = async () => { + await this.dataObserver.unsubscribe(); + this.callbacks = undefined; + }; +} + +type GroupRowsSubscribeCallback = (value: Result) => void; + +class GroupDataObserver { + private notifier?: ChangeNotifier>; + private listener?: DatabaseNotificationObserver; + + constructor(public readonly groupId: string) {} + + subscribe = async (callbacks: { onRowsChanged: GroupRowsSubscribeCallback }) => { + this.notifier = new ChangeNotifier(); + this.notifier?.observer.subscribe(callbacks.onRowsChanged); + + this.listener = new DatabaseNotificationObserver({ + id: this.groupId, + parserHandler: (notification, result) => { + switch (notification) { + case DatabaseNotification.DidUpdateGroupRow: + if (result.ok) { + this.notifier?.notify(Ok(GroupRowsNotificationPB.deserializeBinary(result.val))); + } else { + this.notifier?.notify(result); + } + return; + default: + break; + } + }, + }); + await this.listener.start(); + }; + + unsubscribe = async () => { + await this.listener?.stop(); + this.notifier?.unsubscribe(); + }; +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/group/group_observer.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/group/group_observer.ts new file mode 100644 index 0000000000..5eb7d7c139 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/group/group_observer.ts @@ -0,0 +1,58 @@ +import { ChangeNotifier } from '../../../../utils/change_notifier'; +import { Ok, Result } from 'ts-results'; +import { DatabaseNotification, FlowyError, GroupChangesetPB, GroupPB } from '../../../../../services/backend'; +import { DatabaseNotificationObserver } from '../notifications/observer'; + +export type GroupByFieldCallback = (value: Result) => void; +export type GroupChangesetSubscribeCallback = (value: Result) => void; + +export class DatabaseGroupObserver { + private groupByNotifier?: ChangeNotifier>; + private groupChangesetNotifier?: ChangeNotifier>; + private listener?: DatabaseNotificationObserver; + + constructor(public readonly viewId: string) {} + + subscribe = async (callbacks: { + onGroupBy: GroupByFieldCallback; + onGroupChangeset: GroupChangesetSubscribeCallback; + }) => { + this.groupByNotifier = new ChangeNotifier(); + this.groupByNotifier?.observer.subscribe(callbacks.onGroupBy); + + this.groupChangesetNotifier = new ChangeNotifier(); + this.groupChangesetNotifier?.observer.subscribe(callbacks.onGroupChangeset); + + this.listener = new DatabaseNotificationObserver({ + id: this.viewId, + parserHandler: (notification, result) => { + switch (notification) { + case DatabaseNotification.DidGroupByField: + if (result.ok) { + this.groupByNotifier?.notify(Ok(GroupChangesetPB.deserializeBinary(result.val).initial_groups)); + } else { + this.groupByNotifier?.notify(result); + } + break; + case DatabaseNotification.DidUpdateGroups: + if (result.ok) { + this.groupChangesetNotifier?.notify(Ok(GroupChangesetPB.deserializeBinary(result.val))); + } else { + this.groupChangesetNotifier?.notify(result); + } + break; + default: + break; + } + }, + }); + + await this.listener.start(); + }; + + unsubscribe = async () => { + this.groupByNotifier?.unsubscribe(); + this.groupChangesetNotifier?.unsubscribe(); + await this.listener?.stop(); + }; +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/row/row_cache.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/row/row_cache.ts index 2d68d161a4..52b543b3dc 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/row/row_cache.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/row/row_cache.ts @@ -53,12 +53,14 @@ export class RowCache { } }; - subscribeOnRowsChanged = (callback: (reason: RowChangedReason, cellMap?: Map) => void) => { + subscribe = (callbacks: { + onRowsChanged: (reason: RowChangedReason, cellMap?: Map) => void; + }) => { return this.notifier.observer.subscribe((change) => { if (change.rowId !== undefined) { - callback(change.reason, this._toCellMap(change.rowId, this.getFieldInfos())); + callbacks.onRowsChanged(change.reason, this._toCellMap(change.rowId, this.getFieldInfos())); } else { - callback(change.reason); + callbacks.onRowsChanged(change.reason); } }); }; diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/view/database_view_cache.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/view/database_view_cache.ts index 2a5d9303e6..5e3d319669 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/view/database_view_cache.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/view/database_view_cache.ts @@ -7,16 +7,17 @@ import { Subscription } from 'rxjs'; export class DatabaseViewCache { private readonly rowsObserver: DatabaseViewRowsObserver; private readonly rowCache: RowCache; - private readonly fieldSubscription?: Subscription; constructor(public readonly viewId: string, fieldController: FieldController) { this.rowsObserver = new DatabaseViewRowsObserver(viewId); this.rowCache = new RowCache(viewId, () => fieldController.fieldInfos); - this.fieldSubscription = fieldController.subscribeOnNumOfFieldsChanged((fieldInfos) => { - fieldInfos.forEach((fieldInfo) => { - this.rowCache.onFieldUpdated(fieldInfo); - }); - this.rowCache.onNumberOfFieldsUpdated(fieldInfos); + fieldController.subscribe({ + onNumOfFieldsChanged: (fieldInfos) => { + fieldInfos.forEach((fieldInfo) => { + this.rowCache.onFieldUpdated(fieldInfo); + }); + this.rowCache.onNumberOfFieldsUpdated(fieldInfos); + }, }); } @@ -33,12 +34,11 @@ export class DatabaseViewCache { }; dispose = async () => { - this.fieldSubscription?.unsubscribe(); await this.rowsObserver.unsubscribe(); await this.rowCache.dispose(); }; - listenOnRowsChanged = async () => { + initialize = async () => { await this.rowsObserver.subscribe({ onRowsVisibilityChanged: (result) => { if (result.ok) { diff --git a/frontend/rust-lib/flowy-database/src/entities/database_entities.rs b/frontend/rust-lib/flowy-database/src/entities/database_entities.rs index 909bcb879b..cd63afa250 100644 --- a/frontend/rust-lib/flowy-database/src/entities/database_entities.rs +++ b/frontend/rust-lib/flowy-database/src/entities/database_entities.rs @@ -1,4 +1,158 @@ +use crate::entities::parser::NotEmptyStr; +use crate::entities::{FieldIdPB, RowPB}; use flowy_derive::ProtoBuf; +use flowy_error::ErrorCode; + +/// [DatabasePB] describes how many fields and blocks the grid has +#[derive(Debug, Clone, Default, ProtoBuf)] +pub struct DatabasePB { + #[pb(index = 1)] + pub id: String, + + #[pb(index = 2)] + pub fields: Vec, + + #[pb(index = 3)] + pub rows: Vec, +} + +#[derive(ProtoBuf, Default)] +pub struct CreateDatabasePayloadPB { + #[pb(index = 1)] + pub name: String, +} + +#[derive(Clone, ProtoBuf, Default, Debug)] +pub struct DatabaseViewIdPB { + #[pb(index = 1)] + pub value: String, +} + +impl AsRef for DatabaseViewIdPB { + fn as_ref(&self) -> &str { + &self.value + } +} + +#[derive(Debug, Clone, Default, ProtoBuf)] +pub struct MoveFieldPayloadPB { + #[pb(index = 1)] + pub view_id: String, + + #[pb(index = 2)] + pub field_id: String, + + #[pb(index = 3)] + pub from_index: i32, + + #[pb(index = 4)] + pub to_index: i32, +} + +#[derive(Clone)] +pub struct MoveFieldParams { + pub view_id: String, + pub field_id: String, + pub from_index: i32, + pub to_index: i32, +} + +impl TryInto for MoveFieldPayloadPB { + type Error = ErrorCode; + + fn try_into(self) -> Result { + let view_id = NotEmptyStr::parse(self.view_id).map_err(|_| ErrorCode::DatabaseViewIdIsEmpty)?; + let item_id = NotEmptyStr::parse(self.field_id).map_err(|_| ErrorCode::InvalidData)?; + Ok(MoveFieldParams { + view_id: view_id.0, + field_id: item_id.0, + from_index: self.from_index, + to_index: self.to_index, + }) + } +} + +#[derive(Debug, Clone, Default, ProtoBuf)] +pub struct MoveRowPayloadPB { + #[pb(index = 1)] + pub view_id: String, + + #[pb(index = 2)] + pub from_row_id: String, + + #[pb(index = 4)] + pub to_row_id: String, +} + +pub struct MoveRowParams { + pub view_id: String, + pub from_row_id: String, + pub to_row_id: String, +} + +impl TryInto for MoveRowPayloadPB { + type Error = ErrorCode; + + fn try_into(self) -> Result { + let view_id = NotEmptyStr::parse(self.view_id).map_err(|_| ErrorCode::DatabaseViewIdIsEmpty)?; + let from_row_id = NotEmptyStr::parse(self.from_row_id).map_err(|_| ErrorCode::RowIdIsEmpty)?; + let to_row_id = NotEmptyStr::parse(self.to_row_id).map_err(|_| ErrorCode::RowIdIsEmpty)?; + + Ok(MoveRowParams { + view_id: view_id.0, + from_row_id: from_row_id.0, + to_row_id: to_row_id.0, + }) + } +} +#[derive(Debug, Clone, Default, ProtoBuf)] +pub struct MoveGroupRowPayloadPB { + #[pb(index = 1)] + pub view_id: String, + + #[pb(index = 2)] + pub from_row_id: String, + + #[pb(index = 3)] + pub to_group_id: String, + + #[pb(index = 4, one_of)] + pub to_row_id: Option, +} + +pub struct MoveGroupRowParams { + pub view_id: String, + pub from_row_id: String, + pub to_group_id: String, + pub to_row_id: Option, +} + +impl TryInto for MoveGroupRowPayloadPB { + type Error = ErrorCode; + + fn try_into(self) -> Result { + let view_id = NotEmptyStr::parse(self.view_id).map_err(|_| ErrorCode::DatabaseViewIdIsEmpty)?; + let from_row_id = NotEmptyStr::parse(self.from_row_id).map_err(|_| ErrorCode::RowIdIsEmpty)?; + let to_group_id = + NotEmptyStr::parse(self.to_group_id).map_err(|_| ErrorCode::GroupIdIsEmpty)?; + + let to_row_id = match self.to_row_id { + None => None, + Some(to_row_id) => Some( + NotEmptyStr::parse(to_row_id) + .map_err(|_| ErrorCode::RowIdIsEmpty)? + .0, + ), + }; + + Ok(MoveGroupRowParams { + view_id: view_id.0, + from_row_id: from_row_id.0, + to_group_id: to_group_id.0, + to_row_id, + }) + } +} #[derive(Debug, Default, ProtoBuf)] pub struct DatabaseDescPB { @@ -14,3 +168,30 @@ pub struct RepeatedDatabaseDescPB { #[pb(index = 1)] pub items: Vec, } + +#[derive(Debug, Clone, Default, ProtoBuf)] +pub struct DatabaseGroupIdPB { + #[pb(index = 1)] + pub view_id: String, + + #[pb(index = 2)] + pub group_id: String, +} + +pub struct DatabaseGroupIdParams { + pub view_id: String, + pub group_id: String, +} + +impl TryInto for DatabaseGroupIdPB { + type Error = ErrorCode; + + fn try_into(self) -> Result { + let view_id = NotEmptyStr::parse(self.view_id).map_err(|_| ErrorCode::DatabaseViewIdIsEmpty)?; + let group_id = NotEmptyStr::parse(self.group_id).map_err(|_| ErrorCode::GroupIdIsEmpty)?; + Ok(DatabaseGroupIdParams { + view_id: view_id.0, + group_id: group_id.0, + }) + } +} diff --git a/frontend/rust-lib/flowy-database/src/entities/grid_entities.rs b/frontend/rust-lib/flowy-database/src/entities/grid_entities.rs deleted file mode 100644 index ffd8264900..0000000000 --- a/frontend/rust-lib/flowy-database/src/entities/grid_entities.rs +++ /dev/null @@ -1,155 +0,0 @@ -use crate::entities::parser::NotEmptyStr; -use crate::entities::{FieldIdPB, RowPB}; -use flowy_derive::ProtoBuf; -use flowy_error::ErrorCode; - -/// [DatabasePB] describes how many fields and blocks the grid has -#[derive(Debug, Clone, Default, ProtoBuf)] -pub struct DatabasePB { - #[pb(index = 1)] - pub id: String, - - #[pb(index = 2)] - pub fields: Vec, - - #[pb(index = 3)] - pub rows: Vec, -} - -#[derive(ProtoBuf, Default)] -pub struct CreateDatabasePayloadPB { - #[pb(index = 1)] - pub name: String, -} - -#[derive(Clone, ProtoBuf, Default, Debug)] -pub struct DatabaseViewIdPB { - #[pb(index = 1)] - pub value: String, -} - -impl AsRef for DatabaseViewIdPB { - fn as_ref(&self) -> &str { - &self.value - } -} - -#[derive(Debug, Clone, Default, ProtoBuf)] -pub struct MoveFieldPayloadPB { - #[pb(index = 1)] - pub view_id: String, - - #[pb(index = 2)] - pub field_id: String, - - #[pb(index = 3)] - pub from_index: i32, - - #[pb(index = 4)] - pub to_index: i32, -} - -#[derive(Clone)] -pub struct MoveFieldParams { - pub view_id: String, - pub field_id: String, - pub from_index: i32, - pub to_index: i32, -} - -impl TryInto for MoveFieldPayloadPB { - type Error = ErrorCode; - - fn try_into(self) -> Result { - let view_id = NotEmptyStr::parse(self.view_id).map_err(|_| ErrorCode::DatabaseViewIdIsEmpty)?; - let item_id = NotEmptyStr::parse(self.field_id).map_err(|_| ErrorCode::InvalidData)?; - Ok(MoveFieldParams { - view_id: view_id.0, - field_id: item_id.0, - from_index: self.from_index, - to_index: self.to_index, - }) - } -} - -#[derive(Debug, Clone, Default, ProtoBuf)] -pub struct MoveRowPayloadPB { - #[pb(index = 1)] - pub view_id: String, - - #[pb(index = 2)] - pub from_row_id: String, - - #[pb(index = 4)] - pub to_row_id: String, -} - -pub struct MoveRowParams { - pub view_id: String, - pub from_row_id: String, - pub to_row_id: String, -} - -impl TryInto for MoveRowPayloadPB { - type Error = ErrorCode; - - fn try_into(self) -> Result { - let view_id = NotEmptyStr::parse(self.view_id).map_err(|_| ErrorCode::DatabaseViewIdIsEmpty)?; - let from_row_id = NotEmptyStr::parse(self.from_row_id).map_err(|_| ErrorCode::RowIdIsEmpty)?; - let to_row_id = NotEmptyStr::parse(self.to_row_id).map_err(|_| ErrorCode::RowIdIsEmpty)?; - - Ok(MoveRowParams { - view_id: view_id.0, - from_row_id: from_row_id.0, - to_row_id: to_row_id.0, - }) - } -} -#[derive(Debug, Clone, Default, ProtoBuf)] -pub struct MoveGroupRowPayloadPB { - #[pb(index = 1)] - pub view_id: String, - - #[pb(index = 2)] - pub from_row_id: String, - - #[pb(index = 3)] - pub to_group_id: String, - - #[pb(index = 4, one_of)] - pub to_row_id: Option, -} - -pub struct MoveGroupRowParams { - pub view_id: String, - pub from_row_id: String, - pub to_group_id: String, - pub to_row_id: Option, -} - -impl TryInto for MoveGroupRowPayloadPB { - type Error = ErrorCode; - - fn try_into(self) -> Result { - let view_id = NotEmptyStr::parse(self.view_id).map_err(|_| ErrorCode::DatabaseViewIdIsEmpty)?; - let from_row_id = NotEmptyStr::parse(self.from_row_id).map_err(|_| ErrorCode::RowIdIsEmpty)?; - let to_group_id = - NotEmptyStr::parse(self.to_group_id).map_err(|_| ErrorCode::GroupIdIsEmpty)?; - - let to_row_id = match self.to_row_id { - None => None, - Some(to_row_id) => Some( - NotEmptyStr::parse(to_row_id) - .map_err(|_| ErrorCode::RowIdIsEmpty)? - .0, - ), - }; - - Ok(MoveGroupRowParams { - view_id: view_id.0, - from_row_id: from_row_id.0, - to_group_id: to_group_id.0, - to_row_id, - }) - } -} diff --git a/frontend/rust-lib/flowy-database/src/entities/mod.rs b/frontend/rust-lib/flowy-database/src/entities/mod.rs index 9eb59729cd..e3d02d9ffc 100644 --- a/frontend/rust-lib/flowy-database/src/entities/mod.rs +++ b/frontend/rust-lib/flowy-database/src/entities/mod.rs @@ -3,7 +3,6 @@ mod cell_entities; mod database_entities; mod field_entities; pub mod filter_entities; -mod grid_entities; mod group_entities; pub mod parser; mod row_entities; @@ -14,9 +13,9 @@ mod view_entities; pub use calendar_entities::*; pub use cell_entities::*; pub use database_entities::*; +pub use database_entities::*; pub use field_entities::*; pub use filter_entities::*; -pub use grid_entities::*; pub use group_entities::*; pub use row_entities::*; pub use setting_entities::*; diff --git a/frontend/rust-lib/flowy-database/src/event_handler.rs b/frontend/rust-lib/flowy-database/src/event_handler.rs index 638c67f0fc..631ff8b7b0 100644 --- a/frontend/rust-lib/flowy-database/src/event_handler.rs +++ b/frontend/rust-lib/flowy-database/src/event_handler.rs @@ -538,6 +538,17 @@ pub(crate) async fn get_groups_handler( data_result_ok(groups) } +#[tracing::instrument(level = "trace", skip_all, err)] +pub(crate) async fn get_group_handler( + data: AFPluginData, + manager: AFPluginState>, +) -> DataResult { + let params: DatabaseGroupIdParams = data.into_inner().try_into()?; + let editor = manager.get_database_editor(¶ms.view_id).await?; + let group = editor.get_group(¶ms.view_id, ¶ms.group_id).await?; + data_result_ok(group) +} + #[tracing::instrument(level = "debug", skip(data, manager), err)] pub(crate) async fn create_board_card_handler( data: AFPluginData, diff --git a/frontend/rust-lib/flowy-database/src/event_map.rs b/frontend/rust-lib/flowy-database/src/event_map.rs index d4c737241e..aaf9b0fc95 100644 --- a/frontend/rust-lib/flowy-database/src/event_map.rs +++ b/frontend/rust-lib/flowy-database/src/event_map.rs @@ -47,7 +47,8 @@ pub fn init(database_manager: Arc) -> AFPlugin { .event(DatabaseEvent::CreateBoardCard, create_board_card_handler) .event(DatabaseEvent::MoveGroup, move_group_handler) .event(DatabaseEvent::MoveGroupRow, move_group_row_handler) - .event(DatabaseEvent::GetGroup, get_groups_handler) + .event(DatabaseEvent::GetGroups, get_groups_handler) + .event(DatabaseEvent::GetGroup, get_group_handler) // Database .event(DatabaseEvent::GetDatabases, get_databases_handler) // Calendar @@ -221,7 +222,10 @@ pub enum DatabaseEvent { UpdateDateCell = 80, #[event(input = "DatabaseViewIdPB", output = "RepeatedGroupPB")] - GetGroup = 100, + GetGroups = 100, + + #[event(input = "DatabaseGroupIdPB", output = "GroupPB")] + GetGroup = 101, #[event(input = "CreateBoardCardPayloadPB", output = "RowPB")] CreateBoardCard = 110, diff --git a/frontend/rust-lib/flowy-database/src/manager.rs b/frontend/rust-lib/flowy-database/src/manager.rs index 44f6a761cd..afff510ddf 100644 --- a/frontend/rust-lib/flowy-database/src/manager.rs +++ b/frontend/rust-lib/flowy-database/src/manager.rs @@ -207,14 +207,14 @@ impl DatabaseManager { let create_view_editor = |database_editor: Arc| async move { let user_id = user.user_id()?; let (view_pad, view_rev_manager) = make_database_view_revision_pad(view_id, user).await?; - return DatabaseViewEditor::from_pad( + DatabaseViewEditor::from_pad( &user_id, database_editor.database_view_data.clone(), database_editor.cell_data_cache.clone(), view_rev_manager, view_pad, ) - .await; + .await }; let database_editor = self @@ -224,7 +224,7 @@ impl DatabaseManager { .get(database_id) .cloned(); - return match database_editor { + match database_editor { None => { let mut editors_by_database_id = self.editors_by_database_id.write().await; let db_pool = self.database_user.db_pool()?; @@ -241,7 +241,7 @@ impl DatabaseManager { Ok(database_editor) }, - }; + } } #[tracing::instrument(level = "trace", skip(self, pool), err)] diff --git a/frontend/rust-lib/flowy-database/src/services/database/database_editor.rs b/frontend/rust-lib/flowy-database/src/services/database/database_editor.rs index c2740556e9..46e836bbf1 100644 --- a/frontend/rust-lib/flowy-database/src/services/database/database_editor.rs +++ b/frontend/rust-lib/flowy-database/src/services/database/database_editor.rs @@ -924,6 +924,11 @@ impl DatabaseEditor { self.database_views.load_groups(view_id).await } + #[tracing::instrument(level = "trace", skip_all, err)] + pub async fn get_group(&self, view_id: &str, group_id: &str) -> FlowyResult { + self.database_views.get_group(view_id, group_id).await + } + async fn create_row_rev(&self) -> FlowyResult { let field_revs = self.database_pad.read().await.get_field_revs(None)?; let block_id = self.block_id().await?; diff --git a/frontend/rust-lib/flowy-database/src/services/database_view/editor.rs b/frontend/rust-lib/flowy-database/src/services/database_view/editor.rs index 67270def56..0a7ba9a38a 100644 --- a/frontend/rust-lib/flowy-database/src/services/database_view/editor.rs +++ b/frontend/rust-lib/flowy-database/src/services/database_view/editor.rs @@ -24,7 +24,7 @@ use database_model::{ use flowy_client_sync::client_database::{ make_database_view_operations, DatabaseViewRevisionChangeset, DatabaseViewRevisionPad, }; -use flowy_error::FlowyResult; +use flowy_error::{FlowyError, FlowyResult}; use flowy_revision::RevisionManager; use flowy_sqlite::ConnectionPool; use flowy_task::TaskDispatcher; @@ -379,7 +379,7 @@ impl DatabaseViewEditor { } } } - /// Only call once after grid view editor initialized + /// Only call once after database view editor initialized #[tracing::instrument(level = "trace", skip(self))] pub async fn v_load_groups(&self) -> FlowyResult> { let groups = self @@ -394,6 +394,14 @@ impl DatabaseViewEditor { Ok(groups.into_iter().map(GroupPB::from).collect()) } + #[tracing::instrument(level = "trace", skip(self))] + pub async fn v_get_group(&self, group_id: &str) -> FlowyResult { + match self.group_controller.read().await.get_group(group_id) { + None => Err(FlowyError::record_not_found().context("Can't find the group")), + Some((_, group)) => Ok(GroupPB::from(group)), + } + } + #[tracing::instrument(level = "trace", skip(self), err)] pub async fn v_move_group(&self, params: MoveGroupParams) -> FlowyResult<()> { self diff --git a/frontend/rust-lib/flowy-database/src/services/database_view/editor_manager.rs b/frontend/rust-lib/flowy-database/src/services/database_view/editor_manager.rs index c8cf5f5453..550dc80124 100644 --- a/frontend/rust-lib/flowy-database/src/services/database_view/editor_manager.rs +++ b/frontend/rust-lib/flowy-database/src/services/database_view/editor_manager.rs @@ -1,7 +1,8 @@ #![allow(clippy::while_let_loop)] use crate::entities::{ AlterFilterParams, AlterSortParams, CreateRowParams, DatabaseViewSettingPB, DeleteFilterParams, - DeleteGroupParams, DeleteSortParams, InsertGroupParams, MoveGroupParams, RepeatedGroupPB, RowPB, + DeleteGroupParams, DeleteSortParams, GroupPB, InsertGroupParams, MoveGroupParams, + RepeatedGroupPB, RowPB, }; use crate::manager::DatabaseUser; use crate::services::cell::AtomicCellDataCache; @@ -201,6 +202,11 @@ impl DatabaseViews { Ok(RepeatedGroupPB { items: groups }) } + pub async fn get_group(&self, view_id: &str, group_id: &str) -> FlowyResult { + let view_editor = self.get_view_editor(view_id).await?; + view_editor.v_get_group(group_id).await + } + pub async fn insert_or_update_group(&self, params: InsertGroupParams) -> FlowyResult<()> { let view_editor = self.get_view_editor(¶ms.view_id).await?; view_editor.v_initialize_new_group(params).await From 45045beeb90b7b42087b22099a79986dd0e204fd Mon Sep 17 00:00:00 2001 From: "Nathan.fooo" <86001920+appflowy@users.noreply.github.com> Date: Sun, 5 Mar 2023 19:19:40 +0800 Subject: [PATCH 3/8] chore: rename document test (#1925) --- .../components/tests/DocumentTestHelper.ts | 10 +++++ .../appflowy_app/components/tests/TestAPI.tsx | 2 + .../components/tests/TestDocument.tsx | 40 +++++++++++++++++++ .../effects/document/document_bd_svc.ts | 30 ++++++++++++++ .../stores/effects/document/entities.ts | 1 + 5 files changed, 83 insertions(+) create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/tests/DocumentTestHelper.ts create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/tests/TestDocument.tsx create mode 100644 frontend/appflowy_tauri/src/appflowy_app/stores/effects/document/document_bd_svc.ts create mode 100644 frontend/appflowy_tauri/src/appflowy_app/stores/effects/document/entities.ts diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/tests/DocumentTestHelper.ts b/frontend/appflowy_tauri/src/appflowy_app/components/tests/DocumentTestHelper.ts new file mode 100644 index 0000000000..f05220df94 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/tests/DocumentTestHelper.ts @@ -0,0 +1,10 @@ +import { ViewLayoutTypePB, WorkspaceSettingPB } from '../../../services/backend'; +import { FolderEventReadCurrentWorkspace } from '../../../services/backend/events/flowy-folder'; +import { AppBackendService } from '../../stores/effects/folder/app/app_bd_svc'; + +export async function createTestDocument() { + const workspaceSetting: WorkspaceSettingPB = await FolderEventReadCurrentWorkspace().then((result) => result.unwrap()); + const app = workspaceSetting.workspace.apps.items[0]; + const appService = new AppBackendService(app.id); + return await appService.createView({ name: 'New Document', layoutType: ViewLayoutTypePB.Document }); +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/tests/TestAPI.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/tests/TestAPI.tsx index c86960c881..464fdbf959 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/tests/TestAPI.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/tests/TestAPI.tsx @@ -21,6 +21,7 @@ import { TestMoveKanbanBoardColumn, TestMoveKanbanBoardRow, } from './TestGroup'; +import { TestCreateDocument } from './TestDocument'; export const TestAPI = () => { return ( @@ -46,6 +47,7 @@ export const TestAPI = () => { + ); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/tests/TestDocument.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/tests/TestDocument.tsx new file mode 100644 index 0000000000..aad7621e36 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/tests/TestDocument.tsx @@ -0,0 +1,40 @@ +import React from 'react'; +import { createTestDocument } from './DocumentTestHelper'; +import { DocumentBackendService } from '../../stores/effects/document/document_bd_svc'; + +async function testCreateDocument() { + const view = await createTestDocument(); + const svc = new DocumentBackendService(view.id); + const document = await svc.open().then((result) => result.unwrap()); + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const content = JSON.parse(document.content); + // The initial document content: + // { + // "document": { + // "type": "editor", + // "children": [ + // { + // "type": "text" + // } + // ] + // } + // } + await svc.close(); +} + +export const TestCreateDocument = () => { + return TestButton('Test create document', testCreateDocument); +}; + +const TestButton = (title: string, onClick: () => void) => { + return ( + +
+ +
+
+ ); +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/document/document_bd_svc.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/document/document_bd_svc.ts new file mode 100644 index 0000000000..3918de746d --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/document/document_bd_svc.ts @@ -0,0 +1,30 @@ +import { + DocumentDataPB, + DocumentVersionPB, + EditPayloadPB, + FlowyError, + OpenDocumentPayloadPB, + ViewIdPB, +} from '../../../../services/backend'; +import { DocumentEventApplyEdit, DocumentEventGetDocument } from '../../../../services/backend/events/flowy-document'; +import { Result } from 'ts-results'; +import { FolderEventCloseView } from '../../../../services/backend/events/flowy-folder'; + +export class DocumentBackendService { + constructor(public readonly viewId: string) {} + + open = (): Promise> => { + const payload = OpenDocumentPayloadPB.fromObject({ document_id: this.viewId, version: DocumentVersionPB.V1 }); + return DocumentEventGetDocument(payload); + }; + + applyEdit = (operations: string) => { + const payload = EditPayloadPB.fromObject({ doc_id: this.viewId, operations: operations }); + return DocumentEventApplyEdit(payload); + }; + + close = () => { + const payload = ViewIdPB.fromObject({ value: this.viewId }); + return FolderEventCloseView(payload); + }; +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/document/entities.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/document/entities.ts new file mode 100644 index 0000000000..8ea0a151d3 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/document/entities.ts @@ -0,0 +1 @@ +export class Document {} From 99f0fad74c5028b1ea79fe809368a990a694ee57 Mon Sep 17 00:00:00 2001 From: GouravShDev <74348508+GouravShDev@users.noreply.github.com> Date: Mon, 6 Mar 2023 08:07:10 +0530 Subject: [PATCH 4/8] feat: add test for appflowy_editor path (#1926) --- .../test/core/document/path_test.dart | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/frontend/appflowy_flutter/packages/appflowy_editor/test/core/document/path_test.dart b/frontend/appflowy_flutter/packages/appflowy_editor/test/core/document/path_test.dart index cf11a96dd6..fa2725db97 100644 --- a/frontend/appflowy_flutter/packages/appflowy_editor/test/core/document/path_test.dart +++ b/frontend/appflowy_flutter/packages/appflowy_editor/test/core/document/path_test.dart @@ -29,5 +29,43 @@ void main() async { expect(p2 <= p1, true); expect(p1.equals(p2), true); }); + test( + "test path next, previous and parent getters", + () { + var p1 = [0, 0]; + var p2 = [0, 1]; + + expect(p1.next.equals(p2), true); + expect(p1.previous.equals(p2), false); + expect(p1.parent.equals(p2), false); + + p1 = [0, 1, 0]; + p2 = [0, 1, 1]; + + expect(p2.next.equals(p1), false); + expect(p2.previous.equals(p1), true); + expect(p2.parent.equals(p1), false); + + p1 = [0, 1, 1]; + p2 = [0, 1, 1]; + + expect(p1.next.equals(p2), false); + expect(p1.previous.equals(p2), false); + expect(p1.parent.equals(p2), false); + + p1 = []; + p2 = []; + + expect(p1.next.equals(p2), true); + expect(p2.previous.equals(p1), true); + expect(p1.parent.equals(p2), true); + + p1 = [1, 0, 2]; + p2 = [1, 0]; + + expect(p1.parent.equals(p2), true); + expect(p2.parent.equals(p1), false); + }, + ); }); } From d3ee346cb76be9092b0b593f490944d7332f1981 Mon Sep 17 00:00:00 2001 From: Aman Negi <37607224+AmanNegi@users.noreply.github.com> Date: Mon, 6 Mar 2023 11:40:10 +0530 Subject: [PATCH 5/8] docs(appflowy_flutter) : Update Documentation (#1910) * docs(appflowy_flutter): Update Documentation - Added basic information so that new contributors can easily get started with contributions. * docs(appflowy_flutter): Fix doc issues - Fixed the wrong Flutter version chip - Added contributions types --- frontend/appflowy_flutter/README.md | 53 ++++++++++++++++++++++------- 1 file changed, 40 insertions(+), 13 deletions(-) diff --git a/frontend/appflowy_flutter/README.md b/frontend/appflowy_flutter/README.md index a589d60ea4..74e4f803a3 100644 --- a/frontend/appflowy_flutter/README.md +++ b/frontend/appflowy_flutter/README.md @@ -1,21 +1,48 @@ -# appflowy_flutter +

AppFlowy_Flutter

+
+ + +
-A new Flutter project. +> Documentation for Contributors -## Getting Started +This Repository contains the codebase for the frontend of the application, currently we use Flutter as our frontend framework. -This project is a starting point for a Flutter application. +### Platforms Supported Using Flutter 💻 +- Linux +- macOS +- Windows +> We later expect to extend support to Android and iOS devices using Flutter. -A few resources to get you started if this is your first Flutter project: - -- [Lab: Write your first Flutter app](https://flutter.dev/docs/get-started/codelab) -- [Cookbook: Useful Flutter samples](https://flutter.dev/docs/cookbook) - -For help getting started with Flutter, view our -[online documentation](https://flutter.dev/docs), which offers tutorials, -samples, guidance on mobile development, and a full API reference. +### Am I Eligible to Contribute? +Yes! You are eligible to contribute, check out the ways in which you can [contribute to AppFlowy](https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/contributing-to-appflowy). Some of the ways in which you can contribute are: +- Non-Coding Contributions + - Documentation + - Feature Requests and Feedbacks + - Report Bugs + - Improve Translations +- Coding Contributions +To contribute to `AppFlowy_Flutter` codebase specifically (coding contribution) we suggest you to have basic knowledge of Flutter. In case you are new to Flutter, we may suggest you to learn the basics and then try to contribute, get started with Flutter [here](https://flutter.dev/docs/get-started/codelab). + +### What OS Should I Use for Development? +We support all OS for Development i.e Linux, macOS and Windows. However, most of us promote macOS and Linux over Windows. We have detailed [docs](https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/software-contributions/environment-setup) on How to Setup `AppFlowy_Flutter` in your local system in each OS. + + +### Getting Started ❇ +We have a detailed documentation, on how to [get started](https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/contributing-to-appflowy) with the project, and make your first contribution. However, we do have some specific picks for you. +- [Code Architecture](https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/architecture/frontend/frontend/codemap) +- [Making Your First PR](https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/software-contributions/submitting-code/submitting-your-first-pull-request) +- [The Style Guide](https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/software-contributions/submitting-code/style-guides) +- [How to run/debug the application](https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/software-contributions/launcher-and-tasks) + + +### Need Help? +- New to GitHub? Follow [these](https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/software-contributions/submitting-code/setting-up-your-repositories) steps to get started +- Stuck Somewhere? Join the [Discord](https://discord.gg/9Q2xaN37tV) Group and we are there to help you! + + From 675c833f070c3eb66df426954dfbbedceabb93e4 Mon Sep 17 00:00:00 2001 From: GouravShDev <74348508+GouravShDev@users.noreply.github.com> Date: Tue, 7 Mar 2023 07:03:35 +0530 Subject: [PATCH 6/8] feat: add test for appflowy_editor attributes (#1931) --- .../test/core/document/attributes_test.dart | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/frontend/appflowy_flutter/packages/appflowy_editor/test/core/document/attributes_test.dart b/frontend/appflowy_flutter/packages/appflowy_editor/test/core/document/attributes_test.dart index 873ab2788b..a7b2349944 100644 --- a/frontend/appflowy_flutter/packages/appflowy_editor/test/core/document/attributes_test.dart +++ b/frontend/appflowy_flutter/packages/appflowy_editor/test/core/document/attributes_test.dart @@ -54,6 +54,37 @@ void main() async { 'b': 3, 'c': 4, }); + expect(invertAttributes(null, base), { + 'a': null, + 'b': null, + }); + expect(invertAttributes(other, null), { + 'b': 3, + 'c': 4, + }); }); + test( + "hasAttributes", + () { + final base = { + 'a': 1, + 'b': 2, + }; + final other = { + 'c': 3, + 'd': 4, + }; + + var x = hashAttributes(base); + var y = hashAttributes(base); + // x & y should have same hash code + expect(x == y, true); + + y = hashAttributes(other); + + // x & y should have different hash code + expect(x == y, false); + }, + ); }); } From 7ff4cecd09b569ac9c371073fd7d527264191b60 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Tue, 7 Mar 2023 09:33:59 +0800 Subject: [PATCH 7/8] feat: add cover migration for document (#1929) * feat: add cover migration for document * fix: should not delete the cover when selecting all * fix: chinese characters for openai --- .../document/application/doc_bloc.dart | 79 +++++++++++++++---- .../plugins/openai/service/openai_client.dart | 16 +++- .../home/menu/app/header/add_button.dart | 16 +--- .../backspace_handler.dart | 5 +- 4 files changed, 81 insertions(+), 35 deletions(-) diff --git a/frontend/appflowy_flutter/lib/plugins/document/application/doc_bloc.dart b/frontend/appflowy_flutter/lib/plugins/document/application/doc_bloc.dart index 5d0c0cd0f9..ae47a120ca 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/application/doc_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/application/doc_bloc.dart @@ -1,15 +1,18 @@ import 'dart:convert'; +import 'package:appflowy/plugins/document/presentation/plugins/cover/cover_node_widget.dart'; import 'package:appflowy/plugins/trash/application/trash_service.dart'; import 'package:appflowy/user/application/user_service.dart'; import 'package:appflowy/workspace/application/view/view_listener.dart'; import 'package:appflowy/plugins/document/application/doc_service.dart'; +import 'package:appflowy_backend/protobuf/flowy-document/entities.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pbserver.dart'; import 'package:appflowy_editor/appflowy_editor.dart' - show EditorState, Document, Transaction; + show EditorState, Document, Transaction, Node; import 'package:appflowy_backend/protobuf/flowy-folder/trash.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:appflowy_backend/log.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:dartz/dartz.dart'; @@ -78,29 +81,27 @@ class DocumentBloc extends Bloc { Future _initial(Initial value, Emitter emit) async { final userProfile = await UserBackendService.getCurrentUserProfile(); if (userProfile.isRight()) { - emit( + return emit( state.copyWith( loadingState: DocumentLoadingState.finish( right(userProfile.asRight()), ), ), ); - return; } final result = await _documentService.openDocument(view: view); - result.fold( - (documentData) { - final document = Document.fromJson(jsonDecode(documentData.content)); - editorState = EditorState(document: document); - _listenOnDocumentChange(); - emit( - state.copyWith( - loadingState: DocumentLoadingState.finish(left(unit)), - userProfilePB: userProfile.asLeft(), - ), - ); + return result.fold( + (documentData) async { + await _initEditorState(documentData).whenComplete(() { + emit( + state.copyWith( + loadingState: DocumentLoadingState.finish(left(unit)), + userProfilePB: userProfile.asLeft(), + ), + ); + }); }, - (err) { + (err) async { emit( state.copyWith( loadingState: DocumentLoadingState.finish(right(err)), @@ -127,8 +128,13 @@ class DocumentBloc extends Bloc { ); } - void _listenOnDocumentChange() { - _subscription = editorState?.transactionStream.listen((transaction) { + Future _initEditorState(DocumentDataPB documentData) async { + final document = Document.fromJson(jsonDecode(documentData.content)); + final editorState = EditorState(document: document); + this.editorState = editorState; + + // listen on document change + _subscription = editorState.transactionStream.listen((transaction) { final json = jsonEncode(TransactionAdaptor(transaction).toJson()); _documentService .applyEdit(docId: view.id, operations: json) @@ -139,6 +145,15 @@ class DocumentBloc extends Bloc { ); }); }); + // log + if (kDebugMode) { + editorState.logConfiguration.handler = (log) { + Log.debug(log); + }; + } + // migration + final migration = DocumentMigration(editorState: editorState); + await migration.apply(); } } @@ -215,3 +230,33 @@ class TransactionAdaptor { return json; } } + +class DocumentMigration { + const DocumentMigration({ + required this.editorState, + }); + + final EditorState editorState; + + /// Migrate the document to the latest version. + Future apply() async { + final transaction = editorState.transaction; + + // A temporary solution to migrate the document to the latest version. + // Once the editor is stable, we can remove this. + + // cover plugin + if (editorState.document.nodeAtPath([0])?.type != kCoverType) { + transaction.insertNode( + [0], + Node(type: kCoverType), + ); + } + + transaction.afterSelection = null; + + if (transaction.operations.isNotEmpty) { + editorState.apply(transaction); + } + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/service/openai_client.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/service/openai_client.dart index 3bd41e2d52..37a5279662 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/service/openai_client.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/service/openai_client.dart @@ -91,7 +91,13 @@ class HttpOpenAIRepository implements OpenAIRepository { ); if (response.statusCode == 200) { - return Right(TextCompletionResponse.fromJson(json.decode(response.body))); + return Right( + TextCompletionResponse.fromJson( + json.decode( + utf8.decode(response.bodyBytes), + ), + ), + ); } else { return Left(OpenAIError.fromJson(json.decode(response.body)['error'])); } @@ -119,7 +125,13 @@ class HttpOpenAIRepository implements OpenAIRepository { ); if (response.statusCode == 200) { - return Right(TextEditResponse.fromJson(json.decode(response.body))); + return Right( + TextEditResponse.fromJson( + json.decode( + utf8.decode(response.bodyBytes), + ), + ), + ); } else { return Left(OpenAIError.fromJson(json.decode(response.body)['error'])); } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/app/header/add_button.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/app/header/add_button.dart index 5d1bfee6bc..642cc063da 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/app/header/add_button.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/app/header/add_button.dart @@ -1,10 +1,9 @@ import 'package:appflowy/plugins/document/document.dart'; -import 'package:appflowy/plugins/document/presentation/plugins/cover/cover_node_widget.dart'; import 'package:appflowy/startup/plugin/plugin.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/workspace/presentation/home/menu/app/header/import/import_panel.dart'; import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart'; -import 'package:appflowy_editor/appflowy_editor.dart' show Document, Node; +import 'package:appflowy_editor/appflowy_editor.dart' show Document; import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:flowy_infra/image.dart'; import 'package:flowy_infra_ui/style_widget/icon_button.dart'; @@ -61,12 +60,7 @@ class AddButton extends StatelessWidget { }, onSelected: (action, controller) { if (action is AddButtonActionWrapper) { - Document? document; - if (action.pluginType == PluginType.editor) { - // initialize the document if needed. - document = buildInitialDocument(); - } - onSelected(action.pluginBuilder, document); + onSelected(action.pluginBuilder, null); } if (action is ImportActionWrapper) { showImportPanel(context, (document) { @@ -80,12 +74,6 @@ class AddButton extends StatelessWidget { }, ); } - - Document buildInitialDocument() { - final document = Document.empty(); - document.insert([0], [Node(type: kCoverType)]); - return document; - } } class AddButtonActionWrapper extends ActionCell { diff --git a/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/backspace_handler.dart b/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/backspace_handler.dart index 49f5da8798..2a1f9db324 100644 --- a/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/backspace_handler.dart +++ b/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/backspace_handler.dart @@ -12,8 +12,9 @@ ShortcutEventHandler backspaceEventHandler = (editorState, event) { nodes = selection.isBackward ? nodes : nodes.reversed.toList(growable: false); selection = selection.isBackward ? selection : selection.reversed; final textNodes = nodes.whereType().toList(); - final List nonTextNodes = - nodes.where((node) => node is! TextNode).toList(growable: false); + final List nonTextNodes = nodes + .where((node) => node is! TextNode && node.selectable != null) + .toList(growable: false); final transaction = editorState.transaction; List? cancelNumberListPath; From 9e235c578e2eaea2233638c0a6e03abdd1bf289a Mon Sep 17 00:00:00 2001 From: Anderson Cardoso <43047877+andercard0@users.noreply.github.com> Date: Mon, 6 Mar 2023 22:35:04 -0300 Subject: [PATCH 8/8] chore: Update 3: Brazilian Portuguese Localization (#1907) - Fix typos; - Fix inconsistencies; - Removed duplicated block strings. Note: There are some word by word texts that I still need to figure out how to adapt properly; though, do believe most of strings are good to go! Please review! --- .../assets/translations/pt-BR.json | 190 +++++++++++++++--- 1 file changed, 163 insertions(+), 27 deletions(-) diff --git a/frontend/appflowy_flutter/assets/translations/pt-BR.json b/frontend/appflowy_flutter/assets/translations/pt-BR.json index 991e8126d2..aebe816ff0 100644 --- a/frontend/appflowy_flutter/assets/translations/pt-BR.json +++ b/frontend/appflowy_flutter/assets/translations/pt-BR.json @@ -1,7 +1,7 @@ { "appName": "AppFlowy", "defaultUsername": "Eu", - "welcomeText": "Bem-vindo @:appName", + "welcomeText": "Bem-vindo ao @:appName", "githubStarText": "Dar uma estrela no Github", "subscribeNewsletterText": "Inscreva-se para receber novidades", "letsGoButtonText": "Vamos lá", @@ -30,15 +30,22 @@ "unmatchedPasswordError": "As senhas não conferem." }, "workspace": { - "create": "Crie uma área de trabalho", + "create": "Crie um espaço de trabalho", "hint": "Espaço de trabalho", - "notFoundError": "Espaço de trabalho não encontrada" + "notFoundError": "Espaço de trabalho não encontrado" }, "shareAction": { "buttonText": "Compartilhar", "workInProgress": "Em breve", "markdown": "Marcador", - "copyLink": "Copiar o link" + "copyLink": "Copiar link" + }, + "moreAction": { + "small": "pequeno", + "medium": "médio", + "large": "grande", + "fontSize": "Tamanho da fonte", + "import": "Importar" }, "disclosureAction": { "rename": "Renomear", @@ -64,6 +71,7 @@ }, "dialogCreatePageNameHint": "Nome da página", "questionBubble": { + "shortcuts": "Atalhos", "whatsNew": "O que há de novo?", "help": "Ajuda e Suporte", "debug": { @@ -90,14 +98,17 @@ "inlineCode": "Embutir código", "quote": "Citação em bloco", "header": "Cabeçalho", - "highlight": "Realçar" + "highlight": "Destacar", + "color": "Cor" }, "tooltip": { "lightMode": "Mudar para o modo claro", "darkMode": "Mudar para o modo escuro", "openAsPage": "Abrir como uma página", "addNewRow": "Adicionar uma nova linha", - "openMenu": "Clique para abrir o menu" + "openMenu": "Clique para abrir o menu", + "viewDataBase": "Visualizar banco de dados", + "referencePage": "Esta {name} é uma referência" }, "sideBar": { "openSidebar": "Abrir barra lateral", @@ -121,10 +132,16 @@ "signIn": "Conectar", "signOut": "Desconectar", "complete": "Completar", - "save": "Salvar" + "save": "Salvar", + "generate": "Gerar", + "esc": "Sair", + "keep": "Manter", + "tryAGain": "Tentar novamente", + "discard": "Descartar", + "replace": "substituir" }, "label": { - "welcome": "Welcome!", + "welcome": "Bem-vindo!", "firstName": "Nome", "middleName": "Sobrenome", "lastName": "Último nome", @@ -149,28 +166,102 @@ "appearance": "Aparência", "language": "Idioma", "user": "Usuário", - "open": "Abrir as Configurações" + "files": "Arquivos", + "open": "Abrir Configurações" }, "appearance": { "themeMode": { - "label": "Theme Mode", - "light": "Modo Claro", - "dark": "Modo Escuro", - "system": "Adapt to System" - } + "label": "Tema", + "light": "Modo claro", + "dark": "Modo escuro", + "system": "Adaptar-se ao sistema" + }, + "theme": "Tema" + }, + "files": { + "defaultLocation": "Onde os seus dados ficam armazenados", + "doubleTapToCopy": "Clique duas vezes para copiar o caminho", + "restoreLocation": "Restaurar para o caminho padrão do AppFlowy", + "customizeLocation": "Abrir outra pasta", + "restartApp": "Reinicie o aplicativo para que as alterações entrem em vigor.", + "exportDatabase": "Exportar banco de dados", + "selectFiles": "Escolha os arquivos que precisam ser exportados", + "createNewFolder": "Criar uma nova pasta", + "createNewFolderDesc": "Diga-nos onde pretende armazenar os seus dados ...", + "open": "Abrir", + "openFolder": "Abra uma pasta existente", + "openFolderDesc": "Gravar na pasta AppFlowy existente ...", + "folderHintText": "nome da pasta", + "location": "Criando nova pasta", + "locationDesc": "Escolha um nome para sua pasta de dados do AppFlowy", + "browser": "Navegar", + "create": "Criar", + "folderPath": "Caminho para armazenar sua pasta", + "locationCannotBeEmpty": "O caminho não pode estar vazio" + }, + "user": { + "name": "Nome", + "icon": "Ícone", + "selectAnIcon": "Escolha um ícone", + "pleaseInputYourOpenAIKey": "por favor insira sua chave OpenAI" } }, "grid": { "settings": { "filter": "Filtro", - "sortBy": "Ordenar por", + "sort": "Organizar", + "sortBy": "Organizar por", "Properties": "Propriedades", - "group": "Grupo" + "group": "Grupo", + "addFilter": "Adicionar filtro", + "deleteFilter": "Apagar filtro", + "filterBy": "Filtrar por...", + "typeAValue": "Digite um valor..." + }, + "textFilter": { + "contains": "Contém", + "doesNotContain": "Não contém", + "endsWith": "Termina com", + "startWith": "Inicia com", + "is": "É", + "isNot": "Não é", + "isEmpty": "Está vazio", + "isNotEmpty": "Não está vazio", + "choicechipPrefix": { + "isNot": "Não", + "startWith": "Inicia com", + "endWith": "Termina com", + "isEmpty": "está vazio", + "isNotEmpty": "não está vazio" + } + }, + "checkboxFilter": { + "isChecked": "Marcado", + "isUnchecked": "Desmarcado", + "choicechipPrefix": { + "is": "está" + } + }, + "checklistFilter": { + "isComplete": "está completo", + "isIncomplted": "está imcompleto" + }, + "singleSelectOptionFilter": { + "is": "Está", + "isNot": "Não está", + "isEmpty": "Está vazio", + "isNotEmpty": "Não está vazio" + }, + "multiSelectOptionFilter": { + "contains": "Contém", + "doesNotContain": "Não contém", + "isEmpty": "Está vazio", + "isNotEmpty": "Está vazio" }, "field": { - "hide": "Esconder", - "insertLeft": "Inserir à esquerda", - "insertRight": "Inserir à direita", + "hide": "Ocultar", + "insertLeft": "Inserir a esquerda", + "insertRight": "Inserir a direita", "duplicate": "Duplicar", "delete": "Apagar", "textFieldName": "Texto", @@ -178,32 +269,40 @@ "dateFieldName": "Data", "numberFieldName": "Números", "singleSelectFieldName": "Selecionar", - "multiSelectFieldName": "Seleção múltipla", + "multiSelectFieldName": "Multi seleção", "urlFieldName": "URL", + "checklistFieldName": "Lista", "numberFormat": "Formato numérico", "dateFormat": "Formato de data", - "includeTime": "Incluir horário", - "dateFormatFriendly": "Mês/Dia/Ano", - "dateFormatISO": "Ano/Mês/Dia", + "includeTime": "Incluir hora", + "dateFormatFriendly": "Mês Dia,Ano", + "dateFormatISO": "Ano-Mês-Dia", "dateFormatLocal": "Mês/Dia/Ano", "dateFormatUS": "Ano/Mês/Dia", "timeFormat": "Formato de hora", - "invalidTimeFormat": "Formato Inválido", + "invalidTimeFormat": "Formato inválido", "timeFormatTwelveHour": "12 horas", "timeFormatTwentyFourHour": "24 horas", "addSelectOption": "Adicionar uma opção", "optionTitle": "Opções", - "addOption": "Adicionar opção", + "addOption": "Adicioar opção", "editProperty": "Editar propriedade", "newColumn": "Nova coluna", "deleteFieldPromptMessage": "Tem certeza? Esta propriedade será excluída" }, + "sort": { + "ascending": "Crescente", + "descending": "Decrescente", + "deleteSort": "Apagar ordenação", + "addSort": "Adicionar ordenação" + }, "row": { "duplicate": "Duplicar", "delete": "Apagar", "textPlaceholder": "Vazio", "copyProperty": "Propriedade copiada para a área de transferência", - "count": "Contagem" + "count": "Contagem", + "newRow": "Nova linha" }, "selectOption": { "create": "Criar", @@ -219,7 +318,10 @@ "deleteTag": "Apagar etiqueta", "colorPannelTitle": "Cores", "pannelTitle": "Escolha uma opção ou crie uma", - "searchOption": "Procure uma opção" + "searchOption": "Procurar uma opção" + }, + "checklist": { + "panelTitle": "Adicionar um item" }, "menuName": "Grade" }, @@ -228,11 +330,45 @@ "date": { "timeHintTextInTwelveHour": "01:00 PM", "timeHintTextInTwentyFourHour": "13:00" + }, + "slashMenu": { + "board": { + "selectABoardToLinkTo": "Selecione um quadro para vincular" + }, + "grid": { + "selectAGridToLinkTo": "Selecione um grade para vincular" + } + }, + "plugins": { + "referencedBoard": "Quadro vinculado", + "referencedGrid": "Grade vinculado", + "autoCompletionMenuItemName": "Preenchimento Automático", + "autoGeneratorMenuItemName": "Gerar nome automaticamente", + "autoGeneratorTitleName": "Gerar por IA", + "autoGeneratorLearnMore": "Saiba mais", + "autoGeneratorGenerate": "Gerar", + "autoGeneratorHintText": "Diga-nos o que você deseja gerar por IA ...", + "autoGeneratorCantGetOpenAIKey": "Não foi possível obter a chave da OpenAI", + "smartEditTitleName": "IA: edição inteligente", + "smartEditFixSpelling": "Corrigir ortografia", + "smartEditSummarize": "Resumir", + "smartEditCouldNotFetchResult": "Não foi possível obter o resultado do OpenAI", + "smartEditCouldNotFetchKey": "Não foi possível obter a chave OpenAI" } }, "board": { "column": { "create_new_card": "Novo" + }, + "menuName": "Quadro" + }, + "calendar": { + "menuName": "Calendário", + "navigation": { + "today": "Hoje", + "jumpToday": "Pular para hoje", + "previousMonth": "Mês anterior", + "nextMonth": "Próximo mês" } } } \ No newline at end of file