diff --git a/frontend/.vscode/tasks.json b/frontend/.vscode/tasks.json index 5e241241f1..227a5cbfba 100644 --- a/frontend/.vscode/tasks.json +++ b/frontend/.vscode/tasks.json @@ -169,8 +169,8 @@ "label": "AF: Tauri UI Dev", "type": "shell", "isBackground": true, - "command": "npm run dev", - "problemMatcher": "$tsc-watch", + "command": "yarn", + "args": ["dev"], "options": { "cwd": "${workspaceFolder}/appflowy_tauri" } @@ -179,7 +179,6 @@ "label": "AF: Tauri UI Build", "type": "shell", "command": "npm run build", - "problemMatcher": "$tsc-watch", "options": { "cwd": "${workspaceFolder}/appflowy_tauri" } @@ -187,8 +186,7 @@ { "label": "AF: Tauri Dev", "type": "shell", - "command": "npm run tauri dev", - "problemMatcher": "$tsc-watch", + "command": "npm run tauri:dev", "options": { "cwd": "${workspaceFolder}/appflowy_tauri" } @@ -199,8 +197,7 @@ "command": "cargo make tauri_clean", "options": { "cwd": "${workspaceFolder}" - }, - "problemMatcher": "$tsc-watch" + } }, { "label": "AF: Tauri Clean + Dev", diff --git a/frontend/appflowy_tauri/package.json b/frontend/appflowy_tauri/package.json index b0af06ca4d..18fb005b5e 100644 --- a/frontend/appflowy_tauri/package.json +++ b/frontend/appflowy_tauri/package.json @@ -17,10 +17,13 @@ "@reduxjs/toolkit": "^1.9.2", "@tauri-apps/api": "^1.2.0", "google-protobuf": "^3.21.2", + "i18next": "^22.4.10", + "i18next-browser-languagedetector": "^7.0.1", "jest": "^29.4.3", "nanoid": "^4.0.0", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-i18next": "^12.2.0", "react-redux": "^8.0.5", "react-router-dom": "^6.8.0", "react18-input-otp": "^1.1.2", @@ -44,9 +47,9 @@ "eslint": "^8.34.0", "eslint-plugin-react": "^7.32.2", "postcss": "^8.4.21", - "prettier": "^2.8.3", + "prettier": "2.8.4", "prettier-plugin-tailwindcss": "^0.2.2", - "tailwindcss": "^3.2.4", + "tailwindcss": "^3.2.7", "ts-jest": "^29.0.5", "typescript": "^4.6.4", "vite": "^4.0.0" diff --git a/frontend/appflowy_tauri/src-tauri/Cargo.lock b/frontend/appflowy_tauri/src-tauri/Cargo.lock index f38f5bc6a2..84e76e18ec 100644 --- a/frontend/appflowy_tauri/src-tauri/Cargo.lock +++ b/frontend/appflowy_tauri/src-tauri/Cargo.lock @@ -88,6 +88,7 @@ dependencies = [ "serde_json", "tauri", "tauri-build", + "tauri-utils", "tracing", ] diff --git a/frontend/appflowy_tauri/src-tauri/Cargo.toml b/frontend/appflowy_tauri/src-tauri/Cargo.toml index 84ed6fe03a..42c5cd15b7 100644 --- a/frontend/appflowy_tauri/src-tauri/Cargo.toml +++ b/frontend/appflowy_tauri/src-tauri/Cargo.toml @@ -17,6 +17,7 @@ tauri-build = { version = "1.2", features = [] } serde_json = "1.0" serde = { version = "1.0", features = ["derive"] } tauri = { version = "1.2", features = ["shell-open"] } +tauri-utils = "1.2" bytes = { version = "1.0" } tracing = { version = "0.1", features = ["log"] } lib-dispatch = { path = "../../rust-lib/lib-dispatch", features = ["use_serde"] } diff --git a/frontend/appflowy_tauri/src-tauri/src/init.rs b/frontend/appflowy_tauri/src-tauri/src/init.rs index 8ce59c187d..f368fadb2f 100644 --- a/frontend/appflowy_tauri/src-tauri/src/init.rs +++ b/frontend/appflowy_tauri/src-tauri/src/init.rs @@ -1,10 +1,21 @@ use flowy_core::{get_client_server_configuration, AppFlowyCore, AppFlowyCoreConfig}; pub fn init_flowy_core() -> AppFlowyCore { - let data_path = tauri::api::path::data_dir().unwrap(); - let path = format!("{}/AppFlowy", data_path.to_str().unwrap()); + let config_json = include_str!("../tauri.conf.json"); + let config: tauri_utils::config::Config = serde_json::from_str(config_json).unwrap(); + + let mut data_path = tauri::api::path::app_local_data_dir(&config).unwrap(); + if cfg!(debug_assertions) { + data_path.push("dev"); + } + data_path.push("data"); + let server_config = get_client_server_configuration().unwrap(); - let config = AppFlowyCoreConfig::new(&path, "AppFlowy".to_string(), server_config) - .log_filter("trace", vec!["appflowy_tauri".to_string()]); + let config = AppFlowyCoreConfig::new( + data_path.to_str().unwrap(), + "AppFlowy".to_string(), + server_config, + ) + .log_filter("trace", vec!["appflowy_tauri".to_string()]); AppFlowyCore::new(config) } diff --git a/frontend/appflowy_tauri/src-tauri/tauri.conf.json b/frontend/appflowy_tauri/src-tauri/tauri.conf.json index b2dab2bcab..e2351d80a5 100644 --- a/frontend/appflowy_tauri/src-tauri/tauri.conf.json +++ b/frontend/appflowy_tauri/src-tauri/tauri.conf.json @@ -60,10 +60,11 @@ "windows": [ { "fullscreen": false, - "height": 600, + "height": 1000, "resizable": true, "title": "AppFlowy", - "width": 800 + "width": 1200, + "transparent": true } ] } diff --git a/frontend/appflowy_tauri/src/appflowy_app/App.tsx b/frontend/appflowy_tauri/src/appflowy_app/App.tsx index 48601f4cd2..f586610cf0 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/App.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/App.tsx @@ -1,7 +1,6 @@ import { Routes, Route, BrowserRouter } from 'react-router-dom'; import { TestColors } from './components/TestColors/TestColors'; -import TestApiButton from './components/TestApiButton/TestApiButton'; import { Welcome } from './views/Welcome'; import { Provider } from 'react-redux'; import { store } from './stores/store'; @@ -12,19 +11,20 @@ import { LoginPage } from './views/LoginPage'; import { ProtectedRoutes } from './components/auth/ProtectedRoutes'; 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'; + +initializeI18n(); const App = () => { - // const location = useLocation(); - - // console.log(location); - return ( }> } /> - } /> + } /> } /> } /> } /> @@ -33,8 +33,8 @@ const App = () => { }> }> }> - Not Found + ); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/TestApiButton/DatabaseTestHelper.ts b/frontend/appflowy_tauri/src/appflowy_app/components/TestApiButton/DatabaseTestHelper.ts new file mode 100644 index 0000000000..f04b9d7b41 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/TestApiButton/DatabaseTestHelper.ts @@ -0,0 +1,106 @@ +import { FieldType, 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'; +import { RowInfo } from '../../stores/effects/database/row/row_cache'; +import { RowController } from '../../stores/effects/database/row/row_controller'; +import { + CellControllerBuilder, + DateCellController, + NumberCellController, + SelectOptionCellController, + TextCellController, +} from '../../stores/effects/database/cell/controller_builder'; +import assert from 'assert'; +import { None, Option, Some } from 'ts-results'; + +// Create a database view for specific layout type +// Do not use it production code. Just for testing +export async function createTestDatabaseView(layout: ViewLayoutTypePB): Promise { + 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 Grid', layoutType: layout }); +} + +export async function openTestDatabase(viewId: string): Promise { + return new DatabaseController(viewId); +} + +export async function assertTextCell(rowInfo: RowInfo, databaseController: DatabaseController, expectedContent: string) { + const cellController = await makeTextCellController(rowInfo, databaseController).then((result) => result.unwrap()); + cellController.subscribeChanged({ + onCellChanged: (value) => { + const cellContent = value.unwrap(); + if (cellContent !== expectedContent) { + throw Error(); + } + }, + }); + cellController.getCellData(); +} + +export async function editTextCell(rowInfo: RowInfo, databaseController: DatabaseController, content: string) { + const cellController = await makeTextCellController(rowInfo, databaseController).then((result) => result.unwrap()); + await cellController.saveCellData(content); +} + +export async function makeTextCellController( + rowInfo: RowInfo, + databaseController: DatabaseController +): Promise> { + const builder = await makeCellControllerBuilder(rowInfo, FieldType.RichText, databaseController).then((result) => + result.unwrap() + ); + return Some(builder.build() as TextCellController); +} + +export async function makeNumberCellController( + rowInfo: RowInfo, + databaseController: DatabaseController +): Promise> { + const builder = await makeCellControllerBuilder(rowInfo, FieldType.Number, databaseController).then((result) => + result.unwrap() + ); + return Some(builder.build() as NumberCellController); +} + +export async function makeSingleSelectCellController( + rowInfo: RowInfo, + databaseController: DatabaseController +): Promise> { + const builder = await makeCellControllerBuilder(rowInfo, FieldType.SingleSelect, databaseController).then((result) => + result.unwrap() + ); + return Some(builder.build() as SelectOptionCellController); +} + +export async function makeDateCellController( + rowInfo: RowInfo, + databaseController: DatabaseController +): Promise> { + const builder = await makeCellControllerBuilder(rowInfo, FieldType.DateTime, databaseController).then((result) => + result.unwrap() + ); + return Some(builder.build() as DateCellController); +} + +export async function makeCellControllerBuilder( + rowInfo: RowInfo, + fieldType: FieldType, + databaseController: DatabaseController +): Promise> { + const rowCache = databaseController.databaseViewCache.getRowCache(); + const cellCache = rowCache.getCellCache(); + const fieldController = databaseController.fieldController; + const rowController = new RowController(rowInfo, fieldController, rowCache); + const cellByFieldId = await rowController.loadCells(); + for (const cellIdentifier of cellByFieldId.values()) { + const builder = new CellControllerBuilder(cellIdentifier, cellCache, fieldController); + if (cellIdentifier.fieldType === fieldType) { + return Some(builder); + } + } + + return None; +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/TestApiButton/TestAPI.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/TestApiButton/TestAPI.tsx new file mode 100644 index 0000000000..9a3098f601 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/TestApiButton/TestAPI.tsx @@ -0,0 +1,16 @@ +import React from 'react'; +import TestApiButton from './TestApiButton'; +import { TestCreateGrid, TestCreateSelectOption, TestEditCell } from './TestGrid'; + +export const TestAPI = () => { + return ( + +
    + + + + +
+
+ ); +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/TestApiButton/TestApiButton.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/TestApiButton/TestApiButton.tsx index e25973e66c..161e94d596 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/TestApiButton/TestApiButton.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/TestApiButton/TestApiButton.tsx @@ -1,6 +1,4 @@ -import { - SignInPayloadPB, -} from '../../../services/backend/models/flowy-user/index'; +import { SignInPayloadPB } from '../../../services/backend/models/flowy-user/index'; import { nanoid } from 'nanoid'; import { UserNotificationListener } from '../user/application/notifications'; import { @@ -119,10 +117,8 @@ const TestApiButton = () => { return ( <> -

Welcome to AppFlowy!

-
-
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/TestApiButton/TestGrid.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/TestApiButton/TestGrid.tsx new file mode 100644 index 0000000000..58fc8497ad --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/TestApiButton/TestGrid.tsx @@ -0,0 +1,93 @@ +import React from 'react'; +import { SelectOptionCellDataPB, ViewLayoutTypePB } from '../../../services/backend'; +import { Log } from '../../utils/log'; +import { + assertTextCell, + createTestDatabaseView, + editTextCell, + makeSingleSelectCellController, + openTestDatabase, +} from './DatabaseTestHelper'; +import assert from 'assert'; +import { SelectOptionBackendService } from '../../stores/effects/database/cell/select_option_bd_svc'; + +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) => { + assert(rows.length === 3); + }, + onFieldsChanged: (fields) => { + assert(fields.length === 3); + }, + }); + await databaseController.open().then((result) => result.unwrap()); + } + + 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); + databaseController.subscribe({ + onRowsChanged: async (rows) => { + for (const [index, row] of rows.entries()) { + const cellContent = index.toString(); + await editTextCell(row, databaseController, cellContent); + await assertTextCell(row, databaseController, cellContent); + } + }, + }); + await databaseController.open().then((result) => result.unwrap()); + } + + return TestButton('Test editing cell', testGridRow); +}; + +export const TestCreateSelectOption = () => { + async function testCreateOption() { + const view = await createTestDatabaseView(ViewLayoutTypePB.Grid); + const databaseController = await openTestDatabase(view.id); + databaseController.subscribe({ + onRowsChanged: async (rows) => { + for (const [index, row] of rows.entries()) { + if (index === 0) { + const cellController = await makeSingleSelectCellController(row, databaseController).then((result) => + result.unwrap() + ); + cellController.subscribeChanged({ + onCellChanged: (value) => { + const option: SelectOptionCellDataPB = value.unwrap(); + console.log(option); + }, + }); + const backendSvc = new SelectOptionBackendService(cellController.cellIdentifier); + await backendSvc.createOption({ name: 'option' + index }); + } + } + }, + }); + await databaseController.open().then((result) => result.unwrap()); + } + + return TestButton('Test create a select option', testCreateOption); +}; + +const TestButton = (title: string, onClick: () => void) => { + return ( + +
+ +
+
+ ); +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/Database.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/Database.hooks.ts new file mode 100644 index 0000000000..d5a4345c92 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/Database.hooks.ts @@ -0,0 +1,45 @@ +import { useAppDispatch, useAppSelector } from '../../stores/store'; +import { useEffect, useState } from 'react'; +import { databaseActions, IDatabase } from '../../stores/reducers/database/slice'; +import { nanoid } from 'nanoid'; +import { FieldType } from '../../../services/backend'; + +export const useDatabase = () => { + const dispatch = useAppDispatch(); + const database = useAppSelector((state) => state.database); + + const newField = () => { + dispatch( + databaseActions.addField({ + field: { + fieldId: nanoid(8), + fieldType: FieldType.RichText, + fieldOptions: {}, + title: 'new field', + }, + }) + ); + }; + + const renameField = (fieldId: string, newTitle: string) => { + const field = database.fields[fieldId]; + field.title = newTitle; + + dispatch( + databaseActions.updateField({ + field, + }) + ); + }; + + const newRow = () => { + dispatch(databaseActions.addRow()); + }; + + return { + database, + newField, + renameField, + newRow, + }; +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/LanguageSelectPopup.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/LanguageSelectPopup.tsx new file mode 100644 index 0000000000..fcb65cbba4 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/LanguageSelectPopup.tsx @@ -0,0 +1,44 @@ +import { IPopupItem, Popup } from './Popup'; +import i18n from 'i18next'; + +const supportedLanguages: { key: string; title: string }[] = [ + { + key: 'en', + title: 'English', + }, + { key: 'ca-ES', title: 'ca-ES' }, + { key: 'de-DE', title: 'de-DE' }, + { key: 'es-VE', title: 'es-VE' }, + { key: 'eu-ES', title: 'eu-ES' }, + { key: 'fr-CA', title: 'fr-CA' }, + { key: 'fr-FR', title: 'fr-FR' }, + { key: 'hu-HU', title: 'hu-HU' }, + { key: 'id-ID', title: 'id-ID' }, + { key: 'it-IT', title: 'it-IT' }, + { key: 'ja-JP', title: 'ja-JP' }, + { key: 'ko-KR', title: 'ko-KR' }, + { key: 'pl-PL', title: 'pl-PL' }, + { key: 'pt-BR', title: 'pt-BR' }, + { key: 'pt-PT', title: 'pt-PT' }, + { key: 'ru-RU', title: 'ru-RU' }, + { key: 'sv', title: 'sv' }, + { key: 'tr-TR', title: 'tr-TR' }, + { key: 'zh-CN', title: 'zh-CN' }, + { key: 'zh-TW', title: 'zh-TW' }, +]; + +export const LanguageSelectPopup = ({ onClose }: { onClose: () => void }) => { + const items: IPopupItem[] = supportedLanguages.map((item) => ({ + onClick: () => void i18n.changeLanguage(item.key), + title: item.title, + icon: <>, + })); + return ( + + ); +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/Popup.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/Popup.tsx index f55365682b..a781437bda 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/Popup.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/Popup.tsx @@ -11,10 +11,12 @@ export const Popup = ({ items, className = '', onOutsideClick, + columns = 1, }: { items: IPopupItem[]; className: string; onOutsideClick?: () => void; + columns?: 1 | 2 | 3; }) => { const ref = useRef(null); useOutsideClick(ref, () => onOutsideClick && onOutsideClick()); @@ -26,16 +28,22 @@ export const Popup = ({ return (
- {items.map((item, index) => ( - - ))} +
+ {items.map((item, index) => ( + + ))} +
); }; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/getColor.ts b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/getColor.ts new file mode 100644 index 0000000000..285d1fb164 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/getColor.ts @@ -0,0 +1,26 @@ +import { SelectOptionColorPB } from '../../../services/backend'; + +export const getBgColor = (color: SelectOptionColorPB | undefined): string => { + switch (color) { + case SelectOptionColorPB.Purple: + return 'bg-tint-1'; + case SelectOptionColorPB.Pink: + return 'bg-tint-2'; + case SelectOptionColorPB.LightPink: + return 'bg-tint-3'; + case SelectOptionColorPB.Orange: + return 'bg-tint-4'; + case SelectOptionColorPB.Yellow: + return 'bg-tint-5'; + case SelectOptionColorPB.Lime: + return 'bg-tint-6'; + case SelectOptionColorPB.Green: + return 'bg-tint-7'; + case SelectOptionColorPB.Aqua: + return 'bg-tint-8'; + case SelectOptionColorPB.Blue: + return 'bg-tint-9'; + default: + return ''; + } +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/CloseSvg.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/CloseSvg.tsx new file mode 100644 index 0000000000..50e76a68c5 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/CloseSvg.tsx @@ -0,0 +1,16 @@ +export const CloseSvg = () => { + return ( + + + + + ); +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/DropDownShowSvg.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/DropDownShowSvg.tsx new file mode 100644 index 0000000000..b3956a77d1 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/DropDownShowSvg.tsx @@ -0,0 +1,10 @@ +export const DropDownShowSvg = () => { + return ( + + + + ); +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/EarthSvg.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/EarthSvg.tsx new file mode 100644 index 0000000000..f2911a940c --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/EarthSvg.tsx @@ -0,0 +1,21 @@ +export const EarthSvg = () => { + return ( + + + + + + ); +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/EyeClosedSvg.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/EyeClosedSvg.tsx index beac613d3c..baa5684f42 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/EyeClosedSvg.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/EyeClosedSvg.tsx @@ -2,12 +2,12 @@ export const EyeClosed = () => { return ( - + ); }; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/EyeOpenSvg.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/EyeOpenSvg.tsx index e8e50191c3..201e67fec6 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/EyeOpenSvg.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/EyeOpenSvg.tsx @@ -3,14 +3,14 @@ export const EyeOpened = () => { { + return ( + + + + + + + ); +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/InformationSvg.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/InformationSvg.tsx new file mode 100644 index 0000000000..8217fe0f82 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/InformationSvg.tsx @@ -0,0 +1,14 @@ +export const InformationSvg = () => { + return ( + + + + + + ); +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/LogoutSvg.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/LogoutSvg.tsx new file mode 100644 index 0000000000..86e69c08c1 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/LogoutSvg.tsx @@ -0,0 +1,14 @@ +export const LogoutSvg = () => { + return ( + + + + + + ); +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/ShowMenuSvg.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/ShowMenuSvg.tsx new file mode 100644 index 0000000000..736e9a8b50 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/ShowMenuSvg.tsx @@ -0,0 +1,10 @@ +export const ShowMenuSvg = () => { + return ( + + + + + + + ); +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/auth/Login/Login.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/auth/Login/Login.hooks.ts index eb6704dc05..7557c9cf38 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/auth/Login/Login.hooks.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/auth/Login/Login.hooks.ts @@ -2,26 +2,60 @@ import { useState } from 'react'; import { currentUserActions } from '../../../stores/reducers/current-user/slice'; import { useAppDispatch, useAppSelector } from '../../../stores/store'; import { useNavigate } from 'react-router-dom'; +import { useAuth } from '../auth.hooks'; export const useLogin = () => { + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); const [showPassword, setShowPassword] = useState(false); const appDispatch = useAppDispatch(); const currentUser = useAppSelector((state) => state.currentUser); const navigate = useNavigate(); + const { login } = useAuth(); + const [authError, setAuthError] = useState(false); function onTogglePassword() { setShowPassword(!showPassword); } - function onSignInClick() { - appDispatch( - currentUserActions.updateUser({ - ...currentUser, - isAuthenticated: true, - }) - ); - navigate('/'); + // reset error + function _setEmail(v: string) { + setAuthError(false); + setEmail(v); } - return { showPassword, onTogglePassword, onSignInClick }; + function _setPassword(v: string) { + setAuthError(false); + setPassword(v); + } + + async function onSignInClick() { + try { + const result = await login(email, password); + const { id, name, token } = result; + appDispatch( + currentUserActions.updateUser({ + id: id, + displayName: name, + email: email, + token: token, + isAuthenticated: true, + }) + ); + navigate('/'); + } catch (e) { + setAuthError(true); + } + } + + return { + showPassword, + onTogglePassword, + onSignInClick, + email, + setEmail: _setEmail, + password, + setPassword: _setPassword, + authError, + }; }; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/auth/Login/Login.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/auth/Login/Login.tsx index 4000e5c869..baca0a5711 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/auth/Login/Login.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/auth/Login/Login.tsx @@ -4,60 +4,97 @@ import { EyeOpened } from '../../_shared/svg/EyeOpenSvg'; import { useLogin } from './Login.hooks'; import { Link } from 'react-router-dom'; import { Button } from '../../_shared/Button'; +import { useTranslation } from 'react-i18next'; +import { EarthSvg } from '../../_shared/svg/EarthSvg'; +import { useState } from 'react'; +import { LanguageSelectPopup } from '../../_shared/LanguageSelectPopup'; + export const Login = () => { - const { showPassword, onTogglePassword, onSignInClick } = useLogin(); + const { showPassword, onTogglePassword, onSignInClick, email, setEmail, password, setPassword, authError } = + useLogin(); + const { t } = useTranslation(''); + const [showLanguagePopup, setShowLanguagePopup] = useState(false); return ( -
e.preventDefault()} method='POST'> -
-
- -
- -
- Login to Appflowy -
- -
- -
- - - {/* Show password button */} - + <> +
e.preventDefault()} method='POST'> +
+
+
-
- {/* Forget password link */} - - Forgot password? - -
-
- -
- - - {/* signup link */} -
- - Don't have an account? - - Sign up - +
+ + {t('signIn.loginTitle').replace('@:appName', 'AppFlowy')}
+ +
+ setEmail(e.target.value)} + /> +
+ {/* Password input field */} + + setPassword(e.target.value)} + /> + + {/* Show password button */} + +
+ +
+ {/* Forget password link */} + + {t('signIn.forgotPassword')} + +
+
+ +
+ + + {/* signup link */} +
+ + {t('signIn.dontHaveAnAccount')} + + {t('signUp.buttonText')} + + +
+
+ +
+
+ + {showLanguagePopup && ( + setShowLanguagePopup(false)}> + )} +
+
-
-
+ + ); }; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/auth/SignUp/SignUp.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/auth/SignUp/SignUp.hooks.ts index 8a89f469d0..3fcf26378b 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/auth/SignUp/SignUp.hooks.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/auth/SignUp/SignUp.hooks.ts @@ -2,13 +2,40 @@ import { useState } from 'react'; import { useAppDispatch, useAppSelector } from '../../../stores/store'; import { currentUserActions } from '../../../stores/reducers/current-user/slice'; import { useNavigate } from 'react-router-dom'; +import { useAuth } from '../auth.hooks'; export const useSignUp = () => { + const [email, _setEmail] = useState(''); + const [displayName, _setDisplayName] = useState(''); + const [password, _setPassword] = useState(''); + const [repeatedPassword, _setRepeatedPassword] = useState(''); const [showPassword, setShowPassword] = useState(false); const [showConfirmPassword, setShowConfirmPassword] = useState(false); const appDispatch = useAppDispatch(); const currentUser = useAppSelector((state) => state.currentUser); const navigate = useNavigate(); + const { register } = useAuth(); + const [authError, setAuthError] = useState(false); + + const setEmail = (v: string) => { + setAuthError(false); + _setEmail(v); + }; + + const setDisplayName = (v: string) => { + setAuthError(false); + _setDisplayName(v); + }; + + const setPassword = (v: string) => { + setAuthError(false); + _setPassword(v); + }; + + const setRepeatedPassword = (v: string) => { + setAuthError(false); + _setRepeatedPassword(v); + }; function onTogglePassword() { setShowPassword(!showPassword); @@ -18,15 +45,39 @@ export const useSignUp = () => { setShowConfirmPassword(!showConfirmPassword); } - function onSignUpClick() { - appDispatch( - currentUserActions.updateUser({ - ...currentUser, - isAuthenticated: true, - }) - ); - navigate('/'); + async function onSignUpClick() { + try { + const result = await register(email, password, displayName); + const { id, token } = result; + appDispatch( + currentUserActions.updateUser({ + id, + token, + email, + displayName, + isAuthenticated: true, + }) + ); + navigate('/'); + } catch (e) { + setAuthError(true); + } } - return { showPassword, onTogglePassword, showConfirmPassword, onToggleConfirmPassword, onSignUpClick }; + return { + email, + setEmail, + displayName, + setDisplayName, + password, + setPassword, + repeatedPassword, + setRepeatedPassword, + showPassword, + onTogglePassword, + showConfirmPassword, + onToggleConfirmPassword, + onSignUpClick, + authError, + }; }; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/auth/SignUp/SignUp.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/auth/SignUp/SignUp.tsx index 7bdd8f355d..6b6a24ae9f 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/auth/SignUp/SignUp.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/auth/SignUp/SignUp.tsx @@ -5,25 +5,66 @@ import { EyeOpened } from '../../_shared/svg/EyeOpenSvg'; import { useSignUp } from './SignUp.hooks'; import { Link } from 'react-router-dom'; import { Button } from '../../_shared/Button'; +import { EarthSvg } from '../../_shared/svg/EarthSvg'; +import { LanguageSelectPopup } from '../../_shared/LanguageSelectPopup'; +import { useTranslation } from 'react-i18next'; +import { useState } from 'react'; export const SignUp = () => { - const { showPassword, onTogglePassword, showConfirmPassword, onToggleConfirmPassword, onSignUpClick } = useSignUp(); + const { + showPassword, + onTogglePassword, + showConfirmPassword, + onToggleConfirmPassword, + onSignUpClick, + email, + setEmail, + displayName, + setDisplayName, + password, + setPassword, + repeatedPassword, + setRepeatedPassword, + authError, + } = useSignUp(); + const { t } = useTranslation(''); + const [showLanguagePopup, setShowLanguagePopup] = useState(false); return (
e.preventDefault()}> -
+
- Sign up to Appflowy + {t('signUp.title').replace('@:appName', 'AppFlowy')}
- + setEmail(e.target.value)} + /> + {/* new user should enter his name, need translation for this field */} + setDisplayName(e.target.value)} + />
- + setPassword(e.target.value)} + /> {/* signup link */}
- Already have an account? + {t('signUp.alreadyHaveAnAccount')} - Sign in + {t('signIn.buttonText')}
+ +
+
+ + {showLanguagePopup && ( + setShowLanguagePopup(false)}> + )} +
+
); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/auth/auth.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/auth/auth.hooks.ts index 5fe606835c..354bf29a57 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/auth/auth.hooks.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/auth/auth.hooks.ts @@ -1,14 +1,71 @@ import { currentUserActions } from '../../stores/reducers/current-user/slice'; import { useAppDispatch, useAppSelector } from '../../stores/store'; +import { UserProfilePB } from '../../../services/backend/events/flowy-user'; +import { AuthBackendService } from '../../stores/effects/user/user_bd_svc'; +import { FolderEventReadCurrentWorkspace } from '../../../services/backend/events/flowy-folder'; +import { WorkspaceSettingPB } from '../../../services/backend/models/flowy-folder/workspace'; export const useAuth = () => { const dispatch = useAppDispatch(); - const currentUser = useAppSelector((state) => state.currentUser); + const authBackendService = new AuthBackendService(); - function logout() { + async function register(email: string, password: string, name: string): Promise { + const authResult = await authBackendService.signUp({ email, password, name }); + + if (authResult.ok) { + const { id, token } = authResult.val; + // Get the workspace setting after user registered. The workspace setting + // contains the latest visiting view and the current workspace data. + const openWorkspaceResult = await _openWorkspace(); + if (openWorkspaceResult.ok) { + const workspaceSetting: WorkspaceSettingPB = openWorkspaceResult.val; + dispatch( + currentUserActions.updateUser({ + id: id, + token: token, + email, + displayName: name, + isAuthenticated: true, + workspaceSetting: workspaceSetting, + }) + ); + } + return authResult.val; + } else { + console.error(authResult.val.msg); + throw new Error(authResult.val.msg); + } + } + + async function login(email: string, password: string): Promise { + const result = await authBackendService.signIn({ email, password }); + if (result.ok) { + const { id, token, name } = result.val; + dispatch( + currentUserActions.updateUser({ + id: id, + token: token, + email, + displayName: name, + isAuthenticated: true, + }) + ); + return result.val; + } else { + console.error(result.val.msg); + throw new Error(result.val.msg); + } + } + + async function logout() { + await authBackendService.signOut(); dispatch(currentUserActions.logout()); } - return { currentUser, logout }; + async function _openWorkspace() { + return FolderEventReadCurrentWorkspace(); + } + + return { currentUser, register, login, logout }; }; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/board/Board.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/board/Board.hooks.ts new file mode 100644 index 0000000000..38f18c3f60 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/board/Board.hooks.ts @@ -0,0 +1,65 @@ +import { useEffect, useState } from 'react'; +import { useAppDispatch, useAppSelector } from '../../stores/store'; +import { boardActions } from '../../stores/reducers/board/slice'; +import { ICellData, IDatabase, IDatabaseRow, ISelectOption } from '../../stores/reducers/database/slice'; + +export const useBoard = () => { + const dispatch = useAppDispatch(); + const groupingFieldId = useAppSelector((state) => state.board); + const database = useAppSelector((state) => state.database); + const [title, setTitle] = useState(''); + const [boardColumns, setBoardColumns] = + useState<(ISelectOption & { rows: (IDatabaseRow & { isGhost: boolean })[] })[]>(); + const [movingRowId, setMovingRowId] = useState(undefined); + const [ghostLocation, setGhostLocation] = useState<{ column: number; row: number }>({ column: 0, row: 0 }); + + useEffect(() => { + setTitle(database.title); + setBoardColumns( + database.fields[groupingFieldId].fieldOptions.selectOptions?.map((groupFieldItem) => { + const rows = database.rows + .filter((row) => row.cells[groupingFieldId].optionIds?.some((so) => so === groupFieldItem.selectOptionId)) + .map((row) => ({ + ...row, + isGhost: false, + })); + return { + ...groupFieldItem, + rows: rows, + }; + }) || [] + ); + }, [database, groupingFieldId]); + + const changeGroupingField = (fieldId: string) => { + dispatch( + boardActions.setGroupingFieldId({ + fieldId, + }) + ); + }; + + const onGhostItemMove = (columnIndex: number, rowIndex: number) => { + setGhostLocation({ column: columnIndex, row: rowIndex }); + }; + + const startMove = (rowId: string) => { + setMovingRowId(rowId); + }; + + const endMove = () => { + setMovingRowId(undefined); + }; + + return { + title, + boardColumns, + groupingFieldId, + changeGroupingField, + startMove, + endMove, + onGhostItemMove, + movingRowId, + ghostLocation, + }; +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/board/Board.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/board/Board.tsx new file mode 100644 index 0000000000..e4cee56016 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/board/Board.tsx @@ -0,0 +1,59 @@ +import { SettingsSvg } from '../_shared/svg/SettingsSvg'; +import { SearchInput } from '../_shared/SearchInput'; +import { useDatabase } from '../_shared/Database.hooks'; +import { BoardBlock } from './BoardBlock'; +import { NewBoardBlock } from './NewBoardBlock'; +import { IDatabaseRow } from '../../stores/reducers/database/slice'; +import { useBoard } from './Board.hooks'; + +export const Board = () => { + const { database, newField, renameField, newRow } = useDatabase(); + const { + title, + boardColumns, + groupingFieldId, + changeGroupingField, + startMove, + endMove, + onGhostItemMove, + movingRowId, + ghostLocation, + } = useBoard(); + + return ( + <> +
+
+
{title}
+ +
+ +
+ +
+
+
+
+ {database && + boardColumns?.map((column, index) => ( + + ))} + + console.log('new block')}> +
+
+ + ); +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/board/BoardBlock.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/board/BoardBlock.tsx new file mode 100644 index 0000000000..9d5e493d70 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/board/BoardBlock.tsx @@ -0,0 +1,64 @@ +import { Details2Svg } from '../_shared/svg/Details2Svg'; +import AddSvg from '../_shared/svg/AddSvg'; +import { DatabaseFieldMap, ICellData, IDatabaseColumn, IDatabaseRow } from '../../stores/reducers/database/slice'; +import { BoardBlockItem } from './BoardBlockItem'; + +export const BoardBlock = ({ + title, + groupingFieldId, + count, + fields, + columns, + rows, + startMove, + endMove, +}: { + title: string; + groupingFieldId: string; + count: number; + fields: DatabaseFieldMap; + columns: IDatabaseColumn[]; + rows: IDatabaseRow[]; + startMove: (id: string) => void; + endMove: () => void; +}) => { + return ( +
+
+
+ {title} + ({count}) +
+
+ + +
+
+
+ {rows.map((row, index) => ( + startMove(row.rowId)} + endMove={() => endMove()} + > + ))} +
+
+ +
+
+ ); +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/board/BoardBlockItem.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/board/BoardBlockItem.tsx new file mode 100644 index 0000000000..7741bbd05f --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/board/BoardBlockItem.tsx @@ -0,0 +1,125 @@ +import { DatabaseFieldMap, IDatabaseColumn, IDatabaseRow } from '../../stores/reducers/database/slice'; +import { Details2Svg } from '../_shared/svg/Details2Svg'; +import { FieldType } from '../../../services/backend'; +import { getBgColor } from '../_shared/getColor'; +import { MouseEventHandler, useEffect, useRef, useState } from 'react'; + +export const BoardBlockItem = ({ + groupingFieldId, + fields, + columns, + row, + startMove, + endMove, +}: { + groupingFieldId: string; + fields: DatabaseFieldMap; + columns: IDatabaseColumn[]; + row: IDatabaseRow; + startMove: () => void; + endMove: () => void; +}) => { + const [isMoving, setIsMoving] = useState(false); + const [isDown, setIsDown] = useState(false); + const [ghostWidth, setGhostWidth] = useState(0); + const [ghostHeight, setGhostHeight] = useState(0); + const [ghostLeft, setGhostLeft] = useState(0); + const [ghostTop, setGhostTop] = useState(0); + const el = useRef(null); + useEffect(() => { + if (el.current?.getBoundingClientRect && isMoving) { + const { left, top, width, height } = el.current.getBoundingClientRect(); + setGhostWidth(width); + setGhostHeight(height); + setGhostLeft(left); + setGhostTop(top); + + startMove(); + + const gEl = document.getElementById('ghost-block'); + if (gEl?.innerHTML) { + gEl.innerHTML = el.current.innerHTML; + } + } + }, [el, isMoving]); + + const onMouseMove: MouseEventHandler = (e) => { + setGhostLeft(ghostLeft + e.movementX); + setGhostTop(ghostTop + e.movementY); + }; + + const onMouseUp: MouseEventHandler = (e) => { + setIsMoving(false); + endMove(); + }; + + const dragStart = () => { + if (isDown) { + setIsMoving(true); + setIsDown(false); + } + }; + + return ( + <> +
setIsDown(true)} + onMouseMove={dragStart} + onMouseUp={() => setIsDown(false)} + onClick={() => console.log('on click')} + className={`relative cursor-pointer select-none rounded-lg border border-shade-6 bg-white px-3 py-2 transition-transform duration-100 hover:bg-main-selector `} + > + +
+ {columns + .filter((column) => column.fieldId !== groupingFieldId) + .map((column, index) => { + switch (fields[column.fieldId].fieldType) { + case FieldType.MultiSelect: + return ( +
+ {row.cells[column.fieldId].optionIds?.map((option, indexOption) => { + const selectOptions = fields[column.fieldId].fieldOptions.selectOptions; + const selectedOption = selectOptions?.find((so) => so.selectOptionId === option); + return ( +
+ {selectedOption?.title} +
+ ); + })} +
+ ); + default: + return
{row.cells[column.fieldId].data}
; + } + })} +
+
+ {isMoving && ( +
+   +
+ )} + + ); +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/board/NewBoardBlock.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/board/NewBoardBlock.tsx new file mode 100644 index 0000000000..d6ecc12ddd --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/board/NewBoardBlock.tsx @@ -0,0 +1,14 @@ +import AddSvg from '../_shared/svg/AddSvg'; + +export const NewBoardBlock = ({ onClick }: { onClick: () => void }) => { + return ( +
+ +
+ ); +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/error/Error.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/error/Error.hooks.ts new file mode 100644 index 0000000000..e2cad72c5f --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/error/Error.hooks.ts @@ -0,0 +1,30 @@ +import { useAppDispatch, useAppSelector } from '../../stores/store'; +import { errorActions } from '../../stores/reducers/error/slice'; +import { useEffect, useState } from 'react'; + +export const useError = () => { + const dispatch = useAppDispatch(); + const error = useAppSelector((state) => state.error); + const [errorMessage, setErrorMessage] = useState(''); + const [displayError, setDisplayError] = useState(false); + + useEffect(() => { + setDisplayError(error.display); + setErrorMessage(error.message); + }, [error]); + + const showError = (msg: string) => { + dispatch(errorActions.showError(msg)); + }; + + const hideError = () => { + dispatch(errorActions.hideError()); + }; + + return { + showError, + hideError, + errorMessage, + displayError, + }; +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/error/ErrorHandlerPage.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/error/ErrorHandlerPage.tsx new file mode 100644 index 0000000000..e4cc72679e --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/error/ErrorHandlerPage.tsx @@ -0,0 +1,8 @@ +import { useError } from './Error.hooks'; +import { ErrorModal } from './ErrorModal'; + +export const ErrorHandlerPage = () => { + const { hideError, errorMessage, displayError } = useError(); + + return displayError ? : <>; +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/error/ErrorModal.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/error/ErrorModal.tsx new file mode 100644 index 0000000000..1251c4dcfe --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/error/ErrorModal.tsx @@ -0,0 +1,28 @@ +import { InformationSvg } from '../_shared/svg/InformationSvg'; +import { CloseSvg } from '../_shared/svg/CloseSvg'; + +export const ErrorModal = ({ message, onClose }: { message: string; onClose: () => void }) => { + return ( +
+
+ +
+ +
+

Oops.. something went wrong

+

{message}

+
+
+ ); +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/layout/AppLogo.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/layout/AppLogo.tsx index 2ac62d47f5..9ae3069f51 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/layout/AppLogo.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/layout/AppLogo.tsx @@ -1,8 +1,32 @@ -export const AppLogo = () => { +import { HideMenuSvg } from '../_shared/svg/HideMenuSvg'; +import { ShowMenuSvg } from '../_shared/svg/ShowMenuSvg'; + +export const AppLogo = ({ + iconToShow, + onHideMenuClick, + onShowMenuClick, +}: { + iconToShow: 'hide' | 'show'; + onHideMenuClick?: () => void; + onShowMenuClick?: () => void; +}) => { return (
{'logo'} - {'hide'} + {iconToShow === 'hide' && ( + + )} + {iconToShow === 'show' && ( + + )}
); }; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/layout/HeaderPanel/OptionsPopup.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/layout/HeaderPanel/OptionsPopup.tsx new file mode 100644 index 0000000000..fe0cf6de9b --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/layout/HeaderPanel/OptionsPopup.tsx @@ -0,0 +1,23 @@ +import { IPopupItem, Popup } from '../../_shared/Popup'; +import { LogoutSvg } from '../../_shared/svg/LogoutSvg'; + +export const OptionsPopup = ({ onSignOutClick, onClose }: { onSignOutClick: () => void; onClose: () => void }) => { + const items: IPopupItem[] = [ + { + title: 'Sign out', + icon: ( + + + + ), + onClick: onSignOutClick, + }, + ]; + return ( + + ); +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/layout/HeaderPanel/PageOptions.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/layout/HeaderPanel/PageOptions.hooks.ts new file mode 100644 index 0000000000..6f0202c6d3 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/layout/HeaderPanel/PageOptions.hooks.ts @@ -0,0 +1,27 @@ +import { useState } from 'react'; +import { useAuth } from '../../auth/auth.hooks'; + +export const usePageOptions = () => { + const [showOptionsPopup, setShowOptionsPopup] = useState(false); + const { logout } = useAuth(); + + const onOptionsClick = () => { + setShowOptionsPopup(true); + }; + + const onClose = () => { + setShowOptionsPopup(false); + }; + + const onSignOutClick = async () => { + await logout(); + onClose(); + }; + + return { + showOptionsPopup, + onOptionsClick, + onClose, + onSignOutClick, + }; +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/layout/HeaderPanel/PageOptions.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/layout/HeaderPanel/PageOptions.tsx index f4ffc6adf3..8f79998027 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/layout/HeaderPanel/PageOptions.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/layout/HeaderPanel/PageOptions.tsx @@ -1,15 +1,23 @@ import { Button } from '../../_shared/Button'; +import { Details2Svg } from '../../_shared/svg/Details2Svg'; +import { usePageOptions } from './PageOptions.hooks'; +import { OptionsPopup } from './OptionsPopup'; export const PageOptions = () => { - return ( -
- + const { showOptionsPopup, onOptionsClick, onClose, onSignOutClick } = usePageOptions(); - -
+ return ( + <> +
+ + + +
+ {showOptionsPopup && } + ); }; 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 60b3a14cbe..4fea0c973b 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 @@ -1,11 +1,17 @@ import { foldersActions, IFolder } from '../../../stores/reducers/folders/slice'; -import { useState } from 'react'; -import { useAppDispatch } from '../../../stores/store'; -import { nanoid } from 'nanoid'; -import { pagesActions } from '../../../stores/reducers/pages/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 { 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'; -export const useFolderEvents = (folder: IFolder) => { +const initialFolderHeight = 40; +const initialPageHeight = 40; +const animationDuration = 500; + +export const useFolderEvents = (folder: IFolder, pages: IPage[]) => { const appDispatch = useAppDispatch(); const [showPages, setShowPages] = useState(false); @@ -13,7 +19,25 @@ export const useFolderEvents = (folder: IFolder) => { const [showNewPageOptions, setShowNewPageOptions] = useState(false); const [showRenamePopup, setShowRenamePopup] = useState(false); + const [folderHeight, setFolderHeight] = useState(`${initialFolderHeight}px`); + + const workspace = useAppSelector((state) => state.workspace); + + const appBackendService = new AppBackendService(folder.id); + const workspaceBackendService = new WorkspaceBackendService(workspace.id || ''); + const error = useError(); + useEffect(() => { + if (showPages) { + setFolderHeight(`${initialFolderHeight + pages.length * initialPageHeight}px`); + } + }, [pages]); + const onFolderNameClick = () => { + if (showPages) { + setFolderHeight(`${initialFolderHeight}px`); + } else { + setFolderHeight(`${initialFolderHeight + pages.length * initialPageHeight}px`); + } setShowPages(!showPages); }; @@ -30,22 +54,39 @@ export const useFolderEvents = (folder: IFolder) => { setShowRenamePopup(true); }; - const changeFolderTitle = (newTitle: string) => { - appDispatch(foldersActions.renameFolder({ id: folder.id, newTitle })); + const changeFolderTitle = async (newTitle: string) => { + try { + await appBackendService.update({ name: newTitle }); + appDispatch(foldersActions.renameFolder({ id: folder.id, newTitle })); + } catch (e: any) { + error.showError(e?.message); + } }; const closeRenamePopup = () => { setShowRenamePopup(false); }; - const deleteFolder = () => { + const deleteFolder = async () => { closePopup(); - appDispatch(foldersActions.deleteFolder({ id: folder.id })); + try { + await appBackendService.delete(); + appDispatch(foldersActions.deleteFolder({ id: folder.id })); + } catch (e: any) { + error.showError(e?.message); + } }; - const duplicateFolder = () => { + const duplicateFolder = async () => { closePopup(); - appDispatch(foldersActions.addFolder({ id: nanoid(8), title: folder.title })); + try { + const newApp = await workspaceBackendService.createApp({ + name: folder.title, + }); + appDispatch(foldersActions.addFolder({ id: newApp.id, title: folder.title })); + } catch (e: any) { + error.showError(e?.message); + } }; const closePopup = () => { @@ -53,35 +94,67 @@ export const useFolderEvents = (folder: IFolder) => { setShowNewPageOptions(false); }; - const onAddNewDocumentPage = () => { + const onAddNewDocumentPage = async () => { closePopup(); - appDispatch( - pagesActions.addPage({ - folderId: folder.id, - pageType: ViewLayoutTypePB.Document, - title: 'New Page 1', - id: nanoid(6), - }) - ); + try { + const newView = await appBackendService.createView({ + name: 'New Document 1', + layoutType: ViewLayoutTypePB.Document, + }); + + appDispatch( + pagesActions.addPage({ + folderId: folder.id, + pageType: ViewLayoutTypePB.Document, + title: newView.name, + id: newView.id, + }) + ); + } catch (e: any) { + error.showError(e?.message); + } }; - const onAddNewBoardPage = () => { + const onAddNewBoardPage = async () => { closePopup(); - appDispatch( - pagesActions.addPage({ - folderId: folder.id, - pageType: ViewLayoutTypePB.Board, - title: 'New Board 1', - id: nanoid(6), - }) - ); + try { + const newView = await appBackendService.createView({ + name: 'New Board 1', + layoutType: ViewLayoutTypePB.Board, + }); + + appDispatch( + pagesActions.addPage({ + folderId: folder.id, + pageType: ViewLayoutTypePB.Board, + title: newView.name, + id: newView.id, + }) + ); + } catch (e: any) { + error.showError(e?.message); + } }; - const onAddNewGridPage = () => { + const onAddNewGridPage = async () => { closePopup(); - appDispatch( - pagesActions.addPage({ folderId: folder.id, pageType: ViewLayoutTypePB.Grid, title: 'New Grid 1', id: nanoid(6) }) - ); + try { + const newView = await appBackendService.createView({ + name: 'New Grid 1', + layoutType: ViewLayoutTypePB.Grid, + }); + + appDispatch( + pagesActions.addPage({ + folderId: folder.id, + pageType: ViewLayoutTypePB.Grid, + title: newView.name, + id: newView.id, + }) + ); + } catch (e: any) { + error.showError(e?.message); + } }; return { @@ -104,5 +177,7 @@ export const useFolderEvents = (folder: IFolder) => { onAddNewGridPage, closePopup, + folderHeight, + animationDuration, }; }; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/layout/NavigationPanel/FolderItem.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/layout/NavigationPanel/FolderItem.tsx index b1718c2c88..a4db96f52e 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/layout/NavigationPanel/FolderItem.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/layout/NavigationPanel/FolderItem.tsx @@ -8,6 +8,10 @@ import { IPage } from '../../../stores/reducers/pages/slice'; import { PageItem } from './PageItem'; import { Button } from '../../_shared/Button'; import { RenamePopup } from './RenamePopup'; +import { useEffect, useState } from 'react'; +import { DropDownShowSvg } from '../../_shared/svg/DropDownShowSvg'; + +let timeoutHandle: any; export const FolderItem = ({ folder, @@ -38,46 +42,72 @@ export const FolderItem = ({ onAddNewGridPage, closePopup, - } = useFolderEvents(folder); + folderHeight, + animationDuration, + } = useFolderEvents(folder, pages); + + const [hideOverflow, setHideOverflow] = useState(!showPages); + + useEffect(() => { + clearTimeout(timeoutHandle); + if (showPages) { + timeoutHandle = setTimeout(() => { + setHideOverflow(!showPages); + }, animationDuration); + } else { + setHideOverflow(!showPages); + } + }, [showPages]); return ( -
+ /*transitionTimingFunction:'cubic-bezier(.36,1.55,.65,1.1)'*/ +
onFolderNameClick()} - className={'flex cursor-pointer items-center justify-between rounded-lg px-4 py-2 hover:bg-surface-2'} + className={`relative my-2 ${hideOverflow ? 'overflow-hidden' : ''} transition-all `} + style={{ height: folderHeight, transitionDuration: `${animationDuration}ms` }} > -
-
- {''} +
onFolderNameClick()} + className={'flex cursor-pointer items-center justify-between rounded-lg px-4 py-2 hover:bg-surface-2'} + > + +
+ +
- {folder.title}
-
- - - {showFolderOptions && ( - startFolderRename()} - onDeleteClick={() => deleteFolder()} - onDuplicateClick={() => duplicateFolder()} - onClose={() => closePopup()} - > - )} - {showNewPageOptions && ( - onAddNewDocumentPage()} - onBoardClick={() => onAddNewBoardPage()} - onGridClick={() => onAddNewGridPage()} - onClose={() => closePopup()} - > - )} -
+ {pages.map((page, index) => ( + onPageClick(page)}> + ))}
+ {showFolderOptions && ( + startFolderRename()} + onDeleteClick={() => deleteFolder()} + onDuplicateClick={() => duplicateFolder()} + onClose={() => closePopup()} + > + )} + {showNewPageOptions && ( + onAddNewDocumentPage()} + onBoardClick={() => onAddNewBoardPage()} + onGridClick={() => onAddNewGridPage()} + onClose={() => closePopup()} + > + )} {showRenamePopup && ( )} - {showPages && - pages.map((page, index) => onPageClick(page)}>)}
); }; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/layout/NavigationPanel/NavItemOptionsPopup.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/layout/NavigationPanel/NavItemOptionsPopup.tsx index e583fadb2a..8e195b052a 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/layout/NavigationPanel/NavItemOptionsPopup.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/layout/NavigationPanel/NavItemOptionsPopup.tsx @@ -48,7 +48,7 @@ export const NavItemOptionsPopup = ({ onClose && onClose()} items={items} - className={'absolute right-0 top-full z-10'} + className={'absolute right-0 top-[40px] z-10'} > ); }; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/layout/NavigationPanel/NavigationFloatingPanel.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/layout/NavigationPanel/NavigationFloatingPanel.tsx new file mode 100644 index 0000000000..7ab84ab905 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/layout/NavigationPanel/NavigationFloatingPanel.tsx @@ -0,0 +1,79 @@ +import { AppLogo } from '../AppLogo'; +import { Workspace } from '../Workspace'; +import { FolderItem } from './FolderItem'; +import { PluginsButton } from './PluginsButton'; +import { TrashButton } from './TrashButton'; +import { NewFolderButton } from './NewFolderButton'; +import { IFolder } from '../../../stores/reducers/folders/slice'; +import { IPage } from '../../../stores/reducers/pages/slice'; +import { useEffect, useRef, useState } from 'react'; + +const animationDuration = 500; + +export const NavigationFloatingPanel = ({ + onFixNavigationClick, + slideInFloatingPanel, + folders, + pages, + onPageClick, + setWidth, +}: { + onFixNavigationClick: () => void; + slideInFloatingPanel: boolean; + folders: IFolder[]; + pages: IPage[]; + onPageClick: (page: IPage) => void; + setWidth: (v: number) => void; +}) => { + const el = useRef(null); + const [panelLeft, setPanelLeft] = useState(0); + + useEffect(() => { + if (!el?.current) return; + + const { width } = el.current.getBoundingClientRect(); + setWidth(width); + + if (slideInFloatingPanel) { + setPanelLeft(0); + } else { + setPanelLeft(-width); + } + }, [el.current, slideInFloatingPanel]); + + return ( +
+
+ + + + +
+ {folders.map((folder, index) => ( + page.folderId === folder.id)} + onPageClick={onPageClick} + > + ))} +
+
+ +
+
+ + +
+ + +
+
+ ); +}; 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 402f0f3c8c..26c8d40011 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 @@ -1,13 +1,59 @@ import { useAppSelector } from '../../../stores/store'; import { useNavigate } from 'react-router-dom'; +import { IPage } from '../../../stores/reducers/pages/slice'; +import { ViewLayoutTypePB } from '../../../../services/backend'; +import { MouseEventHandler, useState } from 'react'; + +// number of pixels from left side of screen to show hidden navigation panel +const FLOATING_PANEL_SHOW_WIDTH = 10; +const FLOATING_PANEL_HIDE_EXTRA_WIDTH = 10; export const useNavigationPanelHooks = function () { const folders = useAppSelector((state) => state.folders); const pages = useAppSelector((state) => state.pages); const width = useAppSelector((state) => state.navigationWidth); + const [navigationPanelFixed, setNavigationPanelFixed] = useState(true); + const [slideInFloatingPanel, setSlideInFloatingPanel] = useState(true); const navigate = useNavigate(); + const onCollapseNavigationClick = () => { + setSlideInFloatingPanel(true); + setNavigationPanelFixed(false); + }; + + const onFixNavigationClick = () => { + setNavigationPanelFixed(true); + }; + + const [floatingPanelWidth, setFloatingPanelWidth] = useState(0); + + const onPageClick = (page: IPage) => { + let pageTypeRoute = (() => { + switch (page.pageType) { + case ViewLayoutTypePB.Document: + return 'document'; + break; + case ViewLayoutTypePB.Grid: + return 'grid'; + case ViewLayoutTypePB.Board: + return 'board'; + default: + return 'document'; + } + })(); + + navigate(`/page/${pageTypeRoute}/${page.id}`); + }; + + const onScreenMouseMove: MouseEventHandler = (e) => { + if (e.screenX <= FLOATING_PANEL_SHOW_WIDTH) { + setSlideInFloatingPanel(true); + } else if (e.screenX > floatingPanelWidth + FLOATING_PANEL_HIDE_EXTRA_WIDTH) { + setSlideInFloatingPanel(false); + } + }; + return { width, @@ -15,5 +61,13 @@ export const useNavigationPanelHooks = function () { pages, navigate, + onPageClick, + + onCollapseNavigationClick, + onFixNavigationClick, + navigationPanelFixed, + onScreenMouseMove, + slideInFloatingPanel, + setFloatingPanelWidth, }; }; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/layout/NavigationPanel/NavigationPanel.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/layout/NavigationPanel/NavigationPanel.tsx index 238e0d7bbc..119baf795f 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/layout/NavigationPanel/NavigationPanel.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/layout/NavigationPanel/NavigationPanel.tsx @@ -1,4 +1,3 @@ -import { useNavigationPanelHooks } from './NavigationPanel.hooks'; import { Workspace } from '../Workspace'; import { AppLogo } from '../AppLogo'; import { FolderItem } from './FolderItem'; @@ -6,22 +5,27 @@ import { PluginsButton } from './PluginsButton'; import { TrashButton } from './TrashButton'; import { NewFolderButton } from './NewFolderButton'; import { NavigationResizer } from './NavigationResizer'; +import { IFolder } from '../../../stores/reducers/folders/slice'; +import { IPage } from '../../../stores/reducers/pages/slice'; -export const NavigationPanel = () => { - const { - width, - - folders, - pages, - - navigate, - } = useNavigationPanelHooks(); - +export const NavigationPanel = ({ + onCollapseNavigationClick, + width, + folders, + pages, + onPageClick, +}: { + onCollapseNavigationClick: () => void; + width: number; + folders: IFolder[]; + pages: IPage[]; + onPageClick: (page: IPage) => void; +}) => { return ( <>
- + @@ -31,7 +35,7 @@ export const NavigationPanel = () => { key={index} folder={folder} pages={pages.filter((page) => page.folderId === folder.id)} - onPageClick={(page) => navigate(`/page/${page.pageType}/${page.id}`)} + onPageClick={onPageClick} > ))}
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/layout/NavigationPanel/NewFolderButton.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/layout/NavigationPanel/NewFolderButton.hooks.ts index b510412b68..ffef7e5073 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/layout/NavigationPanel/NewFolderButton.hooks.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/layout/NavigationPanel/NewFolderButton.hooks.ts @@ -1,12 +1,23 @@ -import { useAppDispatch } from '../../../stores/store'; +import { useAppDispatch, useAppSelector } from '../../../stores/store'; import { foldersActions } from '../../../stores/reducers/folders/slice'; -import { nanoid } from 'nanoid'; +import { WorkspaceBackendService } from '../../../stores/effects/folder/workspace/workspace_bd_svc'; +import { useError } from '../../error/Error.hooks'; export const useNewFolder = () => { const appDispatch = useAppDispatch(); + const workspace = useAppSelector((state) => state.workspace); + const workspaceBackendService = new WorkspaceBackendService(workspace.id || ''); + const error = useError(); - const onNewFolder = () => { - appDispatch(foldersActions.addFolder({ id: nanoid(8), title: 'New Folder 1' })); + const onNewFolder = async () => { + try { + const newApp = await workspaceBackendService.createApp({ + name: 'New Folder 1', + }); + appDispatch(foldersActions.addFolder({ id: newApp.id, title: newApp.name })); + } catch (e: any) { + error.showError(e?.message); + } }; return { diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/layout/NavigationPanel/NewPagePopup.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/layout/NavigationPanel/NewPagePopup.tsx index f1ad339d2d..8547ced6f4 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/layout/NavigationPanel/NewPagePopup.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/layout/NavigationPanel/NewPagePopup.tsx @@ -48,7 +48,7 @@ export const NewPagePopup = ({ onClose && onClose()} items={items} - className={'absolute right-0 top-full z-10'} + className={'absolute right-0 top-[40px] z-10'} > ); }; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/layout/NavigationPanel/PageItem.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/layout/NavigationPanel/PageItem.hooks.ts index 63ede28832..77e53086be 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/layout/NavigationPanel/PageItem.hooks.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/layout/NavigationPanel/PageItem.hooks.ts @@ -2,11 +2,15 @@ import { IPage, pagesActions } from '../../../stores/reducers/pages/slice'; import { useAppDispatch } from '../../../stores/store'; import { useState } from 'react'; import { nanoid } from 'nanoid'; +import { ViewBackendService } from '../../../stores/effects/folder/view/view_bd_svc'; +import { useError } from '../../error/Error.hooks'; export const usePageEvents = (page: IPage) => { const appDispatch = useAppDispatch(); const [showPageOptions, setShowPageOptions] = useState(false); const [showRenamePopup, setShowRenamePopup] = useState(false); + const viewBackendService: ViewBackendService = new ViewBackendService(page.id); + const error = useError(); const onPageOptionsClick = () => { setShowPageOptions(!showPageOptions); @@ -17,20 +21,34 @@ export const usePageEvents = (page: IPage) => { closePopup(); }; - const changePageTitle = (newTitle: string) => { - appDispatch(pagesActions.renamePage({ id: page.id, newTitle })); + const changePageTitle = async (newTitle: string) => { + try { + await viewBackendService.update({ name: newTitle }); + appDispatch(pagesActions.renamePage({ id: page.id, newTitle })); + } catch (e: any) { + error.showError(e?.message); + } }; - const deletePage = () => { + const deletePage = async () => { closePopup(); - appDispatch(pagesActions.deletePage({ id: page.id })); + try { + await viewBackendService.delete(); + appDispatch(pagesActions.deletePage({ id: page.id })); + } catch (e: any) { + error.showError(e?.message); + } }; const duplicatePage = () => { closePopup(); - appDispatch( - pagesActions.addPage({ id: nanoid(8), pageType: page.pageType, title: page.title, folderId: page.folderId }) - ); + try { + appDispatch( + pagesActions.addPage({ id: nanoid(8), pageType: page.pageType, title: page.title, folderId: page.folderId }) + ); + } catch (e: any) { + error.showError(e?.message); + } }; const closePopup = () => { diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/layout/NavigationPanel/PageItem.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/layout/NavigationPanel/PageItem.tsx index 3873093e0c..7b1186f95d 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/layout/NavigationPanel/PageItem.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/layout/NavigationPanel/PageItem.tsx @@ -7,7 +7,7 @@ import { IPage } from '../../../stores/reducers/pages/slice'; import { Button } from '../../_shared/Button'; import { usePageEvents } from './PageItem.hooks'; import { RenamePopup } from './RenamePopup'; -import { ViewLayoutTypePB } from '../../../../services/backend/models/flowy-folder/view'; +import { ViewLayoutTypePB } from '../../../../services/backend'; export const PageItem = ({ page, onPageClick }: { page: IPage; onPageClick: () => void }) => { const { @@ -28,28 +28,30 @@ export const PageItem = ({ page, onPageClick }: { page: IPage; onPageClick: () = onClick={() => onPageClick()} className={'flex cursor-pointer items-center justify-between rounded-lg py-2 pl-8 pr-4 hover:bg-surface-2 '} > -
-
+
- {page.title} -
+ + + {page.title} + +
- {showPageOptions && ( - startPageRename()} - onDeleteClick={() => deletePage()} - onDuplicateClick={() => duplicatePage()} - onClose={() => closePopup()} - > - )}
+ {showPageOptions && ( + startPageRename()} + onDeleteClick={() => deletePage()} + onDuplicateClick={() => duplicatePage()} + onClose={() => closePopup()} + > + )} {showRenamePopup && ( { + const currentUser = useAppSelector((state) => state.currentUser); + const { loadWorkspaceItems } = useWorkspace(); + useEffect(() => { + void (async () => { + await loadWorkspaceItems(); + })(); + }, [currentUser.isAuthenticated]); + + const { + width, + folders, + pages, + onPageClick, + onCollapseNavigationClick, + onFixNavigationClick, + navigationPanelFixed, + onScreenMouseMove, + slideInFloatingPanel, + setFloatingPanelWidth, + } = useNavigationPanelHooks(); + return ( -
- +
+ {navigationPanelFixed ? ( + + ) : ( + + )} + {children}
); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/layout/Workspace.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/layout/Workspace.hooks.ts new file mode 100644 index 0000000000..5c1efcbc53 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/layout/Workspace.hooks.ts @@ -0,0 +1,49 @@ +import { foldersActions } from '../../stores/reducers/folders/slice'; +import { useAppDispatch, useAppSelector } from '../../stores/store'; +import { pagesActions } from '../../stores/reducers/pages/slice'; +import { workspaceActions } from '../../stores/reducers/workspace/slice'; +import { UserBackendService } from '../../stores/effects/user/user_bd_svc'; +import { useError } from '../error/Error.hooks'; + +export const useWorkspace = () => { + const appDispatch = useAppDispatch(); + const currentUser = useAppSelector((state) => state.currentUser); + const error = useError(); + + const userBackendService: UserBackendService = new UserBackendService(currentUser.id || ''); + + const loadWorkspaceItems = async () => { + try { + const workspaceSettingPB = await userBackendService.getCurrentWorkspace(); + const workspace = workspaceSettingPB.workspace; + appDispatch(workspaceActions.updateWorkspace({ id: workspace.id, name: workspace.name })); + appDispatch(foldersActions.clearFolders()); + appDispatch(pagesActions.clearPages()); + + const apps = workspace.apps.items; + for (const app of apps) { + appDispatch(foldersActions.addFolder({ id: app.id, title: app.name })); + + const views = app.belongings.items; + for (const view of views) { + appDispatch(pagesActions.addPage({ folderId: app.id, id: view.id, pageType: view.layout, title: view.name })); + } + } + } catch (e1) { + // create workspace for first start + try { + const workspace = await userBackendService.createWorkspace({ name: 'New Workspace', desc: '' }); + appDispatch(workspaceActions.updateWorkspace({ id: workspace.id, name: workspace.name })); + + appDispatch(foldersActions.clearFolders()); + appDispatch(pagesActions.clearPages()); + } catch (e2: any) { + error.showError(e2?.message); + } + } + }; + + return { + loadWorkspaceItems, + }; +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/user/application/notifications/parser.ts b/frontend/appflowy_tauri/src/appflowy_app/components/user/application/notifications/parser.ts index f8308517d6..21e5bde9ff 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/user/application/notifications/parser.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/user/application/notifications/parser.ts @@ -1,7 +1,8 @@ -import { UserNotification } from '../../../../../services/backend'; -import { NotificationParser, OnNotificationError } from '../../../../../services/backend/notifications/parser'; +import { FlowyError, UserNotification } from '../../../../../services/backend'; +import { NotificationParser, OnNotificationError } from '../../../../../services/backend/notifications'; +import { Result } from 'ts-results'; -declare type UserNotificationCallback = (ty: UserNotification, payload: Uint8Array) => void; +declare type UserNotificationCallback = (ty: UserNotification, payload: Result) => void; export class UserNotificationParser extends NotificationParser { constructor(params: { id?: string; callback: UserNotificationCallback; onError?: OnNotificationError }) { @@ -15,8 +16,7 @@ export class UserNotificationParser extends NotificationParser return UserNotification.Unknown; } }, - params.id, - params.onError + params.id ); } } diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/user/application/notifications/user_listener.ts b/frontend/appflowy_tauri/src/appflowy_app/components/user/application/notifications/user_listener.ts index e5ca53dea1..b22a285ce2 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/user/application/notifications/user_listener.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/user/application/notifications/user_listener.ts @@ -1,9 +1,10 @@ -import { UserNotification, UserProfilePB } from '../../../../../services/backend'; +import { FlowyError, UserNotification, UserProfilePB } from '../../../../../services/backend'; import { AFNotificationObserver, OnNotificationError } from '../../../../../services/backend/notifications'; import { UserNotificationParser } from './parser'; +import { Ok, Result } from 'ts-results'; -declare type OnUserProfileUpdate = (userProfile: UserProfilePB) => void; -declare type OnUserSignIn = (userProfile: UserProfilePB) => void; +declare type OnUserProfileUpdate = (result: Result) => void; +declare type OnUserSignIn = (result: Result) => void; export class UserNotificationListener extends AFNotificationObserver { onProfileUpdate?: OnUserProfileUpdate; @@ -16,13 +17,21 @@ export class UserNotificationListener extends AFNotificationObserver { + callback: (notification, result) => { switch (notification) { case UserNotification.DidUpdateUserProfile: - this.onProfileUpdate?.(UserProfilePB.deserializeBinary(payload)); + if (result.ok) { + this.onProfileUpdate?.(Ok(UserProfilePB.deserializeBinary(result.val))); + } else { + this.onProfileUpdate?.(result); + } break; case UserNotification.DidUserSignIn: - this.onUserSignIn?.(UserProfilePB.deserializeBinary(payload)); + if (result.ok) { + this.onUserSignIn?.(Ok(UserProfilePB.deserializeBinary(result.val))); + } else { + this.onUserSignIn?.(result); + } break; default: break; diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/cell/backend_service.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/cell/cell_bd_svc.ts similarity index 100% rename from frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/cell/backend_service.ts rename to frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/cell/cell_bd_svc.ts diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/cell/cache.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/cell/cell_cache.ts similarity index 85% rename from frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/cell/cache.ts rename to frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/cell/cell_cache.ts index 0e5f6692b4..1b6c3349d7 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/cell/cache.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/cell/cell_cache.ts @@ -1,4 +1,6 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ +import { None, Option, Some } from 'ts-results'; + export class CellCacheKey { constructor(public readonly fieldId: string, public readonly rowId: string) {} } @@ -27,19 +29,19 @@ export class CellCache { inner.set(key.rowId, value); }; - get(key: CellCacheKey): T | null { + get(key: CellCacheKey): Option { const inner = this._cellDataByFieldId.get(key.fieldId); if (inner === undefined) { - return null; + return None; } else { const value = inner.get(key.rowId); if (typeof value === typeof undefined || typeof value === typeof null) { - return null; + return None; } if (value satisfies T) { - return value as T; + return Some(value as T); } - return null; + return None; } } } diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/cell/controller.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/cell/cell_controller.ts similarity index 72% rename from frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/cell/controller.ts rename to frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/cell/cell_controller.ts index 1f55c9333c..4d4c879194 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/cell/controller.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/cell/cell_controller.ts @@ -1,23 +1,23 @@ -import { CellIdentifier } from './backend_service'; -import { CellCache, CellCacheKey } from './cache'; -import { FieldController } from '../field/controller'; +import { CellIdentifier } from './cell_bd_svc'; +import { CellCache, CellCacheKey } from './cell_cache'; +import { FieldController } from '../field/field_controller'; import { CellDataLoader } from './data_parser'; import { CellDataPersistence } from './data_persistence'; -import { FieldBackendService, TypeOptionParser } from '../field/backend_service'; +import { FieldBackendService, TypeOptionParser } from '../field/field_bd_svc'; import { ChangeNotifier } from '../../../../utils/change_notifier'; import { CellObserver } from './cell_observer'; import { Log } from '../../../../utils/log'; -import { Err, Ok } from 'ts-results'; +import { Err, None, Ok, Option, Some } from 'ts-results'; export abstract class CellFieldNotifier { abstract subscribeOnFieldChanged(callback: () => void): void; } export class CellController { - _fieldBackendService: FieldBackendService; - _cellDataNotifier: CellDataNotifier; - _cellObserver: CellObserver; - _cacheKey: CellCacheKey; + private _fieldBackendService: FieldBackendService; + private _cellDataNotifier: CellDataNotifier>; + private _cellObserver: CellObserver; + private _cacheKey: CellCacheKey; constructor( public readonly cellIdentifier: CellIdentifier, @@ -27,15 +27,9 @@ export class CellController { private readonly cellDataPersistence: CellDataPersistence ) { this._fieldBackendService = new FieldBackendService(cellIdentifier.viewId, cellIdentifier.fieldId); - this._cacheKey = new CellCacheKey(cellIdentifier.rowId, cellIdentifier.fieldId); - - this._cellDataNotifier = new CellDataNotifier(cellCache.get(this._cacheKey)); - + this._cellDataNotifier = new CellDataNotifier(cellCache.get(this._cacheKey)); this._cellObserver = new CellObserver(cellIdentifier.rowId, cellIdentifier.fieldId); - } - - subscribeChanged = (callbacks: { onCellChanged: (value: T | null) => void; onFieldChanged?: () => void }) => { this._cellObserver.subscribe({ /// 1.Listen on user edit event and load the new cell data if needed. /// For example: @@ -46,10 +40,11 @@ export class CellController { await this._loadCellData(); }, }); + } + subscribeChanged = (callbacks: { onCellChanged: (value: Option) => void; onFieldChanged?: () => void }) => { /// 2.Listen on the field event and load the cell data if needed. this.fieldNotifier.subscribeOnFieldChanged(async () => { - // callbacks.onFieldChanged?.(); /// reloadOnFieldChanged should be true if you need to load the data when the corresponding field is changed @@ -61,7 +56,9 @@ export class CellController { }); this._cellDataNotifier.observer.subscribe((cellData) => { - callbacks.onCellChanged(cellData); + if (cellData !== null) { + callbacks.onCellChanged(cellData); + } }); }; @@ -81,14 +78,26 @@ export class CellController { } }; - _loadCellData = () => { + /// Return the cell data if it exists in the cache + /// If the cell data is not exist, it will load the cell + /// data from the backend and then the [onCellChanged] will + /// get called + getCellData = (): Option => { + const cellData = this.cellCache.get(this._cacheKey); + if (cellData.none) { + void this._loadCellData(); + } + return cellData; + }; + + private _loadCellData = () => { return this.cellDataLoader.loadData().then((result) => { if (result.ok && result.val !== undefined) { this.cellCache.insert(this._cacheKey, result.val); - this._cellDataNotifier.cellData = result.val; + this._cellDataNotifier.cellData = Some(result.val); } else { this.cellCache.remove(this._cacheKey); - this._cellDataNotifier.cellData = null; + this._cellDataNotifier.cellData = None; } }); }; @@ -98,6 +107,7 @@ export class CellFieldNotifierImpl extends CellFieldNotifier { constructor(private readonly fieldController: FieldController) { super(); } + subscribeOnFieldChanged(callback: () => void): void { this.fieldController.subscribeOnFieldsChanged(callback); } @@ -105,6 +115,7 @@ export class CellFieldNotifierImpl extends CellFieldNotifier { class CellDataNotifier extends ChangeNotifier { _cellData: T | null; + constructor(cellData: T) { super(); this._cellData = cellData; diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/cell/cell_observer.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/cell/cell_observer.ts index a021a6137d..27695a0c3c 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/cell/cell_observer.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/cell/cell_observer.ts @@ -1,34 +1,37 @@ -import { Err, Ok, Result } from 'ts-results'; +import { Ok, Result } from 'ts-results'; import { ChangeNotifier } from '../../../../utils/change_notifier'; import { DatabaseNotificationObserver } from '../notifications/observer'; -import { FlowyError } from '../../../../../services/backend/models/flowy-error'; -import { DatabaseNotification } from '../../../../../services/backend'; +import { DatabaseNotification, FlowyError } from '../../../../../services/backend'; type UpdateCellNotifiedValue = Result; -export type CellListenerCallback = (value: UpdateCellNotifiedValue) => void; +export type CellChangedCallback = (value: UpdateCellNotifiedValue) => void; export class CellObserver { - _notifier?: ChangeNotifier; - _listener?: DatabaseNotificationObserver; + private _notifier?: ChangeNotifier; + private _listener?: DatabaseNotificationObserver; + constructor(public readonly rowId: string, public readonly fieldId: string) {} - subscribe = (callbacks: { onCellChanged: CellListenerCallback }) => { + subscribe = (callbacks: { onCellChanged: CellChangedCallback }) => { this._notifier = new ChangeNotifier(); this._notifier?.observer.subscribe(callbacks.onCellChanged); this._listener = new DatabaseNotificationObserver({ viewId: this.rowId + ':' + this.fieldId, - parserHandler: (notification) => { + parserHandler: (notification, result) => { switch (notification) { case DatabaseNotification.DidUpdateCell: - this._notifier?.notify(Ok.EMPTY); + if (result.ok) { + this._notifier?.notify(Ok.EMPTY); + } else { + this._notifier?.notify(result); + } return; default: break; } }, - onError: (error) => this._notifier?.notify(Err(error)), }); return undefined; }; diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/cell/controller_builder.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/cell/controller_builder.ts index 3fe8036fcc..584e77394f 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/cell/controller_builder.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/cell/controller_builder.ts @@ -4,8 +4,8 @@ import { SelectOptionCellDataPB, URLCellDataPB, } from '../../../../../services/backend/models/flowy-database'; -import { CellIdentifier } from './backend_service'; -import { CellController, CellFieldNotifierImpl } from './controller'; +import { CellIdentifier } from './cell_bd_svc'; +import { CellController, CellFieldNotifierImpl } from './cell_controller'; import { CellDataLoader, DateCellDataParser, @@ -13,9 +13,10 @@ import { StringCellDataParser, URLCellDataParser, } from './data_parser'; -import { CellCache } from './cache'; -import { FieldController } from '../field/controller'; +import { CellCache } from './cell_cache'; +import { FieldController } from '../field/field_controller'; import { DateCellDataPersistence, TextCellDataPersistence } from './data_persistence'; + export type TextCellController = CellController; export type CheckboxCellController = CellController; @@ -24,9 +25,8 @@ export type NumberCellController = CellController; export type SelectOptionCellController = CellController; -export type ChecklistCellController = CellController; - export type DateCellController = CellController; + export class CalendarData { constructor(public readonly date: Date, public readonly time?: string) {} } @@ -35,6 +35,7 @@ export type URLCellController = CellController; export class CellControllerBuilder { _fieldNotifier: CellFieldNotifierImpl; + constructor( public readonly cellIdentifier: CellIdentifier, public readonly cellCache: CellCache, @@ -42,6 +43,8 @@ export class CellControllerBuilder { ) { this._fieldNotifier = new CellFieldNotifierImpl(this.fieldController); } + + /// build = () => { switch (this.cellIdentifier.fieldType) { case FieldType.Checkbox: diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/cell/data_parser.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/cell/data_parser.ts index 8ee30a8bd5..836cfcadc8 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/cell/data_parser.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/cell/data_parser.ts @@ -1,5 +1,5 @@ import utf8 from 'utf8'; -import { CellBackendService, CellIdentifier } from './backend_service'; +import { CellBackendService, CellIdentifier } from './cell_bd_svc'; import { DateCellDataPB } from '../../../../../services/backend/models/flowy-database/date_type_option_entities'; import { SelectOptionCellDataPB } from '../../../../../services/backend/models/flowy-database/select_type_option'; import { URLCellDataPB } from '../../../../../services/backend/models/flowy-database/url_type_option_entities'; @@ -30,9 +30,11 @@ class CellDataLoader { }; } +const utf8Decoder = new TextDecoder('utf-8'); + class StringCellDataParser extends CellDataParser { parserData(data: Uint8Array): string { - return utf8.decode(data.toString()); + return utf8Decoder.decode(data); } } diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/cell/data_persistence.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/cell/data_persistence.ts index c023d5e864..12845aaa64 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/cell/data_persistence.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/cell/data_persistence.ts @@ -1,6 +1,6 @@ import { Result } from 'ts-results'; import { FlowyError } from '../../../../../services/backend/models/flowy-error'; -import { CellBackendService, CellIdentifier } from './backend_service'; +import { CellBackendService, CellIdentifier } from './cell_bd_svc'; import { CalendarData } from './controller_builder'; import { DateChangesetPB } from '../../../../../services/backend/models/flowy-database/date_type_option_entities'; import { CellIdPB } from '../../../../../services/backend/models/flowy-database/cell_entities'; @@ -24,6 +24,7 @@ export class DateCellDataPersistence extends CellDataPersistence { constructor(public readonly cellIdentifier: CellIdentifier) { super(); } + save(data: CalendarData): Promise> { const payload = DateChangesetPB.fromObject({ cell_path: _makeCellPath(this.cellIdentifier) }); diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/cell/select_option_bd_svc.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/cell/select_option_bd_svc.ts new file mode 100644 index 0000000000..a70d5e1e7f --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/cell/select_option_bd_svc.ts @@ -0,0 +1,79 @@ +import { CellIdentifier } from './cell_bd_svc'; +import { + CellIdPB, + CreateSelectOptionPayloadPB, + SelectOptionCellChangesetPB, + SelectOptionChangesetPB, + SelectOptionPB, +} from '../../../../../services/backend'; +import { + DatabaseEventCreateSelectOption, + DatabaseEventGetSelectOptionCellData, + DatabaseEventUpdateSelectOption, + DatabaseEventUpdateSelectOptionCell, +} from '../../../../../services/backend/events/flowy-database'; + +export class SelectOptionBackendService { + constructor(public readonly cellIdentifier: CellIdentifier) {} + + createOption = async (params: { name: string; isSelect?: boolean }) => { + const payload = CreateSelectOptionPayloadPB.fromObject({ + option_name: params.name, + view_id: this.cellIdentifier.viewId, + field_id: this.cellIdentifier.fieldId, + }); + + const result = await DatabaseEventCreateSelectOption(payload); + if (result.ok) { + return this._insertOption(result.val, params.isSelect || true); + } else { + return result; + } + }; + + updateOption = (option: SelectOptionPB) => { + const payload = SelectOptionChangesetPB.fromObject({ cell_identifier: this._cellIdentifier() }); + payload.update_options.push(option); + return DatabaseEventUpdateSelectOption(payload); + }; + + deleteOption = (options: SelectOptionPB[]) => { + const payload = SelectOptionChangesetPB.fromObject({ cell_identifier: this._cellIdentifier() }); + payload.delete_options.push(...options); + return DatabaseEventUpdateSelectOption(payload); + }; + + getOptionCellData = () => { + return DatabaseEventGetSelectOptionCellData(this._cellIdentifier()); + }; + + selectOption = (optionIds: string[]) => { + const payload = SelectOptionCellChangesetPB.fromObject({ cell_identifier: this._cellIdentifier() }); + payload.insert_option_ids.push(...optionIds); + return DatabaseEventUpdateSelectOptionCell(payload); + }; + + unselectOption = (optionIds: string[]) => { + const payload = SelectOptionCellChangesetPB.fromObject({ cell_identifier: this._cellIdentifier() }); + payload.delete_option_ids.push(...optionIds); + return DatabaseEventUpdateSelectOptionCell(payload); + }; + + private _insertOption = (option: SelectOptionPB, isSelect: boolean) => { + const payload = SelectOptionChangesetPB.fromObject({ cell_identifier: this._cellIdentifier() }); + if (isSelect) { + payload.insert_options.push(option); + } else { + payload.update_options.push(option); + } + return DatabaseEventUpdateSelectOption(payload); + }; + + private _cellIdentifier = () => { + return CellIdPB.fromObject({ + view_id: this.cellIdentifier.viewId, + field_id: this.cellIdentifier.fieldId, + row_id: this.cellIdentifier.rowId, + }); + }; +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/controller.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/controller.ts deleted file mode 100644 index df3469f09c..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/controller.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { DatabaseBackendService } from './backend_service'; -import { FieldController, FieldInfo } from './field/controller'; -import { DatabaseViewCache } from './view/cache'; -import { DatabasePB } from '../../../../services/backend/models/flowy-database/grid_entities'; -import { RowChangedReason, RowInfo } from './row/cache'; -import { Err } from 'ts-results'; - -export type SubscribeCallback = { - onViewChanged: (data: DatabasePB) => void; - onRowsChanged: (rowInfos: RowInfo[], reason: RowChangedReason) => void; - onFieldsChanged: (fieldInfos: FieldInfo[]) => void; -}; - -export class DatabaseController { - _backendService: DatabaseBackendService; - _fieldController: FieldController; - _databaseViewCache: DatabaseViewCache; - _callback?: SubscribeCallback; - - constructor(public readonly viewId: string) { - this._backendService = new DatabaseBackendService(viewId); - this._fieldController = new FieldController(viewId); - this._databaseViewCache = new DatabaseViewCache(viewId, this._fieldController); - } - - subscribe = (callbacks: SubscribeCallback) => { - this._callback = callbacks; - this._fieldController.subscribeOnFieldsChanged(callbacks.onFieldsChanged); - }; - - open = async () => { - const result = await this._backendService.openDatabase(); - if (result.ok) { - const database: DatabasePB = result.val; - this._callback?.onViewChanged(database); - this._databaseViewCache.initializeWithRows(database.rows); - return await this._fieldController.loadFields(database.fields); - } else { - return Err(result.val); - } - }; - - createRow = async () => { - return this._backendService.createRow(); - }; - - dispose = async () => { - await this._backendService.closeDatabase(); - await this._fieldController.dispose(); - }; -} diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/backend_service.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/database_bd_svc.ts similarity index 75% rename from frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/backend_service.ts rename to frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/database_bd_svc.ts index 25f9fcb29a..c4d1495d11 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/backend_service.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/database_bd_svc.ts @@ -2,15 +2,15 @@ import { DatabaseEventCreateRow, DatabaseEventGetDatabase, DatabaseEventGetFields, -} from '../../../../services/backend/events/flowy-database/event'; -import { DatabaseViewIdPB } from '../../../../services/backend/models/flowy-database'; -import { CreateRowPayloadPB } from '../../../../services/backend/models/flowy-database/row_entities'; +} from '../../../../services/backend/events/flowy-database'; import { GetFieldPayloadPB, RepeatedFieldIdPB, FieldIdPB, -} from '../../../../services/backend/models/flowy-database/field_entities'; -import { ViewIdPB } from '../../../../services/backend/models/flowy-folder/view'; + DatabaseViewIdPB, + CreateRowPayloadPB, + ViewIdPB, +} from '../../../../services/backend'; import { FolderEventCloseView } from '../../../../services/backend/events/flowy-folder'; export class DatabaseBackendService { 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 new file mode 100644 index 0000000000..3380cffb71 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/database_controller.ts @@ -0,0 +1,56 @@ +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/models/flowy-database/grid_entities'; +import { RowChangedReason, RowInfo } from './row/row_cache'; +import { Err, Ok } from 'ts-results'; + +export type SubscribeCallback = { + onViewChanged?: (data: DatabasePB) => void; + onRowsChanged?: (rowInfos: readonly RowInfo[], reason: RowChangedReason) => void; + onFieldsChanged?: (fieldInfos: readonly FieldInfo[]) => void; +}; + +export class DatabaseController { + private _backendService: DatabaseBackendService; + fieldController: FieldController; + databaseViewCache: DatabaseViewCache; + private _callback?: SubscribeCallback; + + constructor(public readonly viewId: string) { + this._backendService = new DatabaseBackendService(viewId); + this.fieldController = new FieldController(viewId); + this.databaseViewCache = new DatabaseViewCache(viewId, this.fieldController); + } + + subscribe = (callbacks: SubscribeCallback) => { + this._callback = callbacks; + this.fieldController.subscribeOnFieldsChanged(callbacks.onFieldsChanged); + this.databaseViewCache.getRowCache().subscribeOnRowsChanged((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); + await this.fieldController.loadFields(database.fields); + this.databaseViewCache.initializeWithRows(database.rows); + return Ok.EMPTY; + } else { + return Err(result.val); + } + }; + + createRow = async () => { + return this._backendService.createRow(); + }; + + dispose = async () => { + await this._backendService.closeDatabase(); + await this.fieldController.dispose(); + await this.databaseViewCache.dispose(); + }; +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/field/backend_service.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/field/field_bd_svc.ts similarity index 99% rename from frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/field/backend_service.ts rename to frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/field/field_bd_svc.ts index 1f9c878e42..835fa9ea33 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/field/backend_service.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/field/field_bd_svc.ts @@ -71,7 +71,6 @@ export class FieldBackendService { duplicateField = () => { const payload = DuplicateFieldPayloadPB.fromObject({ view_id: this.viewId, field_id: this.fieldId }); - return DatabaseEventDuplicateField(payload); }; diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/field/controller.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/field/field_controller.ts similarity index 90% rename from frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/field/controller.ts rename to frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/field/field_controller.ts index 86bd7bc276..b3268e9ca9 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/field/controller.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/field/field_controller.ts @@ -1,13 +1,13 @@ import { Log } from '../../../../utils/log'; -import { DatabaseBackendService } from '../backend_service'; +import { DatabaseBackendService } from '../database_bd_svc'; import { DatabaseFieldObserver } from './field_observer'; import { FieldIdPB, FieldPB, IndexFieldPB } from '../../../../../services/backend/models/flowy-database/field_entities'; import { ChangeNotifier } from '../../../../utils/change_notifier'; export class FieldController { - _fieldListener: DatabaseFieldObserver; - _backendService: DatabaseBackendService; - _fieldNotifier = new FieldNotifier([]); + private _fieldListener: DatabaseFieldObserver; + private _backendService: DatabaseBackendService; + private _fieldNotifier = new FieldNotifier([]); constructor(public readonly viewId: string) { this._backendService = new DatabaseBackendService(viewId); @@ -33,12 +33,14 @@ export class FieldController { const result = await this._backendService.getFields(fieldIds); if (result.ok) { this._fieldNotifier.fieldInfos = result.val.map((field) => new FieldInfo(field)); + } else { + Log.error(result.val); } }; - subscribeOnFieldsChanged = (callback: (fieldInfos: FieldInfo[]) => void) => { + subscribeOnFieldsChanged = (callback?: (fieldInfos: readonly FieldInfo[]) => void) => { return this._fieldNotifier.observer.subscribe((fieldInfos) => { - callback(fieldInfos); + callback?.(fieldInfos); }); }; 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 e203a6be1e..8ae3d58249 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 @@ -9,27 +9,30 @@ type UpdateFieldNotifiedValue = Result; export type DatabaseNotificationCallback = (value: UpdateFieldNotifiedValue) => void; export class DatabaseFieldObserver { - _notifier?: ChangeNotifier; - _listener?: DatabaseNotificationObserver; + private _notifier?: ChangeNotifier; + private _listener?: DatabaseNotificationObserver; - constructor(public readonly databaseId: string) {} + constructor(public readonly viewId: string) {} subscribe = (callbacks: { onFieldsChanged: DatabaseNotificationCallback }) => { this._notifier = new ChangeNotifier(); this._notifier?.observer.subscribe(callbacks.onFieldsChanged); this._listener = new DatabaseNotificationObserver({ - viewId: this.databaseId, - parserHandler: (notification, payload) => { + viewId: this.viewId, + parserHandler: (notification, result) => { switch (notification) { case DatabaseNotification.DidUpdateFields: - this._notifier?.notify(Ok(DatabaseFieldChangesetPB.deserializeBinary(payload))); + if (result.ok) { + this._notifier?.notify(Ok(DatabaseFieldChangesetPB.deserializeBinary(result.val))); + } else { + this._notifier?.notify(result); + } return; default: break; } }, - onError: (error) => this._notifier?.notify(Err(error)), }); return undefined; }; diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/notifications/observer.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/notifications/observer.ts index 317d829ad6..855c6a3ab4 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/notifications/observer.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/notifications/observer.ts @@ -1,16 +1,16 @@ -import { DatabaseNotification } from '../../../../../services/backend/models/flowy-database/notification'; -import { OnNotificationError } from '../../../../../services/backend/notifications'; -import { AFNotificationObserver } from '../../../../../services/backend/notifications/observer'; +import { DatabaseNotification } from '../../../../../services/backend'; +import { AFNotificationObserver } from '../../../../../services/backend/notifications'; import { DatabaseNotificationParser } from './parser'; +import { FlowyError } from '../../../../../services/backend'; +import { Result } from 'ts-results'; -export type ParserHandler = (notification: DatabaseNotification, payload: Uint8Array) => void; +export type ParserHandler = (notification: DatabaseNotification, result: Result) => void; export class DatabaseNotificationObserver extends AFNotificationObserver { - constructor(params: { viewId?: string; parserHandler: ParserHandler; onError?: OnNotificationError }) { + constructor(params: { viewId?: string; parserHandler: ParserHandler }) { const parser = new DatabaseNotificationParser({ callback: params.parserHandler, id: params.viewId, - onError: params.onError, }); super(parser); } diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/notifications/parser.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/notifications/parser.ts index b507c004a3..8232a281c9 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/notifications/parser.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/notifications/parser.ts @@ -1,10 +1,11 @@ -import { DatabaseNotification } from '../../../../../services/backend'; -import { NotificationParser, OnNotificationError } from '../../../../../services/backend/notifications'; +import { DatabaseNotification, FlowyError } from '../../../../../services/backend'; +import { NotificationParser } from '../../../../../services/backend/notifications'; +import { Result } from 'ts-results'; -declare type DatabaseNotificationCallback = (ty: DatabaseNotification, payload: Uint8Array) => void; +declare type DatabaseNotificationCallback = (ty: DatabaseNotification, payload: Result) => void; export class DatabaseNotificationParser extends NotificationParser { - constructor(params: { id?: string; callback: DatabaseNotificationCallback; onError?: OnNotificationError }) { + constructor(params: { id?: string; callback: DatabaseNotificationCallback }) { super( params.callback, (ty) => { @@ -15,8 +16,7 @@ export class DatabaseNotificationParser extends NotificationParser; export class RowCache { - _rowList: RowList; - _cellCache: CellCache; - _notifier: RowChangeNotifier; + private readonly _rowList: RowList; + private readonly _cellCache: CellCache; + private readonly _notifier: RowChangeNotifier; constructor(public readonly viewId: string, private readonly getFieldInfos: () => readonly FieldInfo[]) { this._rowList = new RowList(); @@ -24,6 +33,26 @@ export class RowCache { return this._rowList.rows; } + getCellCache = () => { + return this._cellCache; + }; + + loadCells = async (rowId: string): Promise => { + const opRow = this._rowList.getRow(rowId); + if (opRow.some) { + return this._toCellMap(opRow.val.row.id, this.getFieldInfos()); + } else { + const rowResult = await this._loadRow(rowId); + if (rowResult.ok) { + this._refreshRow(rowResult.val); + return this._toCellMap(rowId, this.getFieldInfos()); + } else { + Log.error(rowResult.val); + return new Map(); + } + } + }; + subscribeOnRowsChanged = (callback: (reason: RowChangedReason, cellMap?: Map) => void) => { return this._notifier.observer.subscribe((change) => { if (change.rowId !== undefined) { @@ -47,6 +76,7 @@ export class RowCache { rows.forEach((rowPB) => { this._rowList.push(this._toRowInfo(rowPB)); }); + this._notifier.withChange(RowChangedReason.ReorderRows); }; applyRowsChanged = (changeset: RowsChangesetPB) => { @@ -73,7 +103,28 @@ export class RowCache { } }; - _deleteRows = (rowIds: string[]) => { + private _refreshRow = (opRow: OptionalRowPB) => { + if (!opRow.has_row) { + return; + } + const updatedRow = opRow.row; + const option = this._rowList.getRowWithIndex(updatedRow.id); + if (option.some) { + const { rowInfo, index } = option.val; + this._rowList.remove(rowInfo.row.id); + this._rowList.insert(index, rowInfo.copyWith({ row: updatedRow })); + } else { + const newRowInfo = new RowInfo(this.viewId, this.getFieldInfos(), updatedRow); + this._rowList.push(newRowInfo); + } + }; + + private _loadRow = (rowId: string) => { + const payload = RowIdPB.fromObject({ view_id: this.viewId, row_id: rowId }); + return DatabaseEventGetRow(payload); + }; + + private _deleteRows = (rowIds: string[]) => { rowIds.forEach((rowId) => { const deletedRow = this._rowList.remove(rowId); if (deletedRow !== undefined) { @@ -82,7 +133,7 @@ export class RowCache { }); }; - _insertRows = (rows: InsertedRowPB[]) => { + private _insertRows = (rows: InsertedRowPB[]) => { rows.forEach((insertedRow) => { const rowInfo = this._toRowInfo(insertedRow.row); const insertedIndex = this._rowList.insert(insertedRow.index, rowInfo); @@ -92,7 +143,7 @@ export class RowCache { }); }; - _updateRows = (updatedRows: UpdatedRowPB[]) => { + private _updateRows = (updatedRows: UpdatedRowPB[]) => { if (updatedRows.length === 0) { return; } @@ -113,7 +164,7 @@ export class RowCache { }); }; - _hideRows = (rowIds: string[]) => { + private _hideRows = (rowIds: string[]) => { rowIds.forEach((rowId) => { const deletedRow = this._rowList.remove(rowId); if (deletedRow !== undefined) { @@ -122,7 +173,7 @@ export class RowCache { }); }; - _displayRows = (insertedRows: InsertedRowPB[]) => { + private _displayRows = (insertedRows: InsertedRowPB[]) => { insertedRows.forEach((insertedRow) => { const insertedIndex = this._rowList.insert(insertedRow.index, this._toRowInfo(insertedRow.row)); @@ -136,11 +187,11 @@ export class RowCache { this._notifier.dispose(); }; - _toRowInfo = (rowPB: RowPB) => { + private _toRowInfo = (rowPB: RowPB) => { return new RowInfo(this.viewId, this.getFieldInfos(), rowPB); }; - _toCellMap = (rowId: string, fieldInfos: readonly FieldInfo[]): Map => { + private _toCellMap = (rowId: string, fieldInfos: readonly FieldInfo[]): CellByFieldId => { const cellIdentifierByFieldId: Map = new Map(); fieldInfos.forEach((fieldInfo) => { @@ -160,17 +211,22 @@ class RowList { return this._rowInfos; } - getRow = (rowId: string) => { - return this._rowInfoByRowId.get(rowId); + getRow = (rowId: string): Option => { + const rowInfo = this._rowInfoByRowId.get(rowId); + if (rowInfo === undefined) { + return None; + } else { + return Some(rowInfo); + } }; - getRowWithIndex = (rowId: string): { rowInfo: RowInfo; index: number } | undefined => { + getRowWithIndex = (rowId: string): Option<{ rowInfo: RowInfo; index: number }> => { const rowInfo = this._rowInfoByRowId.get(rowId); if (rowInfo !== undefined) { const index = this._rowInfos.indexOf(rowInfo, 0); - return { rowInfo: rowInfo, index: index }; + return Some({ rowInfo: rowInfo, index: index }); } - return undefined; + return None; }; indexOfRow = (rowId: string): number => { @@ -194,27 +250,29 @@ class RowList { remove = (rowId: string): DeletedRow | undefined => { const result = this.getRowWithIndex(rowId); - if (result !== undefined) { - this._rowInfoByRowId.delete(result.rowInfo.row.id); - this._rowInfos.splice(result.index, 1); - return new DeletedRow(result.index, result.rowInfo); + if (result.some) { + const { rowInfo, index } = result.val; + this._rowInfoByRowId.delete(rowInfo.row.id); + this._rowInfos.splice(index, 1); + return new DeletedRow(index, rowInfo); } else { return undefined; } }; - insert = (index: number, newRowInfo: RowInfo): InsertedRow | undefined => { + insert = (insertIndex: number, newRowInfo: RowInfo): InsertedRow | undefined => { const rowId = newRowInfo.row.id; // Calibrate where to insert - let insertedIndex = index; + let insertedIndex = insertIndex; if (this._rowInfos.length <= insertedIndex) { insertedIndex = this._rowInfos.length; } const result = this.getRowWithIndex(rowId); - if (result !== undefined) { + if (result.some) { + const { index } = result.val; // remove the old row info - this._rowInfos.splice(result.index, 1); + this._rowInfos.splice(index, 1); // insert the new row info to the insertedIndex this._rowInfos.splice(insertedIndex, 0, newRowInfo); this._rowInfoByRowId.set(rowId, newRowInfo); @@ -268,10 +326,14 @@ class RowList { export class RowInfo { constructor( - public readonly databaseId: string, + public readonly viewId: string, public readonly fieldInfos: readonly FieldInfo[], public readonly row: RowPB ) {} + + copyWith = (params: { row?: RowPB }) => { + return new RowInfo(this.viewId, this.fieldInfos, params.row || this.row); + }; } export class DeletedRow { diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/row/row_controller.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/row/row_controller.ts new file mode 100644 index 0000000000..7de6ee0e57 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/row/row_controller.ts @@ -0,0 +1,16 @@ +import { CellByFieldId, RowCache, RowInfo } from './row_cache'; +import { FieldController } from '../field/field_controller'; + +export class RowController { + constructor( + public readonly rowInfo: RowInfo, + public readonly fieldController: FieldController, + private readonly cache: RowCache + ) { + // + } + + loadCells = async (): Promise => { + return this.cache.loadCells(this.rowInfo.row.id); + }; +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/view/cache.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/view/database_view_cache.ts similarity index 62% rename from frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/view/cache.ts rename to frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/view/database_view_cache.ts index 497e6354d2..1468ce3246 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/view/cache.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/view/database_view_cache.ts @@ -1,15 +1,22 @@ -import { DatabaseViewRowsObserver } from './row_observer'; -import { RowCache, RowChangedReason } from '../row/cache'; -import { FieldController } from '../field/controller'; +import { DatabaseViewRowsObserver } from './view_row_observer'; +import { RowCache, RowChangedReason, RowInfo } from '../row/row_cache'; +import { FieldController } from '../field/field_controller'; import { RowPB } from '../../../../../services/backend/models/flowy-database/row_entities'; +import { Subscription } from 'rxjs'; export class DatabaseViewCache { - _rowsObserver: DatabaseViewRowsObserver; - _rowCache: RowCache; + 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.subscribeOnFieldsChanged((fieldInfos) => { + fieldInfos.forEach((fieldInfo) => { + this._rowCache.onFieldUpdated(fieldInfo); + }); + }); this._listenOnRowsChanged(); } @@ -17,13 +24,16 @@ export class DatabaseViewCache { this._rowCache.initializeRows(rows); }; - subscribeOnRowsChanged = (onRowsChanged: (reason: RowChangedReason) => void) => { - return this._rowCache.subscribeOnRowsChanged((reason) => { - onRowsChanged(reason); - }); + get rowInfos(): readonly RowInfo[] { + return this._rowCache.rows; + } + + getRowCache = () => { + return this._rowCache; }; dispose = async () => { + this._fieldSubscription?.unsubscribe(); await this._rowsObserver.unsubscribe(); await this._rowCache.dispose(); }; diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/view/row_observer.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/view/view_row_observer.ts similarity index 60% rename from frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/view/row_observer.ts rename to frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/view/view_row_observer.ts index 0bc25e0e57..72e50273bf 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/view/row_observer.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/view/view_row_observer.ts @@ -1,14 +1,12 @@ import { Ok, Result } from 'ts-results'; import { DatabaseNotification, + FlowyError, ReorderAllRowsPB, ReorderSingleRowPB, -} from '../../../../../services/backend/events/flowy-database'; -import { RowsChangesetPB, RowsVisibilityChangesetPB, -} from '../../../../../services/backend/models/flowy-database/view_entities'; -import { FlowyError } from '../../../../../services/backend/models/flowy-error/errors'; +} from '../../../../../services/backend'; import { ChangeNotifier } from '../../../../utils/change_notifier'; import { DatabaseNotificationObserver } from '../notifications/observer'; @@ -18,12 +16,13 @@ export type ReorderRowsNotifyValue = Result; export type ReorderSingleRowNotifyValue = Result; export class DatabaseViewRowsObserver { - _rowsVisibilityNotifier = new ChangeNotifier(); - _rowsNotifier = new ChangeNotifier(); - _reorderRowsNotifier = new ChangeNotifier(); - _reorderSingleRowNotifier = new ChangeNotifier(); + private _rowsVisibilityNotifier = new ChangeNotifier(); + private _rowsNotifier = new ChangeNotifier(); + private _reorderRowsNotifier = new ChangeNotifier(); + private _reorderSingleRowNotifier = new ChangeNotifier(); + + private _listener?: DatabaseNotificationObserver; - _listener?: DatabaseNotificationObserver; constructor(public readonly viewId: string) {} subscribe = (callbacks: { @@ -40,19 +39,35 @@ export class DatabaseViewRowsObserver { this._listener = new DatabaseNotificationObserver({ viewId: this.viewId, - parserHandler: (notification, payload) => { + parserHandler: (notification, result) => { switch (notification) { case DatabaseNotification.DidUpdateViewRowsVisibility: - this._rowsVisibilityNotifier.notify(Ok(RowsVisibilityChangesetPB.deserializeBinary(payload))); + if (result.ok) { + this._rowsVisibilityNotifier.notify(Ok(RowsVisibilityChangesetPB.deserializeBinary(result.val))); + } else { + this._rowsVisibilityNotifier.notify(result); + } break; case DatabaseNotification.DidUpdateViewRows: - this._rowsNotifier.notify(Ok(RowsChangesetPB.deserializeBinary(payload))); + if (result.ok) { + this._rowsNotifier.notify(Ok(RowsChangesetPB.deserializeBinary(result.val))); + } else { + this._rowsNotifier.notify(result); + } break; case DatabaseNotification.DidReorderRows: - this._reorderRowsNotifier.notify(Ok(ReorderAllRowsPB.deserializeBinary(payload).row_orders)); + if (result.ok) { + this._reorderRowsNotifier.notify(Ok(ReorderAllRowsPB.deserializeBinary(result.val).row_orders)); + } else { + this._reorderRowsNotifier.notify(result); + } break; case DatabaseNotification.DidReorderSingleRow: - this._reorderSingleRowNotifier.notify(Ok(ReorderSingleRowPB.deserializeBinary(payload))); + if (result.ok) { + this._reorderSingleRowNotifier.notify(Ok(ReorderSingleRowPB.deserializeBinary(result.val))); + } else { + this._reorderSingleRowNotifier.notify(result); + } break; default: break; diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/folder/app/backend_service.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/folder/app/app_bd_svc.ts similarity index 77% rename from frontend/appflowy_tauri/src/appflowy_app/stores/effects/folder/app/backend_service.ts rename to frontend/appflowy_tauri/src/appflowy_app/stores/effects/folder/app/app_bd_svc.ts index d1d8fd1aa3..e4fb4d7848 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/folder/app/backend_service.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/folder/app/app_bd_svc.ts @@ -5,18 +5,18 @@ import { FolderEventMoveItem, FolderEventReadApp, FolderEventUpdateApp, - ViewDataFormatPB, ViewLayoutTypePB, } from '../../../../../services/backend/events/flowy-folder'; -import { AppIdPB, UpdateAppPayloadPB } from '../../../../../services/backend/models/flowy-folder/app'; import { + AppIdPB, + UpdateAppPayloadPB, CreateViewPayloadPB, RepeatedViewIdPB, ViewPB, MoveFolderItemPayloadPB, MoveFolderItemType, -} from '../../../../../services/backend/models/flowy-folder/view'; -import { FlowyError } from '../../../../../services/backend/models/flowy-error/errors'; + FlowyError, +} from '../../../../../services/backend'; import { None, Result, Some } from 'ts-results'; export class AppBackendService { @@ -27,12 +27,11 @@ export class AppBackendService { return FolderEventReadApp(payload); }; - createView = (params: { + createView = async (params: { name: string; desc?: string; - dataFormatType: ViewDataFormatPB; layoutType: ViewLayoutTypePB; - /// The initial data should be the JSON of the doucment + /// The initial data should be the JSON of the document /// For example: {"document":{"type":"editor","children":[]}} initialData?: string; }) => { @@ -45,7 +44,13 @@ export class AppBackendService { initial_data: encoder.encode(params.initialData || ''), }); - return FolderEventCreateView(payload); + const result = await FolderEventCreateView(payload); + + if (result.ok) { + return result.val; + } else { + throw new Error(result.val.msg); + } }; getAllViews = (): Promise> => { @@ -69,14 +74,20 @@ export class AppBackendService { } }; - update = (params: { name: string }) => { + update = async (params: { name: string }) => { const payload = UpdateAppPayloadPB.fromObject({ app_id: this.appId, name: params.name }); - return FolderEventUpdateApp(payload); + const result = await FolderEventUpdateApp(payload); + if (!result.ok) { + throw new Error(result.val.msg); + } }; - delete = () => { + delete = async () => { const payload = AppIdPB.fromObject({ value: this.appId }); - return FolderEventDeleteApp(payload); + const result = await FolderEventDeleteApp(payload); + if (!result.ok) { + throw new Error(result.val.msg); + } }; deleteView = (viewId: string) => { 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 78607b4b1a..f1471a5a46 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 @@ -1,6 +1,5 @@ import { Ok, Result } from 'ts-results'; -import { AppPB, FolderNotification } from '../../../../../services/backend'; -import { FlowyError } from '../../../../../services/backend/models/flowy-error'; +import { AppPB, FlowyError, FolderNotification } from '../../../../../services/backend'; import { ChangeNotifier } from '../../../../utils/change_notifier'; import { FolderNotificationObserver } from '../notifications/observer'; @@ -18,10 +17,14 @@ export class WorkspaceObserver { this._listener = new FolderNotificationObserver({ viewId: this.appId, - parserHandler: (notification, payload) => { + parserHandler: (notification, result) => { switch (notification) { case FolderNotification.DidUpdateWorkspaceApps: - this._appNotifier?.notify(Ok(AppPB.deserializeBinary(payload))); + if (result.ok) { + this._appNotifier?.notify(Ok(AppPB.deserializeBinary(result.val))); + } else { + this._appNotifier?.notify(result); + } break; default: break; diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/folder/notifications/observer.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/folder/notifications/observer.ts index 0090c7df69..e89fd322f8 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/folder/notifications/observer.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/folder/notifications/observer.ts @@ -1,16 +1,16 @@ import { OnNotificationError } from '../../../../../services/backend/notifications'; -import { AFNotificationObserver } from '../../../../../services/backend/notifications/observer'; +import { AFNotificationObserver } from '../../../../../services/backend/notifications'; import { FolderNotificationParser } from './parser'; -import { FolderNotification } from '../../../../../services/backend/models/flowy-folder/notification'; +import { FlowyError, FolderNotification } from '../../../../../services/backend'; +import { Result } from 'ts-results'; -export type ParserHandler = (notification: FolderNotification, payload: Uint8Array) => void; +export type ParserHandler = (notification: FolderNotification, payload: Result) => void; export class FolderNotificationObserver extends AFNotificationObserver { constructor(params: { viewId?: string; parserHandler: ParserHandler; onError?: OnNotificationError }) { const parser = new FolderNotificationParser({ callback: params.parserHandler, id: params.viewId, - onError: params.onError, }); super(parser); } diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/folder/notifications/parser.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/folder/notifications/parser.ts index ee18dfcb7f..0d04698833 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/folder/notifications/parser.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/folder/notifications/parser.ts @@ -1,7 +1,8 @@ import { NotificationParser, OnNotificationError } from '../../../../../services/backend/notifications'; -import { FolderNotification } from '../../../../../services/backend/models/flowy-folder/notification'; +import { FlowyError, FolderNotification } from '../../../../../services/backend'; +import { Result } from 'ts-results'; -declare type FolderNotificationCallback = (ty: FolderNotification, payload: Uint8Array) => void; +declare type FolderNotificationCallback = (ty: FolderNotification, payload: Result) => void; export class FolderNotificationParser extends NotificationParser { constructor(params: { id?: string; callback: FolderNotificationCallback; onError?: OnNotificationError }) { @@ -15,8 +16,7 @@ export class FolderNotificationParser extends NotificationParser; type MoveToTrashViewNotifyValue = Result; export class ViewObserver { - _deleteViewNotifier = new ChangeNotifier(); - _updateViewNotifier = new ChangeNotifier(); - _restoreViewNotifier = new ChangeNotifier(); - _moveToTashNotifier = new ChangeNotifier(); - _listener?: FolderNotificationObserver; + private _deleteViewNotifier = new ChangeNotifier(); + private _updateViewNotifier = new ChangeNotifier(); + private _restoreViewNotifier = new ChangeNotifier(); + private _moveToTashNotifier = new ChangeNotifier(); + private _listener?: FolderNotificationObserver; constructor(public readonly viewId: string) {} @@ -42,19 +42,35 @@ export class ViewObserver { this._listener = new FolderNotificationObserver({ viewId: this.viewId, - parserHandler: (notification, payload) => { + parserHandler: (notification, result) => { switch (notification) { case FolderNotification.DidUpdateView: - this._updateViewNotifier.notify(Ok(ViewPB.deserializeBinary(payload))); + if (result.ok) { + this._updateViewNotifier.notify(Ok(ViewPB.deserializeBinary(result.val))); + } else { + this._updateViewNotifier.notify(result); + } break; case FolderNotification.DidDeleteView: - this._deleteViewNotifier.notify(Ok(ViewPB.deserializeBinary(payload))); + if (result.ok) { + this._deleteViewNotifier.notify(Ok(ViewPB.deserializeBinary(result.val))); + } else { + this._deleteViewNotifier.notify(result); + } break; case FolderNotification.DidRestoreView: - this._restoreViewNotifier.notify(Ok(ViewPB.deserializeBinary(payload))); + if (result.ok) { + this._restoreViewNotifier.notify(Ok(ViewPB.deserializeBinary(result.val))); + } else { + this._restoreViewNotifier.notify(result); + } break; case FolderNotification.DidMoveViewToTrash: - this._moveToTashNotifier.notify(Ok(DeletedViewPB.deserializeBinary(payload))); + if (result.ok) { + this._moveToTashNotifier.notify(Ok(DeletedViewPB.deserializeBinary(result.val))); + } else { + this._moveToTashNotifier.notify(result); + } break; default: break; diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/folder/workspace/backend_service.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/folder/workspace/workspace_bd_svc.ts similarity index 75% rename from frontend/appflowy_tauri/src/appflowy_app/stores/effects/folder/workspace/backend_service.ts rename to frontend/appflowy_tauri/src/appflowy_app/stores/effects/folder/workspace/workspace_bd_svc.ts index 0d39844dbc..f5a2d4e86f 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/folder/workspace/backend_service.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/folder/workspace/workspace_bd_svc.ts @@ -5,23 +5,25 @@ import { FolderEventReadWorkspaceApps, FolderEventReadWorkspaces, } from '../../../../../services/backend/events/flowy-folder'; -import { CreateAppPayloadPB } from '../../../../../services/backend/models/flowy-folder/app'; -import { WorkspaceIdPB } from '../../../../../services/backend/models/flowy-folder/workspace'; +import { CreateAppPayloadPB, WorkspaceIdPB, FlowyError, MoveFolderItemPayloadPB } from '../../../../../services/backend'; import assert from 'assert'; -import { FlowyError } from '../../../../../services/backend/models/flowy-error/errors'; -import { MoveFolderItemPayloadPB } from '../../../../../services/backend/models/flowy-folder/view'; export class WorkspaceBackendService { constructor(public readonly workspaceId: string) {} - createApp = (params: { name: string; desc?: string }) => { + createApp = async (params: { name: string; desc?: string }) => { const payload = CreateAppPayloadPB.fromObject({ workspace_id: this.workspaceId, name: params.name, desc: params.desc || '', }); - return FolderEventCreateApp(payload); + const result = await FolderEventCreateApp(payload); + if (result.ok) { + return result.val; + } else { + throw new Error(result.val.msg); + } }; getWorkspace = () => { diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/folder/workspace/workspace_observer.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/folder/workspace/workspace_observer.ts index e87bc4018d..80ea5d39b0 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/folder/workspace/workspace_observer.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/folder/workspace/workspace_observer.ts @@ -1,6 +1,5 @@ import { Ok, Result } from 'ts-results'; -import { AppPB, FolderNotification, RepeatedAppPB, WorkspacePB } from '../../../../../services/backend'; -import { FlowyError } from '../../../../../services/backend/models/flowy-error'; +import { AppPB, FolderNotification, RepeatedAppPB, WorkspacePB, FlowyError } from '../../../../../services/backend'; import { ChangeNotifier } from '../../../../utils/change_notifier'; import { FolderNotificationObserver } from '../notifications/observer'; @@ -10,9 +9,9 @@ export type WorkspaceNotifyValue = Result; export type WorkspaceNotifyCallback = (value: WorkspaceNotifyValue) => void; export class WorkspaceObserver { - _appListNotifier = new ChangeNotifier(); - _workspaceNotifier = new ChangeNotifier(); - _listener?: FolderNotificationObserver; + private _appListNotifier = new ChangeNotifier(); + private _workspaceNotifier = new ChangeNotifier(); + private _listener?: FolderNotificationObserver; constructor(public readonly workspaceId: string) {} @@ -22,13 +21,21 @@ export class WorkspaceObserver { this._listener = new FolderNotificationObserver({ viewId: this.workspaceId, - parserHandler: (notification, payload) => { + parserHandler: (notification, result) => { switch (notification) { case FolderNotification.DidUpdateWorkspace: - this._workspaceNotifier?.notify(Ok(WorkspacePB.deserializeBinary(payload))); + if (result.ok) { + this._workspaceNotifier?.notify(Ok(WorkspacePB.deserializeBinary(result.val))); + } else { + this._workspaceNotifier?.notify(result); + } break; case FolderNotification.DidUpdateWorkspaceApps: - this._appListNotifier?.notify(Ok(RepeatedAppPB.deserializeBinary(payload).items)); + if (result.ok) { + this._appListNotifier?.notify(Ok(RepeatedAppPB.deserializeBinary(result.val).items)); + } else { + this._appListNotifier?.notify(result); + } break; default: break; diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/user/backend_service.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/user/user_bd_svc.ts similarity index 75% rename from frontend/appflowy_tauri/src/appflowy_app/stores/effects/user/backend_service.ts rename to frontend/appflowy_tauri/src/appflowy_app/stores/effects/user/user_bd_svc.ts index 90ee60ee3f..c681cd5330 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/user/backend_service.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/user/user_bd_svc.ts @@ -6,12 +6,19 @@ import { UserEventSignUp, UserEventUpdateUserProfile, } from '../../../../services/backend/events/flowy-user'; -import { SignInPayloadPB, SignUpPayloadPB } from '../../../../services/backend/models/flowy-user/auth'; -import { UpdateUserProfilePayloadPB } from '../../../../services/backend/models/flowy-user/user_profile'; -import { WorkspaceIdPB, CreateWorkspacePayloadPB } from '../../../../services/backend/models/flowy-folder/workspace'; +import { + SignInPayloadPB, + SignUpPayloadPB, + UpdateUserProfilePayloadPB, + WorkspaceIdPB, + CreateWorkspacePayloadPB, + WorkspaceSettingPB, + WorkspacePB, +} from '../../../../services/backend'; import { FolderEventCreateWorkspace, FolderEventOpenWorkspace, + FolderEventReadCurrentWorkspace, FolderEventReadWorkspaces, } from '../../../../services/backend/events/flowy-folder'; @@ -39,6 +46,15 @@ export class UserBackendService { return UserEventUpdateUserProfile(payload); }; + getCurrentWorkspace = async (): Promise => { + const result = await FolderEventReadCurrentWorkspace(); + if (result.ok) { + return result.val; + } else { + throw new Error(result.val.msg); + } + }; + getWorkspaces = () => { const payload = WorkspaceIdPB.fromObject({}); return FolderEventReadWorkspaces(payload); @@ -49,9 +65,14 @@ export class UserBackendService { return FolderEventOpenWorkspace(payload); }; - createWorkspace = (params: { name: string; desc: string }) => { + createWorkspace = async (params: { name: string; desc: string }): Promise => { const payload = CreateWorkspacePayloadPB.fromObject({ name: params.name, desc: params.desc }); - return FolderEventCreateWorkspace(payload); + const result = await FolderEventCreateWorkspace(payload); + if (result.ok) { + return result.val; + } else { + throw new Error(result.val.msg); + } }; signOut = () => { diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/i18n/initializeI18n.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/i18n/initializeI18n.ts new file mode 100644 index 0000000000..6e4b47f4c1 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/i18n/initializeI18n.ts @@ -0,0 +1,59 @@ +import i18n from 'i18next'; +import LanguageDetector from 'i18next-browser-languagedetector'; +import { initReactI18next } from 'react-i18next'; +import en from '../../../../../appflowy_flutter/assets/translations/en.json'; +import ca_ES from '../../../../../appflowy_flutter/assets/translations/ca-ES.json'; +import de_DE from '../../../../../appflowy_flutter/assets/translations/de-DE.json'; +import es_VE from '../../../../../appflowy_flutter/assets/translations/es-VE.json'; +import eu_ES from '../../../../../appflowy_flutter/assets/translations/eu-ES.json'; +import fr_CA from '../../../../../appflowy_flutter/assets/translations/fr-CA.json'; +import fr_FR from '../../../../../appflowy_flutter/assets/translations/fr-FR.json'; +import hu_HU from '../../../../../appflowy_flutter/assets/translations/hu-HU.json'; +import id_ID from '../../../../../appflowy_flutter/assets/translations/id-ID.json'; +import it_IT from '../../../../../appflowy_flutter/assets/translations/it-IT.json'; +import ja_JP from '../../../../../appflowy_flutter/assets/translations/ja-JP.json'; +import ko_KR from '../../../../../appflowy_flutter/assets/translations/ko-KR.json'; +import pl_PL from '../../../../../appflowy_flutter/assets/translations/pl-PL.json'; +import pt_BR from '../../../../../appflowy_flutter/assets/translations/pt-BR.json'; +import pt_PT from '../../../../../appflowy_flutter/assets/translations/pt-PT.json'; +import ru_Ru from '../../../../../appflowy_flutter/assets/translations/ru-RU.json'; +import sv from '../../../../../appflowy_flutter/assets/translations/sv.json'; +import tr_TR from '../../../../../appflowy_flutter/assets/translations/tr-TR.json'; +import zh_CN from '../../../../../appflowy_flutter/assets/translations/zh-CN.json'; +import zh_TW from '../../../../../appflowy_flutter/assets/translations/zh-TW.json'; + +export default function () { + void i18n + .use(LanguageDetector) + .use(initReactI18next) + .init({ + resources: { + en: { translation: en }, + 'ca-ES': { translation: ca_ES }, + 'de-DE': { translation: de_DE }, + 'es-VE': { translation: es_VE }, + 'eu-ES': { translation: eu_ES }, + 'fr-CA': { translation: fr_CA }, + 'fr-FR': { translation: fr_FR }, + 'hu-HU': { translation: hu_HU }, + 'id-ID': { translation: id_ID }, + 'it-IT': { translation: it_IT }, + 'ja-JP': { translation: ja_JP }, + 'ko-KR': { translation: ko_KR }, + 'pl-PL': { translation: pl_PL }, + 'pt-BR': { translation: pt_BR }, + 'pt-PT': { translation: pt_PT }, + 'ru-RU': { translation: ru_Ru }, + sv: { translation: sv }, + 'tr-TR': { translation: tr_TR }, + 'zh-CN': { translation: zh_CN }, + 'zh-TW': { translation: zh_TW }, + }, + fallbackLng: 'en', + debug: true, + + interpolation: { + escapeValue: false, // not needed for react as it escapes by default + }, + }); +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/board/slice.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/board/slice.ts new file mode 100644 index 0000000000..9a9fe82ab5 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/board/slice.ts @@ -0,0 +1,15 @@ +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; + +const initialState = 'field1'; + +export const boardSlice = createSlice({ + name: 'board', + initialState: initialState as string, + reducers: { + setGroupingFieldId: (state, action: PayloadAction<{ fieldId: string }>) => { + return action.payload.fieldId; + }, + }, +}); + +export const boardActions = boardSlice.actions; diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/current-user/slice.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/current-user/slice.ts index a3bfcaf5fb..190dfcfb9c 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/current-user/slice.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/current-user/slice.ts @@ -1,12 +1,14 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit'; import { nanoid } from 'nanoid'; +import { WorkspaceSettingPB } from '../../../../services/backend/models/flowy-folder/workspace'; export interface ICurrentUser { - id: string; - displayName: string; - email: string; - token: string; + id?: string; + displayName?: string; + email?: string; + token?: string; isAuthenticated: boolean; + workspaceSetting?: WorkspaceSettingPB, } const initialState: ICurrentUser | null = { @@ -24,8 +26,10 @@ export const currentUserSlice = createSlice({ updateUser: (state, action: PayloadAction) => { return action.payload; }, - logout: (state) => { - state.isAuthenticated = false; + logout: () => { + return { + isAuthenticated: false, + }; }, }, }); diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/database/slice.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/database/slice.ts new file mode 100644 index 0000000000..8c83a0c8f6 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/database/slice.ts @@ -0,0 +1,327 @@ +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; +import { nanoid } from 'nanoid'; +import { FieldType } from '../../../../services/backend/models/flowy-database/field_entities'; +import { DateFormat, NumberFormat, SelectOptionColorPB, TimeFormat } from '../../../../services/backend'; + +export interface ISelectOption { + selectOptionId: string; + title: string; + color?: SelectOptionColorPB; +} + +export interface IFieldOptions { + selectOptions?: ISelectOption[]; + dateFormat?: DateFormat; + timeFormat?: TimeFormat; + includeTime?: boolean; + numberFormat?: NumberFormat; +} + +export interface IDatabaseField { + fieldId: string; + title: string; + fieldType: FieldType; + fieldOptions: IFieldOptions; +} + +export interface IDatabaseColumn { + fieldId: string; + sort: 'none' | 'asc' | 'desc'; + filter?: any; + visible: boolean; +} + +export interface ICellData { + rowId: string; + fieldId: string; + cellId: string; + data: string | number; + optionIds?: string[]; +} + +export type DatabaseCellMap = { [keys: string]: ICellData }; + +export interface IDatabaseRow { + rowId: string; + // key(fieldId) -> value(Cell) + cells: DatabaseCellMap; +} + +export type DatabaseFieldMap = { [keys: string]: IDatabaseField }; + +export interface IDatabase { + title: string; + fields: DatabaseFieldMap; + rows: IDatabaseRow[]; + columns: IDatabaseColumn[]; +} + +// key(databaseId) -> value(IDatabase) +const initialState: IDatabase = { + title: 'Database One', + columns: [ + { + visible: true, + fieldId: 'field1', + sort: 'none', + }, + { + visible: true, + fieldId: 'field2', + sort: 'none', + }, + { + visible: true, + fieldId: 'field3', + sort: 'none', + }, + { + visible: true, + fieldId: 'field4', + sort: 'none', + }, + ], + fields: { + field1: { + title: 'status', + fieldId: 'field1', + fieldType: FieldType.SingleSelect, + fieldOptions: { + selectOptions: [ + { + selectOptionId: 'so1', + title: 'To Do', + color: SelectOptionColorPB.Orange, + }, + { + selectOptionId: 'so2', + title: 'In Progress', + color: SelectOptionColorPB.Green, + }, + { + selectOptionId: 'so3', + title: 'Done', + color: SelectOptionColorPB.Blue, + }, + ], + }, + }, + field2: { + title: 'name', + fieldId: 'field2', + fieldType: FieldType.RichText, + fieldOptions: {}, + }, + field3: { + title: 'percent', + fieldId: 'field3', + fieldType: FieldType.Number, + fieldOptions: { + numberFormat: NumberFormat.Num, + }, + }, + field4: { + title: 'tags', + fieldId: 'field4', + fieldType: FieldType.MultiSelect, + fieldOptions: { + selectOptions: [ + { + selectOptionId: 'f4so1', + title: 'type1', + color: SelectOptionColorPB.Blue, + }, + { + selectOptionId: 'f4so2', + title: 'type2', + color: SelectOptionColorPB.Aqua, + }, + { + selectOptionId: 'f4so3', + title: 'type3', + color: SelectOptionColorPB.Purple, + }, + { + selectOptionId: 'f4so4', + title: 'type4', + color: SelectOptionColorPB.Purple, + }, + { + selectOptionId: 'f4so5', + title: 'type5', + color: SelectOptionColorPB.Purple, + }, + { + selectOptionId: 'f4so6', + title: 'type6', + color: SelectOptionColorPB.Purple, + }, + { + selectOptionId: 'f4so7', + title: 'type7', + color: SelectOptionColorPB.Purple, + }, + ], + }, + }, + }, + rows: [ + { + rowId: 'row1', + cells: { + field1: { + rowId: 'row1', + fieldId: 'field1', + cellId: 'cell11', + data: '', + optionIds: ['so1'], + }, + field2: { + rowId: 'row1', + fieldId: 'field2', + cellId: 'cell12', + data: 'Card 1', + }, + field3: { + rowId: 'row1', + fieldId: 'field3', + cellId: 'cell13', + data: 10, + }, + field4: { + rowId: 'row1', + fieldId: 'field4', + cellId: 'cell14', + data: '', + optionIds: ['f4so2', 'f4so3', 'f4so4', 'f4so5', 'f4so6', 'f4so7'], + }, + }, + }, + { + rowId: 'row2', + cells: { + field1: { + rowId: 'row2', + fieldId: 'field1', + cellId: 'cell21', + data: '', + optionIds: ['so1'], + }, + field2: { + rowId: 'row2', + fieldId: 'field2', + cellId: 'cell22', + data: 'Card 2', + }, + field3: { + rowId: 'row2', + fieldId: 'field3', + cellId: 'cell23', + data: 20, + }, + field4: { + rowId: 'row2', + fieldId: 'field4', + cellId: 'cell24', + data: '', + optionIds: ['f4so1'], + }, + }, + }, + ], +}; + +export const databaseSlice = createSlice({ + name: 'database', + initialState: initialState, + reducers: { + updateTitle: (state, action: PayloadAction<{ title: string }>) => { + state.title = action.payload.title; + }, + + addField: (state, action: PayloadAction<{ field: IDatabaseField }>) => { + const { field } = action.payload; + + state.fields[field.fieldId] = field; + state.columns.push({ + fieldId: field.fieldId, + sort: 'none', + visible: true, + }); + state.rows = state.rows.map((r: IDatabaseRow) => { + const cells = r.cells; + cells[field.fieldId] = { + rowId: r.rowId, + fieldId: field.fieldId, + data: '', + cellId: nanoid(6), + }; + return { + rowId: r.rowId, + cells: cells, + }; + }); + }, + + updateField: (state, action: PayloadAction<{ field: IDatabaseField }>) => { + const { field } = action.payload; + + state.fields[field.fieldId] = field; + }, + + addFieldSelectOption: (state, action: PayloadAction<{ fieldId: string; option: ISelectOption }>) => { + const { fieldId, option } = action.payload; + + const field = state.fields[fieldId]; + const selectOptions = field.fieldOptions?.selectOptions; + + if (selectOptions) { + selectOptions.push(option); + } else { + state.fields[field.fieldId].fieldOptions = { + ...state.fields[field.fieldId].fieldOptions, + selectOptions: [option], + }; + } + }, + + updateFieldSelectOption: (state, action: PayloadAction<{ fieldId: string; option: ISelectOption }>) => { + const { fieldId, option } = action.payload; + + const field = state.fields[fieldId]; + const selectOptions = field.fieldOptions?.selectOptions; + if (selectOptions) { + selectOptions[selectOptions.findIndex((o) => o.selectOptionId === option.selectOptionId)] = option; + } + }, + + addRow: (state) => { + const rowId = nanoid(6); + const cells: { [keys: string]: ICellData } = {}; + Object.keys(state.fields).forEach((id) => { + cells[id] = { + rowId: rowId, + fieldId: id, + data: '', + cellId: nanoid(6), + }; + }); + const newRow: IDatabaseRow = { + rowId: rowId, + cells: cells, + }; + + state.rows.push(newRow); + }, + + updateCellValue: (source, action: PayloadAction<{ cell: ICellData }>) => { + const { cell } = action.payload; + const row = source.rows.find((r) => r.rowId === cell.rowId); + if (row) { + row.cells[cell.fieldId] = cell; + } + }, + }, +}); + +export const databaseActions = databaseSlice.actions; diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/error/slice.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/error/slice.ts new file mode 100644 index 0000000000..9b47df7777 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/error/slice.ts @@ -0,0 +1,32 @@ +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; + +export interface IErrorOptions { + display: boolean; + message: string; +} + +const initialState: IErrorOptions = { + display: false, + message: '', +}; + +export const errorSlice = createSlice({ + name: 'error', + initialState: initialState, + reducers: { + showError(state, action: PayloadAction) { + return { + display: true, + message: action.payload, + }; + }, + hideError() { + return { + display: false, + message: '', + }; + }, + }, +}); + +export const errorActions = errorSlice.actions; diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/folders/notifications/parser.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/folders/notifications/parser.ts index 09b0c9d967..e0220a9f22 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/folders/notifications/parser.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/folders/notifications/parser.ts @@ -1,7 +1,8 @@ -import { FolderNotification } from '../../../../../services/backend'; +import { FlowyError, FolderNotification } from '../../../../../services/backend'; import { NotificationParser, OnNotificationError } from '../../../../../services/backend/notifications'; +import { Result } from 'ts-results'; -declare type FolderNotificationCallback = (ty: FolderNotification, payload: Uint8Array) => void; +declare type FolderNotificationCallback = (ty: FolderNotification, payload: Result) => void; export class FolderNotificationParser extends NotificationParser { constructor(params: { id?: string; callback: FolderNotificationCallback; onError?: OnNotificationError }) { @@ -15,8 +16,7 @@ export class FolderNotificationParser extends NotificationParser) { return state.filter((f) => f.id !== action.payload.id); }, + clearFolders() { + return []; + }, }, }); 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 e564d9b1a0..cb09fdbc58 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 @@ -25,6 +25,9 @@ export const pagesSlice = createSlice({ deletePage(state, action: PayloadAction<{ id: string }>) { return state.filter((page) => page.id !== action.payload.id); }, + clearPages() { + return []; + }, }, }); diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/workspace/slice.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/workspace/slice.ts new file mode 100644 index 0000000000..2609819031 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/workspace/slice.ts @@ -0,0 +1,18 @@ +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; + +export interface IWorkspace { + id?: string; + name?: string; +} + +export const workspaceSlice = createSlice({ + name: 'workspace', + initialState: {} as IWorkspace, + reducers: { + updateWorkspace: (state, action: PayloadAction) => { + return action.payload; + }, + }, +}); + +export const workspaceActions = workspaceSlice.actions; diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/store.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/store.ts index 17750cdf2f..cf96a7d625 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/stores/store.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/store.ts @@ -12,6 +12,10 @@ import { pagesSlice } from './reducers/pages/slice'; import { navigationWidthSlice } from './reducers/navigation-width/slice'; import { currentUserSlice } from './reducers/current-user/slice'; import { gridSlice } from './reducers/grid/slice'; +import { workspaceSlice } from './reducers/workspace/slice'; +import { databaseSlice } from './reducers/database/slice'; +import { boardSlice } from './reducers/board/slice'; +import { errorSlice } from './reducers/error/slice'; const listenerMiddlewareInstance = createListenerMiddleware({ onError: () => console.error, @@ -24,6 +28,10 @@ const store = configureStore({ [navigationWidthSlice.name]: navigationWidthSlice.reducer, [currentUserSlice.name]: currentUserSlice.reducer, [gridSlice.name]: gridSlice.reducer, + [databaseSlice.name]: databaseSlice.reducer, + [boardSlice.name]: boardSlice.reducer, + [workspaceSlice.name]: workspaceSlice.reducer, + [errorSlice.name]: errorSlice.reducer, }, middleware: (gDM) => gDM().prepend(listenerMiddlewareInstance.middleware), }); diff --git a/frontend/appflowy_tauri/src/appflowy_app/views/BoardPage.tsx b/frontend/appflowy_tauri/src/appflowy_app/views/BoardPage.tsx index 49f22e8a04..0e2cc8cc19 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/views/BoardPage.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/views/BoardPage.tsx @@ -1,7 +1,22 @@ import { useParams } from 'react-router-dom'; +import { useEffect, useState } from 'react'; +import { Board } from '../components/board/Board'; export const BoardPage = () => { const params = useParams(); + const [databaseId, setDatabaseId] = useState(''); - return
Board Page ID: {params.id}
; + useEffect(() => { + if (params?.id?.length) { + // setDatabaseId(params.id); + setDatabaseId('testDb'); + } + }, [params]); + + return ( +
+

Board

+ {databaseId?.length && } +
+ ); }; diff --git a/frontend/appflowy_tauri/src/appflowy_app/views/DocumentPage.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/views/DocumentPage.hooks.ts new file mode 100644 index 0000000000..b2819b0b21 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/views/DocumentPage.hooks.ts @@ -0,0 +1,24 @@ +import { + DocumentEventGetDocument, + DocumentVersionPB, + OpenDocumentPayloadPB, +} from '../../services/backend/events/flowy-document'; + +export const useDocument = () => { + const loadDocument = async (id: string): Promise => { + const getDocumentResult = await DocumentEventGetDocument( + OpenDocumentPayloadPB.fromObject({ + document_id: id, + version: DocumentVersionPB.V1, + }) + ); + + if (getDocumentResult.ok) { + const pb = getDocumentResult.val; + return JSON.parse(pb.content); + } else { + throw new Error('get document error'); + } + }; + return { loadDocument }; +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/views/DocumentPage.tsx b/frontend/appflowy_tauri/src/appflowy_app/views/DocumentPage.tsx index a35611cc1e..2b6cf4e9f6 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/views/DocumentPage.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/views/DocumentPage.tsx @@ -1,8 +1,17 @@ import { useParams } from 'react-router-dom'; import { useEffect } from 'react'; +import { useDocument } from './DocumentPage.hooks'; export const DocumentPage = () => { const params = useParams(); + const { loadDocument } = useDocument(); + useEffect(() => { + void (async () => { + if (!params?.id) return; + const content: any = await loadDocument(params.id); + console.log(content); + })(); + }, [params]); return
Document Page ID: {params.id}
; }; diff --git a/frontend/appflowy_tauri/src/appflowy_app/views/GridPage.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/views/GridPage.hooks.ts new file mode 100644 index 0000000000..68c0f81ebd --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/views/GridPage.hooks.ts @@ -0,0 +1,9 @@ +export const useGrid = () => { + const loadGrid = async (id: string) => { + console.log('loading grid'); + }; + + return { + loadGrid, + }; +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/views/GridPage.tsx b/frontend/appflowy_tauri/src/appflowy_app/views/GridPage.tsx index 350404b339..ce8e474e05 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/views/GridPage.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/views/GridPage.tsx @@ -6,8 +6,20 @@ import { GridTableRows } from '../components/grid/GridTableRows/GridTableRows'; import { GridTitle } from '../components/grid/GridTitle/GridTitle'; import { SearchInput } from '../components/_shared/SearchInput'; import { GridToolbar } from '../components/grid/GridToolbar/GridToolbar'; +import { useParams } from 'react-router-dom'; +import { useGrid } from './GridPage.hooks'; +import { useEffect } from 'react'; export const GridPage = () => { + const params = useParams(); + const { loadGrid } = useGrid(); + useEffect(() => { + void (async () => { + if (!params?.id) return; + await loadGrid(params.id); + })(); + }, [params]); + return (

Grid

diff --git a/frontend/appflowy_tauri/src/main.tsx b/frontend/appflowy_tauri/src/main.tsx index 2be5243f17..e53dc96c43 100644 --- a/frontend/appflowy_tauri/src/main.tsx +++ b/frontend/appflowy_tauri/src/main.tsx @@ -5,8 +5,4 @@ import './styles/tailwind.css'; import './styles/font.css'; import './styles/template.css'; -ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( - - - -); +ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(); diff --git a/frontend/appflowy_tauri/src/services/backend/notifications/parser.ts b/frontend/appflowy_tauri/src/services/backend/notifications/parser.ts index c74dd82b72..dc58c4eec7 100644 --- a/frontend/appflowy_tauri/src/services/backend/notifications/parser.ts +++ b/frontend/appflowy_tauri/src/services/backend/notifications/parser.ts @@ -1,7 +1,8 @@ -import { FlowyError } from '../models/flowy-error'; -import { SubscribeObject } from '../models/flowy-notification'; +import { FlowyError } from "../models/flowy-error"; +import { SubscribeObject } from "../models/flowy-notification"; +import { Err, Ok, Result } from "ts-results"; -export declare type OnNotificationPayload = (ty: T, payload: Uint8Array) => void; +export declare type OnNotificationPayload = (ty: T, payload: Result) => void; export declare type OnNotificationError = (error: FlowyError) => void; export declare type NotificationTyParser = (num: number) => T | null; export declare type ErrParser = (data: Uint8Array) => E; @@ -9,23 +10,20 @@ export declare type ErrParser = (data: Uint8Array) => E; export abstract class NotificationParser { id?: string; onPayload: OnNotificationPayload; - onError?: OnNotificationError; tyParser: NotificationTyParser; - constructor( + protected constructor( onPayload: OnNotificationPayload, tyParser: NotificationTyParser, - id?: string, - onError?: OnNotificationError + id?: string ) { this.id = id; this.onPayload = onPayload; - this.onError = onError; this.tyParser = tyParser; } parse(subject: SubscribeObject) { - if (typeof this.id !== 'undefined' && this.id.length === 0) { + if (typeof this.id !== "undefined" && this.id.length === 0) { if (subject.id !== this.id) { return; } @@ -38,9 +36,9 @@ export abstract class NotificationParser { if (subject.has_error) { const error = FlowyError.deserializeBinary(subject.error); - this.onError?.(error); + this.onPayload(ty, Err(error)); } else { - this.onPayload(ty, subject.payload); + this.onPayload(ty, Ok(subject.payload)); } } } diff --git a/frontend/appflowy_tauri/src/styles/template.css b/frontend/appflowy_tauri/src/styles/template.css index 2971d45a76..165bd3712f 100644 --- a/frontend/appflowy_tauri/src/styles/template.css +++ b/frontend/appflowy_tauri/src/styles/template.css @@ -28,6 +28,10 @@ body { @apply rounded-xl border border-gray-300 px-[18px] py-[14px] text-sm; } +.input.error { + @apply border-main-alert bg-main-alert/10; +} + th { @apply text-left font-normal; } diff --git a/frontend/appflowy_tauri/src/tests/user.test.ts b/frontend/appflowy_tauri/src/tests/user.test.ts index d757e87e99..41be820e97 100644 --- a/frontend/appflowy_tauri/src/tests/user.test.ts +++ b/frontend/appflowy_tauri/src/tests/user.test.ts @@ -1,4 +1,4 @@ -import { AuthBackendService, UserBackendService } from '../appflowy_app/stores/effects/user/backend_service'; +import { AuthBackendService, UserBackendService } from '../appflowy_app/stores/effects/user/user_bd_svc'; import { randomFillSync } from 'crypto'; import { nanoid } from '@reduxjs/toolkit'; diff --git a/frontend/appflowy_tauri/tsconfig.json b/frontend/appflowy_tauri/tsconfig.json index 7c506b2b44..a21d07380e 100644 --- a/frontend/appflowy_tauri/tsconfig.json +++ b/frontend/appflowy_tauri/tsconfig.json @@ -16,6 +16,6 @@ "noEmit": true, "jsx": "react-jsx" }, - "include": ["src", "vite.config.ts"], + "include": ["src", "vite.config.ts", "../app_flowy/assets/translations"], "references": [{ "path": "./tsconfig.node.json" }] } diff --git a/frontend/rust-lib/flowy-revision/src/rev_manager.rs b/frontend/rust-lib/flowy-revision/src/rev_manager.rs index 71f1a452bf..c0722e4a6c 100644 --- a/frontend/rust-lib/flowy-revision/src/rev_manager.rs +++ b/frontend/rust-lib/flowy-revision/src/rev_manager.rs @@ -228,7 +228,7 @@ impl RevisionManager { Ok(revisions) } - #[tracing::instrument(level = "debug", skip(self, revisions), err)] + #[tracing::instrument(level = "trace", skip(self, revisions), err)] pub async fn reset_object(&self, revisions: Vec) -> FlowyResult<()> { let rev_id = pair_rev_id_from_revisions(&revisions).1; self.rev_persistence.reset(revisions).await?;