mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
Feat/appflowy tauri 2 (#1902)
* chore: rename classes to models * refactor: add effects and reducers folder * chore: update user data storage path * chore: subscribe callback * chore: nav items persist, board layout (#1879) * chore: load workspace items, load folders and pages from workspace, load raw document data, load raw grid data * chore: clear folders and pages before load, new folder event * chore: update folder name backend call * chore: folder expand animation * chore: hide arrow on empty folder * chore: Board page layout, board store, board sample data * chore: board block item * chore: test db id * chore: persist new page, persist page rename, create workspace on read error * chore: boardblockitem details btn * chore: boardblockitem multiselect data and colors * chore: board item drag * chore: drag start on move * chore: remove databaseId * chore: remove databaseId * chore: import service classes into auth hook * chore: sign out option * chore: login page event * chore: signup event * chore: make workspace hook to use service * chore: page and folder hooks use backend services * chore: new folder use backend service * chore: error handler page * chore: try catch hooks to show error page * chore: install i18n package and use flutters i18n files * fix: signin signup margin * chore: fix compile errors * chore: remove unused codes * chore: open workspace after user register * chore: open workspace after user register * chore: add create grid demo * chore: load the cell data * chore: print the cell data * chore: fix project errors * fix: tauri UI issues (#1899) * chore: load workspace items, load folders and pages from workspace, load raw document data, load raw grid data * chore: clear folders and pages before load, new folder event * chore: update folder name backend call * chore: folder expand animation * chore: hide arrow on empty folder * chore: Board page layout, board store, board sample data * chore: board block item * chore: test db id * chore: persist new page, persist page rename, create workspace on read error * chore: boardblockitem details btn * chore: boardblockitem multiselect data and colors * chore: board item drag * chore: drag start on move * chore: remove databaseId * chore: remove databaseId * chore: import service classes into auth hook * chore: sign out option * chore: login page event * chore: signup event * chore: make workspace hook to use service * chore: page and folder hooks use backend services * chore: new folder use backend service * chore: error handler page * chore: try catch hooks to show error page * chore: install i18n package and use flutters i18n files * fix: signin signup margin * fix: new page overflow with folder * fix: sign out button * fix: sign out icon * chore: floating navigation panel * refactor: notify with error * chore: config window size * fix: test demo error * chore: update tests --------- Co-authored-by: Askarbek Zadauly <ascarbek@gmail.com>
This commit is contained in:
parent
085ef8f668
commit
f6957fb160
11
frontend/.vscode/tasks.json
vendored
11
frontend/.vscode/tasks.json
vendored
@ -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",
|
||||
|
@ -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"
|
||||
|
1
frontend/appflowy_tauri/src-tauri/Cargo.lock
generated
1
frontend/appflowy_tauri/src-tauri/Cargo.lock
generated
@ -88,6 +88,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"tauri",
|
||||
"tauri-build",
|
||||
"tauri-utils",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
|
@ -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"] }
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -60,10 +60,11 @@
|
||||
"windows": [
|
||||
{
|
||||
"fullscreen": false,
|
||||
"height": 600,
|
||||
"height": 1000,
|
||||
"resizable": true,
|
||||
"title": "AppFlowy",
|
||||
"width": 800
|
||||
"width": 1200,
|
||||
"transparent": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -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 (
|
||||
<BrowserRouter>
|
||||
<Provider store={store}>
|
||||
<Routes>
|
||||
<Route path={'/'} element={<ProtectedRoutes />}>
|
||||
<Route path={'/page/colors'} element={<TestColors />} />
|
||||
<Route path={'/page/api-test'} element={<TestApiButton />} />
|
||||
<Route path={'/page/api-test'} element={<TestAPI />} />
|
||||
<Route path={'/page/document/:id'} element={<DocumentPage />} />
|
||||
<Route path={'/page/board/:id'} element={<BoardPage />} />
|
||||
<Route path={'/page/grid/:id'} element={<GridPage />} />
|
||||
@ -33,8 +33,8 @@ const App = () => {
|
||||
<Route path={'/auth/login'} element={<LoginPage />}></Route>
|
||||
<Route path={'/auth/signUp'} element={<SignUpPage />}></Route>
|
||||
<Route path={'/auth/confirm-account'} element={<ConfirmAccountPage />}></Route>
|
||||
<Route path={'*'}>Not Found</Route>
|
||||
</Routes>
|
||||
<ErrorHandlerPage></ErrorHandlerPage>
|
||||
</Provider>
|
||||
</BrowserRouter>
|
||||
);
|
||||
|
@ -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<ViewPB> {
|
||||
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<DatabaseController> {
|
||||
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<Option<TextCellController>> {
|
||||
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<Option<NumberCellController>> {
|
||||
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<Option<SelectOptionCellController>> {
|
||||
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<Option<DateCellController>> {
|
||||
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<Option<CellControllerBuilder>> {
|
||||
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;
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
import React from 'react';
|
||||
import TestApiButton from './TestApiButton';
|
||||
import { TestCreateGrid, TestCreateSelectOption, TestEditCell } from './TestGrid';
|
||||
|
||||
export const TestAPI = () => {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<ul className='m-6, space-y-2'>
|
||||
<TestApiButton></TestApiButton>
|
||||
<TestCreateGrid></TestCreateGrid>
|
||||
<TestEditCell></TestEditCell>
|
||||
<TestCreateSelectOption></TestCreateSelectOption>
|
||||
</ul>
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
@ -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 (
|
||||
<>
|
||||
<h1 className='text-3xl'>Welcome to AppFlowy!</h1>
|
||||
|
||||
<div>
|
||||
<button className='rounded-md bg-gray-700 p-4' type='button' onClick={() => sendSignInEvent()}>
|
||||
<button className='rounded-md bg-gray-300 p-4' type='button' onClick={() => sendSignInEvent()}>
|
||||
Sign in and create sample data
|
||||
</button>
|
||||
</div>
|
||||
|
@ -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 (
|
||||
<React.Fragment>
|
||||
<div>
|
||||
<button className='rounded-md bg-gray-300 p-4' type='button' onClick={() => onClick()}>
|
||||
{title}
|
||||
</button>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
@ -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,
|
||||
};
|
||||
};
|
@ -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<IPopupItem>((item) => ({
|
||||
onClick: () => void i18n.changeLanguage(item.key),
|
||||
title: item.title,
|
||||
icon: <></>,
|
||||
}));
|
||||
return (
|
||||
<Popup
|
||||
items={items}
|
||||
className={'absolute top-full right-0 z-10 w-[200px]'}
|
||||
onOutsideClick={onClose}
|
||||
columns={2}
|
||||
></Popup>
|
||||
);
|
||||
};
|
@ -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<HTMLDivElement>(null);
|
||||
useOutsideClick(ref, () => onOutsideClick && onOutsideClick());
|
||||
@ -26,16 +28,22 @@ export const Popup = ({
|
||||
|
||||
return (
|
||||
<div ref={ref} className={`${className} rounded-lg bg-white px-2 py-2 shadow-md`}>
|
||||
{items.map((item, index) => (
|
||||
<button
|
||||
key={index}
|
||||
className={'flex w-full cursor-pointer items-center rounded-lg px-2 py-2 hover:bg-main-secondary'}
|
||||
onClick={(e) => handleClick(e, item)}
|
||||
>
|
||||
{item.icon}
|
||||
<span className={'ml-2'}>{item.title}</span>
|
||||
</button>
|
||||
))}
|
||||
<div
|
||||
className={`grid ${columns === 1 && 'grid-cols-1'} ${columns === 2 && 'grid-cols-2'} ${
|
||||
columns === 3 && 'grid-cols-3'
|
||||
} gap-x-4`}
|
||||
>
|
||||
{items.map((item, index) => (
|
||||
<button
|
||||
key={index}
|
||||
className={'flex cursor-pointer items-center gap-2 rounded-lg px-2 py-2 hover:bg-main-secondary'}
|
||||
onClick={(e) => handleClick(e, item)}
|
||||
>
|
||||
{item.icon}
|
||||
<span className={'flex-shrink-0'}>{item.title}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -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 '';
|
||||
}
|
||||
};
|
@ -0,0 +1,16 @@
|
||||
export const CloseSvg = () => {
|
||||
return (
|
||||
<svg width='100%' height='100%' viewBox='0 0 32 32' fill='none' xmlns='http://www.w3.org/2000/svg'>
|
||||
<rect
|
||||
x='20.9497'
|
||||
y='9.63599'
|
||||
width='2'
|
||||
height='16'
|
||||
rx='1'
|
||||
transform='rotate(45 20.9497 9.63599)'
|
||||
fill='currentColor'
|
||||
/>
|
||||
<rect x='22.364' y='20.95' width='2' height='16' rx='1' transform='rotate(135 22.364 20.95)' fill='currentColor' />
|
||||
</svg>
|
||||
);
|
||||
};
|
@ -0,0 +1,10 @@
|
||||
export const DropDownShowSvg = () => {
|
||||
return (
|
||||
<svg width='100%' height='100%' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'>
|
||||
<path
|
||||
d='M11.5757 14.5757L8.52426 11.5243C8.14629 11.1463 8.41399 10.5 8.94853 10.5H15.0515C15.586 10.5 15.8537 11.1463 15.4757 11.5243L12.4243 14.5757C12.1899 14.8101 11.8101 14.8101 11.5757 14.5757Z'
|
||||
fill='currentColor'
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
@ -0,0 +1,21 @@
|
||||
export const EarthSvg = () => {
|
||||
return (
|
||||
<svg width='100%' height='100%' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'>
|
||||
<path
|
||||
d='M12 19C15.866 19 19 15.866 19 12C19 8.13401 15.866 5 12 5C8.13401 5 5 8.13401 5 12C5 15.866 8.13401 19 12 19Z'
|
||||
stroke='currentColor'
|
||||
strokeWidth='1.5'
|
||||
strokeLinecap='round'
|
||||
strokeLinejoin='round'
|
||||
/>
|
||||
<path d='M5 12H19' stroke='currentColor' strokeWidth='1.5' strokeLinecap='round' strokeLinejoin='round' />
|
||||
<path
|
||||
d='M12.0002 5C13.7511 6.91685 14.7461 9.40442 14.8002 12C14.7461 14.5956 13.7511 17.0832 12.0002 19C10.2493 17.0832 9.25427 14.5956 9.2002 12C9.25427 9.40442 10.2493 6.91685 12.0002 5V5Z'
|
||||
stroke='currentColor'
|
||||
strokeWidth='1.5'
|
||||
strokeLinecap='round'
|
||||
strokeLinejoin='round'
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
@ -2,12 +2,12 @@ export const EyeClosed = () => {
|
||||
return (
|
||||
<svg width='100%' height='100%' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'>
|
||||
<path
|
||||
fill-rule='evenodd'
|
||||
clip-rule='evenodd'
|
||||
fillRule='evenodd'
|
||||
clipRule='evenodd'
|
||||
d='M7.68372 17.3771C8.88359 18.1747 10.3278 18.75 12.0001 18.75C15.7261 18.75 18.32 15.8941 19.6011 14.0863C20.4933 12.8272 20.4933 11.1728 19.6011 9.91375C19.1009 9.208 18.4007 8.34252 17.5112 7.54957L16.4489 8.61191C17.236 9.30133 17.8836 10.0844 18.3772 10.781C18.9013 11.5205 18.9013 12.4795 18.3772 13.219C17.1411 14.9633 14.9396 17.25 12.0001 17.25C10.794 17.25 9.71218 16.865 8.77028 16.2905L7.68372 17.3771ZM7.55137 15.3881L6.48903 16.4504C5.5995 15.6575 4.8993 14.792 4.39916 14.0863C3.50692 12.8272 3.50692 11.1728 4.39916 9.91375C5.68028 8.10595 8.27417 5.25 12.0001 5.25C13.6724 5.25 15.1167 5.82531 16.3165 6.62294L15.23 7.7095C14.2881 7.13497 13.2062 6.75 12.0001 6.75C9.06064 6.75 6.85914 9.03672 5.62301 10.781C5.09897 11.5205 5.09897 12.4795 5.62301 13.219C6.11667 13.9156 6.76428 14.6987 7.55137 15.3881ZM10.4887 14.572C10.9279 14.8431 11.4439 15 12.0002 15C13.641 15 14.932 13.6349 14.932 12C14.932 11.4625 14.7925 10.9542 14.5468 10.5139L13.3964 11.6644C13.4197 11.7717 13.432 11.884 13.432 12C13.432 12.8503 12.7694 13.5 12.0002 13.5C11.868 13.5 11.739 13.4808 11.616 13.4448L10.4887 14.572ZM10.6039 12.3355L9.45347 13.486C9.20788 13.0458 9.06836 12.5375 9.06836 12C9.06836 10.3651 10.3594 9 12.0002 9C12.5564 9 13.0724 9.15686 13.5115 9.42792L12.3842 10.5552C12.2612 10.5192 12.1323 10.5 12.0002 10.5C11.231 10.5 10.5684 11.1497 10.5684 12C10.5684 12.116 10.5807 12.2282 10.6039 12.3355Z'
|
||||
fill='#333333'
|
||||
fill='currentColor'
|
||||
/>
|
||||
<path d='M17.5 5L5 17.5' stroke='#333333' strokeWidth='1.5' strokeLinecap='round' />
|
||||
<path d='M17.5 5L5 17.5' stroke='currentColor' strokeWidth='1.5' strokeLinecap='round' />
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
@ -3,14 +3,14 @@ export const EyeOpened = () => {
|
||||
<svg width='100%' height='100%' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'>
|
||||
<path
|
||||
d='M5.01097 13.6526C4.30282 12.6533 4.30282 11.3467 5.01097 10.3474C6.26959 8.57133 8.66728 6 12 6C15.3327 6 17.7304 8.57133 18.989 10.3474C19.6972 11.3467 19.6972 12.6533 18.989 13.6526C17.7304 15.4287 15.3327 18 12 18C8.66728 18 6.26959 15.4287 5.01097 13.6526Z'
|
||||
stroke='#333333'
|
||||
stroke='currentColor'
|
||||
strokeWidth='1.5'
|
||||
strokeLinecap='round'
|
||||
strokeLinejoin='round'
|
||||
/>
|
||||
<path
|
||||
d='M11.9999 14.25C13.2049 14.25 14.1818 13.2426 14.1818 12C14.1818 10.7574 13.2049 9.75 11.9999 9.75C10.7949 9.75 9.81812 10.7574 9.81812 12C9.81812 13.2426 10.7949 14.25 11.9999 14.25Z'
|
||||
stroke='#333333'
|
||||
stroke='currentColor'
|
||||
strokeWidth='1.5'
|
||||
strokeLinecap='round'
|
||||
strokeLinejoin='round'
|
||||
|
@ -0,0 +1,10 @@
|
||||
export const HideMenuSvg = () => {
|
||||
return (
|
||||
<svg width='100%' height='100%' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'>
|
||||
<path d='M9 9L6 12L9 15' stroke='currentColor' strokeWidth='1.5' strokeLinecap='round' strokeLinejoin='round' />
|
||||
<rect width='5' height='1.5' rx='0.75' transform='matrix(-1 0 0 1 18 8.25)' fill='currentColor' />
|
||||
<rect width='7' height='1.5' rx='0.75' transform='matrix(-1 0 0 1 18 11.25)' fill='currentColor' />
|
||||
<rect width='5' height='1.5' rx='0.75' transform='matrix(-1 0 0 1 18 14.25)' fill='currentColor' />
|
||||
</svg>
|
||||
);
|
||||
};
|
@ -0,0 +1,14 @@
|
||||
export const InformationSvg = () => {
|
||||
return (
|
||||
<svg width='100%' height='100%' viewBox='0 0 16 17' fill='none' xmlns='http://www.w3.org/2000/svg'>
|
||||
<path
|
||||
d='M8 14.0039C11.3137 14.0039 14 11.3176 14 8.00391C14 4.6902 11.3137 2.00391 8 2.00391C4.68629 2.00391 2 4.6902 2 8.00391C2 11.3176 4.68629 14.0039 8 14.0039Z'
|
||||
stroke='currentColor'
|
||||
strokeLinecap='round'
|
||||
strokeLinejoin='round'
|
||||
/>
|
||||
<rect x='7.5' y='7.00391' width='1' height='4' rx='0.5' fill='currentColor' />
|
||||
<rect x='7.5' y='5.00391' width='1' height='1' rx='0.5' fill='currentColor' />
|
||||
</svg>
|
||||
);
|
||||
};
|
@ -0,0 +1,14 @@
|
||||
export const LogoutSvg = () => {
|
||||
return (
|
||||
<svg width='100%' height='100%' viewBox='0 0 16 16' fill='none' xmlns='http://www.w3.org/2000/svg'>
|
||||
<path
|
||||
d='M6 13H4C3.73478 13 3.48043 12.8829 3.29289 12.6746C3.10536 12.4662 3 12.1836 3 11.8889V4.11111C3 3.81643 3.10536 3.53381 3.29289 3.32544C3.48043 3.11706 3.73478 3 4 3H6'
|
||||
stroke='currentColor'
|
||||
strokeLinecap='round'
|
||||
strokeLinejoin='round'
|
||||
/>
|
||||
<path d='M10 11L13 8L10 5' stroke='currentColor' strokeLinecap='round' strokeLinejoin='round' />
|
||||
<path d='M13 8L7 8' stroke='currentColor' strokeLinecap='round' strokeLinejoin='round' />
|
||||
</svg>
|
||||
);
|
||||
};
|
@ -0,0 +1,10 @@
|
||||
export const ShowMenuSvg = () => {
|
||||
return (
|
||||
<svg width='100%' height='100%' viewBox='0 0 16 16' fill='none' xmlns='http://www.w3.org/2000/svg'>
|
||||
<path d='M10 5L13 8L10 11' stroke='currentColor' strokeLinecap='round' strokeLinejoin='round' />
|
||||
<rect x='3' y='5' width='4' height='1' rx='0.5' fill='currentColor' />
|
||||
<rect x='3' y='7.5' width='6' height='1' rx='0.5' fill='currentColor' />
|
||||
<rect x='3' y='10' width='4' height='1' rx='0.5' fill='currentColor' />
|
||||
</svg>
|
||||
);
|
||||
};
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
@ -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 (
|
||||
<form onSubmit={(e) => e.preventDefault()} method='POST'>
|
||||
<div className='flex h-screen w-full flex-col items-center justify-center gap-12 text-center'>
|
||||
<div className='flex h-10 w-10 justify-center'>
|
||||
<AppflowyLogo />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<span className='text-2xl font-semibold leading-9'>Login to Appflowy</span>
|
||||
</div>
|
||||
|
||||
<div className='flex w-full max-w-[340px] flex-col gap-6 '>
|
||||
<input type='text' className='input w-full' placeholder='Phone / Email' />
|
||||
<div className='relative w-full'>
|
||||
<input type={showPassword ? 'text' : 'password'} className='input w-full !pr-10' placeholder='Password' />
|
||||
|
||||
{/* Show password button */}
|
||||
<button
|
||||
type='button'
|
||||
className='absolute right-0 top-0 flex h-full w-12 items-center justify-center '
|
||||
onClick={onTogglePassword}
|
||||
>
|
||||
<span className='h-6 w-6'>{showPassword ? <EyeClosed /> : <EyeOpened />}</span>
|
||||
</button>
|
||||
<>
|
||||
<form onSubmit={(e) => e.preventDefault()} method='POST'>
|
||||
<div className='relative flex h-screen w-screen flex-col items-center justify-center gap-12 text-center'>
|
||||
<div className='flex h-10 w-10 justify-center'>
|
||||
<AppflowyLogo />
|
||||
</div>
|
||||
|
||||
<div className='flex justify-center'>
|
||||
{/* Forget password link */}
|
||||
<Link to={'/auth/confirm-account'}>
|
||||
<span className='text-xs text-main-accent hover:text-main-hovered'>Forgot password?</span>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='flex w-full max-w-[340px] flex-col gap-6 '>
|
||||
<Button size={'primary'} onClick={() => onSignInClick()}>
|
||||
Login
|
||||
</Button>
|
||||
|
||||
{/* signup link */}
|
||||
<div className='flex justify-center'>
|
||||
<span className='text-xs text-gray-400'>
|
||||
Don't have an account?
|
||||
<Link to={'/auth/signUp'}>
|
||||
<span className='text-main-accent hover:text-main-hovered'> Sign up</span>
|
||||
</Link>
|
||||
<div>
|
||||
<span className='text-2xl font-semibold leading-9'>
|
||||
{t('signIn.loginTitle').replace('@:appName', 'AppFlowy')}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className='flex w-full max-w-[340px] flex-col gap-6 '>
|
||||
<input
|
||||
type='text'
|
||||
className={`input w-full ${authError && 'error'}`}
|
||||
placeholder={t('signIn.emailHint') || ''}
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
/>
|
||||
<div className='relative w-full'>
|
||||
{/* Password input field */}
|
||||
|
||||
<input
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
className={`input w-full !pr-10 ${authError && 'error'}`}
|
||||
placeholder={t('signIn.passwordHint') || ''}
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
/>
|
||||
|
||||
{/* Show password button */}
|
||||
<button
|
||||
type='button'
|
||||
className='absolute right-0 top-0 flex h-full w-12 items-center justify-center '
|
||||
onClick={onTogglePassword}
|
||||
>
|
||||
<span className='h-6 w-6'>{showPassword ? <EyeClosed /> : <EyeOpened />}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className='flex justify-center'>
|
||||
{/* Forget password link */}
|
||||
<Link to={'/auth/confirm-account'}>
|
||||
<span className='text-xs text-main-accent hover:text-main-hovered'>{t('signIn.forgotPassword')}</span>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='flex w-full max-w-[340px] flex-col gap-6 '>
|
||||
<Button size={'primary'} onClick={() => onSignInClick()}>
|
||||
{t('signIn.loginButtonText')}
|
||||
</Button>
|
||||
|
||||
{/* signup link */}
|
||||
<div className='flex justify-center'>
|
||||
<span className='text-xs text-gray-400'>
|
||||
{t('signIn.dontHaveAnAccount')}
|
||||
<Link to={'/auth/signUp'}>
|
||||
<span className='ml-2 text-main-accent hover:text-main-hovered'>{t('signUp.buttonText')}</span>
|
||||
</Link>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={'absolute right-0 top-0 px-12 py-8'}>
|
||||
<div className={'relative h-full w-full'}>
|
||||
<button className={'h-8 w-8 text-shade-3 hover:text-black'} onClick={() => setShowLanguagePopup(true)}>
|
||||
<EarthSvg></EarthSvg>
|
||||
</button>
|
||||
{showLanguagePopup && (
|
||||
<LanguageSelectPopup onClose={() => setShowLanguagePopup(false)}></LanguageSelectPopup>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</form>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
@ -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 (
|
||||
<form method='POST' onSubmit={(e) => e.preventDefault()}>
|
||||
<div className='flex h-screen w-full flex-col items-center justify-center gap-12 text-center'>
|
||||
<div className='relative flex h-screen w-full flex-col items-center justify-center gap-12 text-center'>
|
||||
<div className='flex h-10 w-10 justify-center'>
|
||||
<AppflowyLogo />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<span className='text-2xl font-semibold'>Sign up to Appflowy</span>
|
||||
<span className='text-2xl font-semibold'>{t('signUp.title').replace('@:appName', 'AppFlowy')}</span>
|
||||
</div>
|
||||
|
||||
<div className='flex w-full max-w-[340px] flex-col gap-6'>
|
||||
<input type='text' className='input w-full' placeholder='Phone / Email' />
|
||||
<input
|
||||
type='text'
|
||||
className={`input w-full ${authError && 'error'}`}
|
||||
placeholder={t('signUp.emailHint') || ''}
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
/>
|
||||
{/* new user should enter his name, need translation for this field */}
|
||||
<input
|
||||
type='text'
|
||||
className={`input w-full ${authError && 'error'}`}
|
||||
placeholder='Name'
|
||||
value={displayName}
|
||||
onChange={(e) => setDisplayName(e.target.value)}
|
||||
/>
|
||||
<div className='relative w-full'>
|
||||
<input type={showPassword ? 'text' : 'password'} className='input w-full !pr-10' placeholder='Password' />
|
||||
<input
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
className={`input w-full !pr-10 ${authError && 'error'}`}
|
||||
placeholder={t('signUp.passwordHint') || ''}
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
/>
|
||||
|
||||
<button
|
||||
className='absolute right-0 top-0 flex h-full w-12 items-center justify-center '
|
||||
@ -37,8 +78,10 @@ export const SignUp = () => {
|
||||
<div className='relative w-full'>
|
||||
<input
|
||||
type={showConfirmPassword ? 'text' : 'password'}
|
||||
className='input w-full !pr-10'
|
||||
placeholder='Repeat Password'
|
||||
className={`input w-full !pr-10 ${authError && 'error'}`}
|
||||
placeholder={t('signUp.repeatPasswordHint') || ''}
|
||||
value={repeatedPassword}
|
||||
onChange={(e) => setRepeatedPassword(e.target.value)}
|
||||
/>
|
||||
|
||||
<button
|
||||
@ -53,19 +96,30 @@ export const SignUp = () => {
|
||||
|
||||
<div className='flex w-full max-w-[340px] flex-col gap-6 '>
|
||||
<Button size={'primary'} onClick={() => onSignUpClick()}>
|
||||
Get Started
|
||||
{t('signUp.getStartedText')}
|
||||
</Button>
|
||||
|
||||
{/* signup link */}
|
||||
<div className='flex justify-center'>
|
||||
<span className='text-xs text-gray-500'>
|
||||
Already have an account?
|
||||
{t('signUp.alreadyHaveAnAccount')}
|
||||
<Link to={'/auth/login'}>
|
||||
<span className=' text-main-accent hover:text-main-hovered'> Sign in</span>
|
||||
<span className='ml-2 text-main-accent hover:text-main-hovered'>{t('signIn.buttonText')}</span>
|
||||
</Link>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={'absolute right-0 top-0 px-12 py-8'}>
|
||||
<div className={'relative h-full w-full'}>
|
||||
<button className={'h-8 w-8 text-shade-3 hover:text-black'} onClick={() => setShowLanguagePopup(true)}>
|
||||
<EarthSvg></EarthSvg>
|
||||
</button>
|
||||
{showLanguagePopup && (
|
||||
<LanguageSelectPopup onClose={() => setShowLanguagePopup(false)}></LanguageSelectPopup>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
|
@ -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<UserProfilePB> {
|
||||
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<UserProfilePB> {
|
||||
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 };
|
||||
};
|
||||
|
@ -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<string | undefined>(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,
|
||||
};
|
||||
};
|
@ -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 (
|
||||
<>
|
||||
<div className='flex w-full items-center justify-between'>
|
||||
<div className={'flex items-center text-xl font-semibold'}>
|
||||
<div>{title}</div>
|
||||
<button className={'ml-2 h-5 w-5'}>
|
||||
<SettingsSvg></SettingsSvg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className='flex shrink-0 items-center gap-4'>
|
||||
<SearchInput />
|
||||
</div>
|
||||
</div>
|
||||
<div className={'relative w-full flex-1 overflow-auto'}>
|
||||
<div className={'absolute flex h-full flex-shrink-0 items-start justify-start gap-4'}>
|
||||
{database &&
|
||||
boardColumns?.map((column, index) => (
|
||||
<BoardBlock
|
||||
key={index}
|
||||
title={column.title}
|
||||
groupingFieldId={groupingFieldId}
|
||||
count={column.rows.length}
|
||||
fields={database.fields}
|
||||
columns={database.columns}
|
||||
rows={column.rows}
|
||||
startMove={startMove}
|
||||
endMove={endMove}
|
||||
/>
|
||||
))}
|
||||
|
||||
<NewBoardBlock onClick={() => console.log('new block')}></NewBoardBlock>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
@ -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 (
|
||||
<div className={'flex h-full w-[250px] flex-col rounded-lg bg-surface-1'}>
|
||||
<div className={'flex items-center justify-between p-4'}>
|
||||
<div className={'flex items-center gap-2'}>
|
||||
<span>{title}</span>
|
||||
<span className={'text-shade-4'}>({count})</span>
|
||||
</div>
|
||||
<div className={'flex items-center gap-2'}>
|
||||
<button className={'h-5 w-5 rounded hover:bg-surface-2'}>
|
||||
<Details2Svg></Details2Svg>
|
||||
</button>
|
||||
<button className={'h-5 w-5 rounded hover:bg-surface-2'}>
|
||||
<AddSvg></AddSvg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className={'flex flex-1 flex-col gap-1 overflow-auto px-2'}>
|
||||
{rows.map((row, index) => (
|
||||
<BoardBlockItem
|
||||
key={index}
|
||||
groupingFieldId={groupingFieldId}
|
||||
fields={fields}
|
||||
columns={columns}
|
||||
row={row}
|
||||
startMove={() => startMove(row.rowId)}
|
||||
endMove={() => endMove()}
|
||||
></BoardBlockItem>
|
||||
))}
|
||||
</div>
|
||||
<div className={'p-2'}>
|
||||
<button className={'flex w-full items-center gap-2 rounded-lg px-2 py-2 hover:bg-surface-2'}>
|
||||
<span className={'h-5 w-5'}>
|
||||
<AddSvg></AddSvg>
|
||||
</span>
|
||||
<span>New</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -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<HTMLDivElement>(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<HTMLDivElement> = (e) => {
|
||||
setGhostLeft(ghostLeft + e.movementX);
|
||||
setGhostTop(ghostTop + e.movementY);
|
||||
};
|
||||
|
||||
const onMouseUp: MouseEventHandler<HTMLDivElement> = (e) => {
|
||||
setIsMoving(false);
|
||||
endMove();
|
||||
};
|
||||
|
||||
const dragStart = () => {
|
||||
if (isDown) {
|
||||
setIsMoving(true);
|
||||
setIsDown(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
ref={el}
|
||||
onMouseDown={() => 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 `}
|
||||
>
|
||||
<button className={'absolute right-4 top-2.5 h-5 w-5 rounded hover:bg-surface-2'}>
|
||||
<Details2Svg></Details2Svg>
|
||||
</button>
|
||||
<div className={'flex flex-col gap-3'}>
|
||||
{columns
|
||||
.filter((column) => column.fieldId !== groupingFieldId)
|
||||
.map((column, index) => {
|
||||
switch (fields[column.fieldId].fieldType) {
|
||||
case FieldType.MultiSelect:
|
||||
return (
|
||||
<div key={index} className={'flex flex-wrap items-center gap-2'}>
|
||||
{row.cells[column.fieldId].optionIds?.map((option, indexOption) => {
|
||||
const selectOptions = fields[column.fieldId].fieldOptions.selectOptions;
|
||||
const selectedOption = selectOptions?.find((so) => so.selectOptionId === option);
|
||||
return (
|
||||
<div
|
||||
key={indexOption}
|
||||
className={`rounded px-1 py-0.5 text-sm ${getBgColor(selectedOption?.color)}`}
|
||||
>
|
||||
{selectedOption?.title}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
default:
|
||||
return <div key={index}>{row.cells[column.fieldId].data}</div>;
|
||||
}
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
{isMoving && (
|
||||
<div
|
||||
onMouseMove={onMouseMove}
|
||||
onMouseUp={onMouseUp}
|
||||
onMouseLeave={onMouseUp}
|
||||
id={'ghost-block'}
|
||||
className={
|
||||
'fixed z-10 rotate-6 scale-105 cursor-pointer select-none rounded-lg border border-shade-6 bg-white px-3 py-2'
|
||||
}
|
||||
style={{
|
||||
width: `${ghostWidth}px`,
|
||||
height: `${ghostHeight}px`,
|
||||
left: `${ghostLeft}px`,
|
||||
top: `${ghostTop}px`,
|
||||
}}
|
||||
>
|
||||
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
@ -0,0 +1,14 @@
|
||||
import AddSvg from '../_shared/svg/AddSvg';
|
||||
|
||||
export const NewBoardBlock = ({ onClick }: { onClick: () => void }) => {
|
||||
return (
|
||||
<div className={'w-[250px]'}>
|
||||
<button onClick={onClick} className={'flex w-full items-center gap-2 rounded-lg px-4 py-2 hover:bg-surface-2'}>
|
||||
<span className={'h-5 w-5'}>
|
||||
<AddSvg></AddSvg>
|
||||
</span>
|
||||
<span>Add Block</span>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -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,
|
||||
};
|
||||
};
|
@ -0,0 +1,8 @@
|
||||
import { useError } from './Error.hooks';
|
||||
import { ErrorModal } from './ErrorModal';
|
||||
|
||||
export const ErrorHandlerPage = () => {
|
||||
const { hideError, errorMessage, displayError } = useError();
|
||||
|
||||
return displayError ? <ErrorModal message={errorMessage} onClose={hideError}></ErrorModal> : <></>;
|
||||
};
|
@ -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 (
|
||||
<div className={'fixed inset-0 z-20 flex items-center justify-center bg-white/30 backdrop-blur-sm'}>
|
||||
<div
|
||||
className={
|
||||
'relative flex flex-col items-center gap-8 rounded-xl border border-shade-5 bg-white px-16 py-8 shadow-md'
|
||||
}
|
||||
>
|
||||
<button
|
||||
onClick={() => onClose()}
|
||||
className={'absolute right-0 top-0 z-20 px-2 py-2 text-shade-5 hover:text-black'}
|
||||
>
|
||||
<i className={'block h-8 w-8'}>
|
||||
<CloseSvg></CloseSvg>
|
||||
</i>
|
||||
</button>
|
||||
<div className={'h-24 w-24 text-main-alert'}>
|
||||
<InformationSvg></InformationSvg>
|
||||
</div>
|
||||
<h1 className={'text-xl'}>Oops.. something went wrong</h1>
|
||||
<h2>{message}</h2>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -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 (
|
||||
<div className={'mb-2 flex h-[60px] items-center justify-between px-6'}>
|
||||
<img src={'/images/flowy_logo_with_text.svg'} alt={'logo'} />
|
||||
<img src={'/images/home/hide_menu.svg'} alt={'hide'} />
|
||||
{iconToShow === 'hide' && (
|
||||
<button onClick={onHideMenuClick} className={'h-5 w-5'}>
|
||||
<i>
|
||||
<HideMenuSvg></HideMenuSvg>
|
||||
</i>
|
||||
</button>
|
||||
)}
|
||||
{iconToShow === 'show' && (
|
||||
<button onClick={onShowMenuClick} className={'h-5 w-5'}>
|
||||
<i>
|
||||
<ShowMenuSvg></ShowMenuSvg>
|
||||
</i>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -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: (
|
||||
<i className={'block h-5 w-5 flex-shrink-0'}>
|
||||
<LogoutSvg></LogoutSvg>
|
||||
</i>
|
||||
),
|
||||
onClick: onSignOutClick,
|
||||
},
|
||||
];
|
||||
return (
|
||||
<Popup
|
||||
className={'absolute top-[50px] right-[30px] z-10 whitespace-nowrap'}
|
||||
items={items}
|
||||
onOutsideClick={onClose}
|
||||
></Popup>
|
||||
);
|
||||
};
|
@ -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,
|
||||
};
|
||||
};
|
@ -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 (
|
||||
<div className={'flex items-center'}>
|
||||
<Button size={'small'} onClick={() => console.log('share click')}>
|
||||
Share
|
||||
</Button>
|
||||
const { showOptionsPopup, onOptionsClick, onClose, onSignOutClick } = usePageOptions();
|
||||
|
||||
<button className={'ml-8'}>
|
||||
<img className={'h-8 w-8'} src={`/images/editor/details.svg`} />
|
||||
</button>
|
||||
</div>
|
||||
return (
|
||||
<>
|
||||
<div className={'relative flex items-center gap-4'}>
|
||||
<Button size={'small'} onClick={() => console.log('share click')}>
|
||||
Share
|
||||
</Button>
|
||||
|
||||
<button className={'relative h-8 w-8'} onClick={onOptionsClick}>
|
||||
<Details2Svg></Details2Svg>
|
||||
</button>
|
||||
</div>
|
||||
{showOptionsPopup && <OptionsPopup onSignOutClick={onSignOutClick} onClose={onClose}></OptionsPopup>}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
@ -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 (
|
||||
<div className={'relative my-2'}>
|
||||
/*transitionTimingFunction:'cubic-bezier(.36,1.55,.65,1.1)'*/
|
||||
<div className={'relative'}>
|
||||
<div
|
||||
onClick={() => 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` }}
|
||||
>
|
||||
<div className={'flex min-w-0 flex-1 items-center'}>
|
||||
<div className={`mr-2 transition-transform duration-500 ${showPages && 'rotate-180'}`}>
|
||||
<img className={''} src={'/images/home/drop_down_show.svg'} alt={''} />
|
||||
<div
|
||||
onClick={() => onFolderNameClick()}
|
||||
className={'flex cursor-pointer items-center justify-between rounded-lg px-4 py-2 hover:bg-surface-2'}
|
||||
>
|
||||
<button className={'flex min-w-0 flex-1 items-center'}>
|
||||
<i className={`mr-2 h-5 w-5 transition-transform duration-500 ${showPages && 'rotate-180'}`}>
|
||||
{pages.length > 0 && <DropDownShowSvg></DropDownShowSvg>}
|
||||
</i>
|
||||
<span className={'min-w-0 flex-1 overflow-hidden overflow-ellipsis whitespace-nowrap text-left'}>
|
||||
{folder.title}
|
||||
</span>
|
||||
</button>
|
||||
<div className={'relative flex items-center'}>
|
||||
<Button size={'box-small-transparent'} onClick={() => onFolderOptionsClick()}>
|
||||
<Details2Svg></Details2Svg>
|
||||
</Button>
|
||||
<Button size={'box-small-transparent'} onClick={() => onNewPageClick()}>
|
||||
<AddSvg></AddSvg>
|
||||
</Button>
|
||||
</div>
|
||||
<span className={'min-w-0 flex-1 overflow-hidden overflow-ellipsis whitespace-nowrap'}>{folder.title}</span>
|
||||
</div>
|
||||
<div className={'relative flex items-center'}>
|
||||
<Button size={'box-small-transparent'} onClick={() => onFolderOptionsClick()}>
|
||||
<Details2Svg></Details2Svg>
|
||||
</Button>
|
||||
<Button size={'box-small-transparent'} onClick={() => onNewPageClick()}>
|
||||
<AddSvg></AddSvg>
|
||||
</Button>
|
||||
|
||||
{showFolderOptions && (
|
||||
<NavItemOptionsPopup
|
||||
onRenameClick={() => startFolderRename()}
|
||||
onDeleteClick={() => deleteFolder()}
|
||||
onDuplicateClick={() => duplicateFolder()}
|
||||
onClose={() => closePopup()}
|
||||
></NavItemOptionsPopup>
|
||||
)}
|
||||
{showNewPageOptions && (
|
||||
<NewPagePopup
|
||||
onDocumentClick={() => onAddNewDocumentPage()}
|
||||
onBoardClick={() => onAddNewBoardPage()}
|
||||
onGridClick={() => onAddNewGridPage()}
|
||||
onClose={() => closePopup()}
|
||||
></NewPagePopup>
|
||||
)}
|
||||
</div>
|
||||
{pages.map((page, index) => (
|
||||
<PageItem key={index} page={page} onPageClick={() => onPageClick(page)}></PageItem>
|
||||
))}
|
||||
</div>
|
||||
{showFolderOptions && (
|
||||
<NavItemOptionsPopup
|
||||
onRenameClick={() => startFolderRename()}
|
||||
onDeleteClick={() => deleteFolder()}
|
||||
onDuplicateClick={() => duplicateFolder()}
|
||||
onClose={() => closePopup()}
|
||||
></NavItemOptionsPopup>
|
||||
)}
|
||||
{showNewPageOptions && (
|
||||
<NewPagePopup
|
||||
onDocumentClick={() => onAddNewDocumentPage()}
|
||||
onBoardClick={() => onAddNewBoardPage()}
|
||||
onGridClick={() => onAddNewGridPage()}
|
||||
onClose={() => closePopup()}
|
||||
></NewPagePopup>
|
||||
)}
|
||||
{showRenamePopup && (
|
||||
<RenamePopup
|
||||
value={folder.title}
|
||||
@ -85,8 +115,6 @@ export const FolderItem = ({
|
||||
onClose={closeRenamePopup}
|
||||
></RenamePopup>
|
||||
)}
|
||||
{showPages &&
|
||||
pages.map((page, index) => <PageItem key={index} page={page} onPageClick={() => onPageClick(page)}></PageItem>)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -48,7 +48,7 @@ export const NavItemOptionsPopup = ({
|
||||
<Popup
|
||||
onOutsideClick={() => onClose && onClose()}
|
||||
items={items}
|
||||
className={'absolute right-0 top-full z-10'}
|
||||
className={'absolute right-0 top-[40px] z-10'}
|
||||
></Popup>
|
||||
);
|
||||
};
|
||||
|
@ -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<HTMLDivElement>(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 (
|
||||
<div
|
||||
ref={el}
|
||||
className={
|
||||
'fixed top-16 z-10 flex flex-col justify-between rounded-tr rounded-br border border-l-0 border-shade-4 bg-white text-sm shadow-md transition-all'
|
||||
}
|
||||
style={{ left: panelLeft, transitionDuration: `${animationDuration}ms` }}
|
||||
>
|
||||
<div className={'flex flex-col'}>
|
||||
<AppLogo iconToShow={'show'} onShowMenuClick={onFixNavigationClick}></AppLogo>
|
||||
|
||||
<Workspace></Workspace>
|
||||
|
||||
<div className={'flex flex-col px-2'}>
|
||||
{folders.map((folder, index) => (
|
||||
<FolderItem
|
||||
key={index}
|
||||
folder={folder}
|
||||
pages={pages.filter((page) => page.folderId === folder.id)}
|
||||
onPageClick={onPageClick}
|
||||
></FolderItem>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={'flex flex-col'}>
|
||||
<div className={'border-b border-shade-6 px-2 pb-4'}>
|
||||
<PluginsButton></PluginsButton>
|
||||
<TrashButton></TrashButton>
|
||||
</div>
|
||||
|
||||
<NewFolderButton></NewFolderButton>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -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<HTMLDivElement> = (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,
|
||||
};
|
||||
};
|
||||
|
@ -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 (
|
||||
<>
|
||||
<div className={'flex flex-col justify-between bg-surface-1 text-sm'} style={{ width: `${width}px` }}>
|
||||
<div className={'flex flex-col'}>
|
||||
<AppLogo></AppLogo>
|
||||
<AppLogo iconToShow={'hide'} onHideMenuClick={onCollapseNavigationClick}></AppLogo>
|
||||
|
||||
<Workspace></Workspace>
|
||||
|
||||
@ -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}
|
||||
></FolderItem>
|
||||
))}
|
||||
</div>
|
||||
|
@ -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 {
|
||||
|
@ -48,7 +48,7 @@ export const NewPagePopup = ({
|
||||
<Popup
|
||||
onOutsideClick={() => onClose && onClose()}
|
||||
items={items}
|
||||
className={'absolute right-0 top-full z-10'}
|
||||
className={'absolute right-0 top-[40px] z-10'}
|
||||
></Popup>
|
||||
);
|
||||
};
|
||||
|
@ -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 = () => {
|
||||
|
@ -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 '}
|
||||
>
|
||||
<div className={'flex min-w-0 flex-1 items-center'}>
|
||||
<div className={'ml-1 mr-1 h-[16px] w-[16px]'}>
|
||||
<button className={'flex min-w-0 flex-1 items-center'}>
|
||||
<i className={'ml-1 mr-1 h-[16px] w-[16px]'}>
|
||||
{page.pageType === ViewLayoutTypePB.Document && <DocumentSvg></DocumentSvg>}
|
||||
{page.pageType === ViewLayoutTypePB.Board && <BoardSvg></BoardSvg>}
|
||||
{page.pageType === ViewLayoutTypePB.Grid && <GridSvg></GridSvg>}
|
||||
</div>
|
||||
<span className={'ml-2 min-w-0 flex-1 overflow-hidden overflow-ellipsis whitespace-nowrap'}>{page.title}</span>
|
||||
</div>
|
||||
</i>
|
||||
<span className={'ml-2 min-w-0 flex-1 overflow-hidden overflow-ellipsis whitespace-nowrap text-left'}>
|
||||
{page.title}
|
||||
</span>
|
||||
</button>
|
||||
<div className={'relative flex items-center'}>
|
||||
<Button size={'box-small-transparent'} onClick={() => onPageOptionsClick()}>
|
||||
<Details2Svg></Details2Svg>
|
||||
</Button>
|
||||
{showPageOptions && (
|
||||
<NavItemOptionsPopup
|
||||
onRenameClick={() => startPageRename()}
|
||||
onDeleteClick={() => deletePage()}
|
||||
onDuplicateClick={() => duplicatePage()}
|
||||
onClose={() => closePopup()}
|
||||
></NavItemOptionsPopup>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{showPageOptions && (
|
||||
<NavItemOptionsPopup
|
||||
onRenameClick={() => startPageRename()}
|
||||
onDeleteClick={() => deletePage()}
|
||||
onDuplicateClick={() => duplicatePage()}
|
||||
onClose={() => closePopup()}
|
||||
></NavItemOptionsPopup>
|
||||
)}
|
||||
{showRenamePopup && (
|
||||
<RenamePopup
|
||||
value={page.title}
|
||||
|
@ -30,7 +30,7 @@ export const RenamePopup = ({
|
||||
<div
|
||||
ref={ref}
|
||||
className={
|
||||
'absolute left-[30px] top-[30px] z-10 flex w-[300px] rounded bg-white py-1 px-1.5 shadow-md ' + className
|
||||
'absolute left-[50px] top-[40px] z-10 flex w-[300px] rounded bg-white py-1 px-1.5 shadow-md ' + className
|
||||
}
|
||||
>
|
||||
<input
|
||||
|
@ -1,11 +1,54 @@
|
||||
import React, { ReactNode } from 'react';
|
||||
import React, { ReactNode, useEffect } from 'react';
|
||||
import { NavigationPanel } from './NavigationPanel/NavigationPanel';
|
||||
import { MainPanel } from './MainPanel';
|
||||
import { useNavigationPanelHooks } from './NavigationPanel/NavigationPanel.hooks';
|
||||
import { NavigationFloatingPanel } from './NavigationPanel/NavigationFloatingPanel';
|
||||
import { useWorkspace } from './Workspace.hooks';
|
||||
import { useAppSelector } from '../../stores/store';
|
||||
|
||||
export const Screen = ({ children }: { children: ReactNode }) => {
|
||||
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 (
|
||||
<div className='flex h-screen w-screen bg-white text-black'>
|
||||
<NavigationPanel></NavigationPanel>
|
||||
<div onMouseMove={onScreenMouseMove} className='flex h-screen w-screen bg-white text-black'>
|
||||
{navigationPanelFixed ? (
|
||||
<NavigationPanel
|
||||
onCollapseNavigationClick={onCollapseNavigationClick}
|
||||
width={width}
|
||||
folders={folders}
|
||||
pages={pages}
|
||||
onPageClick={onPageClick}
|
||||
></NavigationPanel>
|
||||
) : (
|
||||
<NavigationFloatingPanel
|
||||
onFixNavigationClick={onFixNavigationClick}
|
||||
slideInFloatingPanel={slideInFloatingPanel}
|
||||
folders={folders}
|
||||
pages={pages}
|
||||
onPageClick={onPageClick}
|
||||
setWidth={setFloatingPanelWidth}
|
||||
></NavigationFloatingPanel>
|
||||
)}
|
||||
|
||||
<MainPanel>{children}</MainPanel>
|
||||
</div>
|
||||
);
|
||||
|
@ -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,
|
||||
};
|
||||
};
|
@ -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<Uint8Array, FlowyError>) => void;
|
||||
|
||||
export class UserNotificationParser extends NotificationParser<UserNotification> {
|
||||
constructor(params: { id?: string; callback: UserNotificationCallback; onError?: OnNotificationError }) {
|
||||
@ -15,8 +16,7 @@ export class UserNotificationParser extends NotificationParser<UserNotification>
|
||||
return UserNotification.Unknown;
|
||||
}
|
||||
},
|
||||
params.id,
|
||||
params.onError
|
||||
params.id
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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<UserProfilePB, FlowyError>) => void;
|
||||
declare type OnUserSignIn = (result: Result<UserProfilePB, FlowyError>) => void;
|
||||
|
||||
export class UserNotificationListener extends AFNotificationObserver<UserNotification> {
|
||||
onProfileUpdate?: OnUserProfileUpdate;
|
||||
@ -16,13 +17,21 @@ export class UserNotificationListener extends AFNotificationObserver<UserNotific
|
||||
onError?: OnNotificationError;
|
||||
}) {
|
||||
const parser = new UserNotificationParser({
|
||||
callback: (notification, payload) => {
|
||||
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;
|
||||
|
@ -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<T>(key: CellCacheKey): T | null {
|
||||
get<T>(key: CellCacheKey): Option<T> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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<T, D> {
|
||||
_fieldBackendService: FieldBackendService;
|
||||
_cellDataNotifier: CellDataNotifier<T | null>;
|
||||
_cellObserver: CellObserver;
|
||||
_cacheKey: CellCacheKey;
|
||||
private _fieldBackendService: FieldBackendService;
|
||||
private _cellDataNotifier: CellDataNotifier<Option<T>>;
|
||||
private _cellObserver: CellObserver;
|
||||
private _cacheKey: CellCacheKey;
|
||||
|
||||
constructor(
|
||||
public readonly cellIdentifier: CellIdentifier,
|
||||
@ -27,15 +27,9 @@ export class CellController<T, D> {
|
||||
private readonly cellDataPersistence: CellDataPersistence<D>
|
||||
) {
|
||||
this._fieldBackendService = new FieldBackendService(cellIdentifier.viewId, cellIdentifier.fieldId);
|
||||
|
||||
this._cacheKey = new CellCacheKey(cellIdentifier.rowId, cellIdentifier.fieldId);
|
||||
|
||||
this._cellDataNotifier = new CellDataNotifier(cellCache.get(this._cacheKey));
|
||||
|
||||
this._cellDataNotifier = new CellDataNotifier(cellCache.get<T>(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<T, D> {
|
||||
await this._loadCellData();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
subscribeChanged = (callbacks: { onCellChanged: (value: Option<T>) => 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<T, D> {
|
||||
});
|
||||
|
||||
this._cellDataNotifier.observer.subscribe((cellData) => {
|
||||
callbacks.onCellChanged(cellData);
|
||||
if (cellData !== null) {
|
||||
callbacks.onCellChanged(cellData);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@ -81,14 +78,26 @@ export class CellController<T, D> {
|
||||
}
|
||||
};
|
||||
|
||||
_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<T> => {
|
||||
const cellData = this.cellCache.get<T>(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<T> extends ChangeNotifier<T | null> {
|
||||
_cellData: T | null;
|
||||
|
||||
constructor(cellData: T) {
|
||||
super();
|
||||
this._cellData = cellData;
|
@ -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<void, FlowyError>;
|
||||
|
||||
export type CellListenerCallback = (value: UpdateCellNotifiedValue) => void;
|
||||
export type CellChangedCallback = (value: UpdateCellNotifiedValue) => void;
|
||||
|
||||
export class CellObserver {
|
||||
_notifier?: ChangeNotifier<UpdateCellNotifiedValue>;
|
||||
_listener?: DatabaseNotificationObserver;
|
||||
private _notifier?: ChangeNotifier<UpdateCellNotifiedValue>;
|
||||
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;
|
||||
};
|
||||
|
@ -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<string, string>;
|
||||
|
||||
export type CheckboxCellController = CellController<string, string>;
|
||||
@ -24,9 +25,8 @@ export type NumberCellController = CellController<string, string>;
|
||||
|
||||
export type SelectOptionCellController = CellController<SelectOptionCellDataPB, string>;
|
||||
|
||||
export type ChecklistCellController = CellController<SelectOptionCellDataPB, string>;
|
||||
|
||||
export type DateCellController = CellController<DateCellDataPB, CalendarData>;
|
||||
|
||||
export class CalendarData {
|
||||
constructor(public readonly date: Date, public readonly time?: string) {}
|
||||
}
|
||||
@ -35,6 +35,7 @@ export type URLCellController = CellController<URLCellDataPB, string>;
|
||||
|
||||
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:
|
||||
|
@ -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<T> {
|
||||
};
|
||||
}
|
||||
|
||||
const utf8Decoder = new TextDecoder('utf-8');
|
||||
|
||||
class StringCellDataParser extends CellDataParser<string> {
|
||||
parserData(data: Uint8Array): string {
|
||||
return utf8.decode(data.toString());
|
||||
return utf8Decoder.decode(data);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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<CalendarData> {
|
||||
constructor(public readonly cellIdentifier: CellIdentifier) {
|
||||
super();
|
||||
}
|
||||
|
||||
save(data: CalendarData): Promise<Result<void, FlowyError>> {
|
||||
const payload = DateChangesetPB.fromObject({ cell_path: _makeCellPath(this.cellIdentifier) });
|
||||
|
||||
|
@ -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,
|
||||
});
|
||||
};
|
||||
}
|
@ -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();
|
||||
};
|
||||
}
|
@ -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 {
|
@ -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();
|
||||
};
|
||||
}
|
@ -71,7 +71,6 @@ export class FieldBackendService {
|
||||
|
||||
duplicateField = () => {
|
||||
const payload = DuplicateFieldPayloadPB.fromObject({ view_id: this.viewId, field_id: this.fieldId });
|
||||
|
||||
return DatabaseEventDuplicateField(payload);
|
||||
};
|
||||
|
@ -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);
|
||||
});
|
||||
};
|
||||
|
@ -9,27 +9,30 @@ type UpdateFieldNotifiedValue = Result<DatabaseFieldChangesetPB, FlowyError>;
|
||||
export type DatabaseNotificationCallback = (value: UpdateFieldNotifiedValue) => void;
|
||||
|
||||
export class DatabaseFieldObserver {
|
||||
_notifier?: ChangeNotifier<UpdateFieldNotifiedValue>;
|
||||
_listener?: DatabaseNotificationObserver;
|
||||
private _notifier?: ChangeNotifier<UpdateFieldNotifiedValue>;
|
||||
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;
|
||||
};
|
||||
|
@ -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<Uint8Array, FlowyError>) => void;
|
||||
|
||||
export class DatabaseNotificationObserver extends AFNotificationObserver<DatabaseNotification> {
|
||||
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);
|
||||
}
|
||||
|
@ -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<Uint8Array, FlowyError>) => void;
|
||||
|
||||
export class DatabaseNotificationParser extends NotificationParser<DatabaseNotification> {
|
||||
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<DatabaseNotif
|
||||
return DatabaseNotification.Unknown;
|
||||
}
|
||||
},
|
||||
params.id,
|
||||
params.onError
|
||||
params.id
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,18 +1,27 @@
|
||||
import { RowPB, InsertedRowPB, UpdatedRowPB } from '../../../../../services/backend/models/flowy-database/row_entities';
|
||||
import { ChangeNotifier } from '../../../../utils/change_notifier';
|
||||
import { FieldInfo } from '../field/controller';
|
||||
import { CellCache, CellCacheKey } from '../cell/cache';
|
||||
import {
|
||||
RowPB,
|
||||
InsertedRowPB,
|
||||
UpdatedRowPB,
|
||||
RowIdPB,
|
||||
OptionalRowPB,
|
||||
RowsChangesetPB,
|
||||
RowsVisibilityChangesetPB,
|
||||
} from '../../../../../services/backend/models/flowy-database/view_entities';
|
||||
import { CellIdentifier } from '../cell/backend_service';
|
||||
import { ReorderSingleRowPB } from '../../../../../services/backend/models/flowy-database/sort_entities';
|
||||
ReorderSingleRowPB,
|
||||
} from '../../../../../services/backend';
|
||||
import { ChangeNotifier } from '../../../../utils/change_notifier';
|
||||
import { FieldInfo } from '../field/field_controller';
|
||||
import { CellCache, CellCacheKey } from '../cell/cell_cache';
|
||||
import { CellIdentifier } from '../cell/cell_bd_svc';
|
||||
import { DatabaseEventGetRow } from '../../../../../services/backend/events/flowy-database';
|
||||
import { None, Option, Some } from 'ts-results';
|
||||
import { Log } from '../../../../utils/log';
|
||||
|
||||
export type CellByFieldId = Map<string, CellIdentifier>;
|
||||
|
||||
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<CellByFieldId> => {
|
||||
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<string, CellIdentifier>) => 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<string, CellIdentifier> => {
|
||||
private _toCellMap = (rowId: string, fieldInfos: readonly FieldInfo[]): CellByFieldId => {
|
||||
const cellIdentifierByFieldId: Map<string, CellIdentifier> = 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<RowInfo> => {
|
||||
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 {
|
@ -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<CellByFieldId> => {
|
||||
return this.cache.loadCells(this.rowInfo.row.id);
|
||||
};
|
||||
}
|
@ -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();
|
||||
};
|
@ -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<string[], FlowyError>;
|
||||
export type ReorderSingleRowNotifyValue = Result<ReorderSingleRowPB, FlowyError>;
|
||||
|
||||
export class DatabaseViewRowsObserver {
|
||||
_rowsVisibilityNotifier = new ChangeNotifier<RowsVisibilityNotifyValue>();
|
||||
_rowsNotifier = new ChangeNotifier<RowsNotifyValue>();
|
||||
_reorderRowsNotifier = new ChangeNotifier<ReorderRowsNotifyValue>();
|
||||
_reorderSingleRowNotifier = new ChangeNotifier<ReorderSingleRowNotifyValue>();
|
||||
private _rowsVisibilityNotifier = new ChangeNotifier<RowsVisibilityNotifyValue>();
|
||||
private _rowsNotifier = new ChangeNotifier<RowsNotifyValue>();
|
||||
private _reorderRowsNotifier = new ChangeNotifier<ReorderRowsNotifyValue>();
|
||||
private _reorderSingleRowNotifier = new ChangeNotifier<ReorderSingleRowNotifyValue>();
|
||||
|
||||
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;
|
@ -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<Result<ViewPB[], FlowyError>> => {
|
||||
@ -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) => {
|
@ -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;
|
||||
|
@ -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<Uint8Array, FlowyError>) => void;
|
||||
|
||||
export class FolderNotificationObserver extends AFNotificationObserver<FolderNotification> {
|
||||
constructor(params: { viewId?: string; parserHandler: ParserHandler; onError?: OnNotificationError }) {
|
||||
const parser = new FolderNotificationParser({
|
||||
callback: params.parserHandler,
|
||||
id: params.viewId,
|
||||
onError: params.onError,
|
||||
});
|
||||
super(parser);
|
||||
}
|
||||
|
@ -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<Uint8Array, FlowyError>) => void;
|
||||
|
||||
export class FolderNotificationParser extends NotificationParser<FolderNotification> {
|
||||
constructor(params: { id?: string; callback: FolderNotificationCallback; onError?: OnNotificationError }) {
|
||||
@ -15,8 +16,7 @@ export class FolderNotificationParser extends NotificationParser<FolderNotificat
|
||||
return FolderNotification.Unknown;
|
||||
}
|
||||
},
|
||||
params.id,
|
||||
params.onError
|
||||
params.id
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -10,11 +10,11 @@ type RestoreViewNotifyValue = Result<ViewPB, FlowyError>;
|
||||
type MoveToTrashViewNotifyValue = Result<DeletedViewPB, FlowyError>;
|
||||
|
||||
export class ViewObserver {
|
||||
_deleteViewNotifier = new ChangeNotifier<DeleteViewNotifyValue>();
|
||||
_updateViewNotifier = new ChangeNotifier<UpdateViewNotifyValue>();
|
||||
_restoreViewNotifier = new ChangeNotifier<RestoreViewNotifyValue>();
|
||||
_moveToTashNotifier = new ChangeNotifier<MoveToTrashViewNotifyValue>();
|
||||
_listener?: FolderNotificationObserver;
|
||||
private _deleteViewNotifier = new ChangeNotifier<DeleteViewNotifyValue>();
|
||||
private _updateViewNotifier = new ChangeNotifier<UpdateViewNotifyValue>();
|
||||
private _restoreViewNotifier = new ChangeNotifier<RestoreViewNotifyValue>();
|
||||
private _moveToTashNotifier = new ChangeNotifier<MoveToTrashViewNotifyValue>();
|
||||
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;
|
||||
|
@ -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 = () => {
|
@ -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<WorkspacePB, FlowyError>;
|
||||
export type WorkspaceNotifyCallback = (value: WorkspaceNotifyValue) => void;
|
||||
|
||||
export class WorkspaceObserver {
|
||||
_appListNotifier = new ChangeNotifier<AppListNotifyValue>();
|
||||
_workspaceNotifier = new ChangeNotifier<WorkspaceNotifyValue>();
|
||||
_listener?: FolderNotificationObserver;
|
||||
private _appListNotifier = new ChangeNotifier<AppListNotifyValue>();
|
||||
private _workspaceNotifier = new ChangeNotifier<WorkspaceNotifyValue>();
|
||||
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;
|
||||
|
@ -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<WorkspaceSettingPB> => {
|
||||
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<WorkspacePB> => {
|
||||
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 = () => {
|
@ -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
|
||||
},
|
||||
});
|
||||
}
|
@ -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;
|
@ -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<ICurrentUser>) => {
|
||||
return action.payload;
|
||||
},
|
||||
logout: (state) => {
|
||||
state.isAuthenticated = false;
|
||||
logout: () => {
|
||||
return {
|
||||
isAuthenticated: false,
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -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<IDatabaseRow>((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;
|
@ -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<string>) {
|
||||
return {
|
||||
display: true,
|
||||
message: action.payload,
|
||||
};
|
||||
},
|
||||
hideError() {
|
||||
return {
|
||||
display: false,
|
||||
message: '',
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const errorActions = errorSlice.actions;
|
@ -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<Uint8Array, FlowyError>) => void;
|
||||
|
||||
export class FolderNotificationParser extends NotificationParser<FolderNotification> {
|
||||
constructor(params: { id?: string; callback: FolderNotificationCallback; onError?: OnNotificationError }) {
|
||||
@ -15,8 +16,7 @@ export class FolderNotificationParser extends NotificationParser<FolderNotificat
|
||||
return FolderNotification.Unknown;
|
||||
}
|
||||
},
|
||||
params.id,
|
||||
params.onError
|
||||
params.id
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -20,6 +20,9 @@ export const foldersSlice = createSlice({
|
||||
deleteFolder(state, action: PayloadAction<{ id: string }>) {
|
||||
return state.filter((f) => f.id !== action.payload.id);
|
||||
},
|
||||
clearFolders() {
|
||||
return [];
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -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 [];
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -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<IWorkspace>) => {
|
||||
return action.payload;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const workspaceActions = workspaceSlice.actions;
|
@ -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),
|
||||
});
|
||||
|
@ -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 <div className={'p-8'}>Board Page ID: {params.id}</div>;
|
||||
useEffect(() => {
|
||||
if (params?.id?.length) {
|
||||
// setDatabaseId(params.id);
|
||||
setDatabaseId('testDb');
|
||||
}
|
||||
}, [params]);
|
||||
|
||||
return (
|
||||
<div className='flex h-full flex-col gap-8 px-8 pt-8'>
|
||||
<h1 className='text-4xl font-bold'>Board</h1>
|
||||
{databaseId?.length && <Board />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -0,0 +1,24 @@
|
||||
import {
|
||||
DocumentEventGetDocument,
|
||||
DocumentVersionPB,
|
||||
OpenDocumentPayloadPB,
|
||||
} from '../../services/backend/events/flowy-document';
|
||||
|
||||
export const useDocument = () => {
|
||||
const loadDocument = async (id: string): Promise<any> => {
|
||||
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 };
|
||||
};
|
@ -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 <div className={'p-8'}>Document Page ID: {params.id}</div>;
|
||||
};
|
||||
|
@ -0,0 +1,9 @@
|
||||
export const useGrid = () => {
|
||||
const loadGrid = async (id: string) => {
|
||||
console.log('loading grid');
|
||||
};
|
||||
|
||||
return {
|
||||
loadGrid,
|
||||
};
|
||||
};
|
@ -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 (
|
||||
<div className='mx-auto mt-8 flex flex-col gap-8 px-8'>
|
||||
<h1 className='text-4xl font-bold'>Grid</h1>
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user