mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
feat: nested views
* chore: remove folder code merge page and folder into navitem component * chore: test fix * fix: nav item expand fix * fix: unfold page and active page * fix: nav item click area fix * chore: remove old components * chore: remove old code * chore: cell controller reorganize * chore: nav item optimizations * fix: add async queue to fix data problem * chore: change semantics of new folder button * chore: move row methods to database controller --------- Co-authored-by: qinluhe <qinluhe.twodog@gmail.com>
This commit is contained in:
parent
9834eccc7b
commit
eee32110f4
@ -201,7 +201,7 @@ export const EditRow = ({
|
|||||||
}}
|
}}
|
||||||
className={`relative flex h-[90%] w-[70%] flex-col gap-8 rounded-xl bg-white `}
|
className={`relative flex h-[90%] w-[70%] flex-col gap-8 rounded-xl bg-white `}
|
||||||
>
|
>
|
||||||
<div onClick={() => onCloseClick()} className={'absolute top-1 right-1'}>
|
<div onClick={() => onCloseClick()} className={'absolute right-1 top-1'}>
|
||||||
<button className={'block h-8 w-8 rounded-lg text-shade-2 hover:bg-main-secondary'}>
|
<button className={'block h-8 w-8 rounded-lg text-shade-2 hover:bg-main-secondary'}>
|
||||||
<CloseSvg></CloseSvg>
|
<CloseSvg></CloseSvg>
|
||||||
</button>
|
</button>
|
||||||
@ -209,7 +209,7 @@ export const EditRow = ({
|
|||||||
|
|
||||||
<div className={'flex h-full'}>
|
<div className={'flex h-full'}>
|
||||||
<div className={'flex h-full flex-1 flex-col border-r border-shade-6 pb-4 pt-6'}>
|
<div className={'flex h-full flex-1 flex-col border-r border-shade-6 pb-4 pt-6'}>
|
||||||
<div className={'pl-12 pb-4'}>
|
<div className={'pb-4 pl-12'}>
|
||||||
<button className={'flex items-center gap-2 p-4'}>
|
<button className={'flex items-center gap-2 p-4'}>
|
||||||
<i className={'h-5 w-5'}>
|
<i className={'h-5 w-5'}>
|
||||||
<ImageSvg></ImageSvg>
|
<ImageSvg></ImageSvg>
|
||||||
@ -229,7 +229,9 @@ export const EditRow = ({
|
|||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{cells
|
{cells
|
||||||
.filter((cell) => databaseStore.fields[cell.cellIdentifier.fieldId].visible)
|
.filter((cell) => {
|
||||||
|
return databaseStore.fields[cell.cellIdentifier.fieldId]?.visible;
|
||||||
|
})
|
||||||
.map((cell, cellIndex) => (
|
.map((cell, cellIndex) => (
|
||||||
<EditCellWrapper
|
<EditCellWrapper
|
||||||
index={cellIndex}
|
index={cellIndex}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { DatabaseController } from '$app/stores/effects/database/database_controller';
|
import { DatabaseController } from '$app/stores/effects/database/database_controller';
|
||||||
import { databaseActions, DatabaseFieldMap, IDatabaseColumn } from '$app/stores/reducers/database/slice';
|
import { databaseActions, DatabaseFieldMap, IDatabaseColumn } from '$app/stores/reducers/database/slice';
|
||||||
import { useAppDispatch } from '$app/stores/store';
|
import { useAppDispatch } from '$app/stores/store';
|
||||||
@ -8,6 +8,7 @@ import { RowInfo } from '$app/stores/effects/database/row/row_cache';
|
|||||||
import { ViewLayoutPB } from '@/services/backend';
|
import { ViewLayoutPB } from '@/services/backend';
|
||||||
import { DatabaseGroupController } from '$app/stores/effects/database/group/group_controller';
|
import { DatabaseGroupController } from '$app/stores/effects/database/group/group_controller';
|
||||||
import { OnDragEndResponder } from 'react-beautiful-dnd';
|
import { OnDragEndResponder } from 'react-beautiful-dnd';
|
||||||
|
import { AsyncQueue } from '$app/utils/async_queue';
|
||||||
|
|
||||||
export const useDatabase = (viewId: string, type?: ViewLayoutPB) => {
|
export const useDatabase = (viewId: string, type?: ViewLayoutPB) => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
@ -24,25 +25,30 @@ export const useDatabase = (viewId: string, type?: ViewLayoutPB) => {
|
|||||||
return () => void c.dispose();
|
return () => void c.dispose();
|
||||||
}, [viewId]);
|
}, [viewId]);
|
||||||
|
|
||||||
const loadFields = async (fieldInfos: readonly FieldInfo[]) => {
|
const loadFields = useCallback(
|
||||||
const fields: DatabaseFieldMap = {};
|
async (fieldInfos: readonly FieldInfo[]) => {
|
||||||
const columns: IDatabaseColumn[] = [];
|
const fields: DatabaseFieldMap = {};
|
||||||
|
const columns: IDatabaseColumn[] = [];
|
||||||
|
for (const fieldInfo of fieldInfos) {
|
||||||
|
const fieldPB = fieldInfo.field;
|
||||||
|
columns.push({
|
||||||
|
fieldId: fieldPB.id,
|
||||||
|
sort: 'none',
|
||||||
|
visible: fieldPB.visibility,
|
||||||
|
});
|
||||||
|
|
||||||
for (const fieldInfo of fieldInfos) {
|
const field = await loadField(viewId, fieldInfo, dispatch);
|
||||||
const fieldPB = fieldInfo.field;
|
fields[field.fieldId] = field;
|
||||||
columns.push({
|
}
|
||||||
fieldId: fieldPB.id,
|
dispatch(databaseActions.updateFields({ fields }));
|
||||||
sort: 'none',
|
dispatch(databaseActions.updateColumns({ columns }));
|
||||||
visible: fieldPB.visibility,
|
},
|
||||||
});
|
[viewId, dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
const field = await loadField(viewId, fieldInfo, dispatch);
|
const queue = useMemo(() => {
|
||||||
fields[field.fieldId] = field;
|
return new AsyncQueue<readonly FieldInfo[]>(loadFields);
|
||||||
}
|
}, [loadFields]);
|
||||||
|
|
||||||
dispatch(databaseActions.updateFields({ fields }));
|
|
||||||
dispatch(databaseActions.updateColumns({ columns }));
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
void (async () => {
|
void (async () => {
|
||||||
@ -53,7 +59,7 @@ export const useDatabase = (viewId: string, type?: ViewLayoutPB) => {
|
|||||||
setRows([...rowInfos]);
|
setRows([...rowInfos]);
|
||||||
},
|
},
|
||||||
onFieldsChanged: (fieldInfos) => {
|
onFieldsChanged: (fieldInfos) => {
|
||||||
void loadFields(fieldInfos);
|
queue.enqueue(fieldInfos);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -76,7 +82,7 @@ export const useDatabase = (viewId: string, type?: ViewLayoutPB) => {
|
|||||||
return () => {
|
return () => {
|
||||||
void controller?.dispose();
|
void controller?.dispose();
|
||||||
};
|
};
|
||||||
}, [controller]);
|
}, [controller, queue]);
|
||||||
|
|
||||||
const onNewRowClick = async (index: number) => {
|
const onNewRowClick = async (index: number) => {
|
||||||
if (!groups) return;
|
if (!groups) return;
|
||||||
@ -95,7 +101,7 @@ export const useDatabase = (viewId: string, type?: ViewLayoutPB) => {
|
|||||||
|
|
||||||
if (source.droppableId === destination?.droppableId) {
|
if (source.droppableId === destination?.droppableId) {
|
||||||
// move inside the block (group)
|
// move inside the block (group)
|
||||||
await controller.exchangeRow(
|
await controller.exchangeGroupRow(
|
||||||
group.rows[source.index].id,
|
group.rows[source.index].id,
|
||||||
destination.droppableId,
|
destination.droppableId,
|
||||||
group.rows[destination.index].id
|
group.rows[destination.index].id
|
||||||
@ -103,7 +109,7 @@ export const useDatabase = (viewId: string, type?: ViewLayoutPB) => {
|
|||||||
} else {
|
} else {
|
||||||
// move to different block (group)
|
// move to different block (group)
|
||||||
if (!destination?.droppableId) return;
|
if (!destination?.droppableId) return;
|
||||||
await controller.moveRow(group.rows[source.index].id, destination.droppableId);
|
await controller.moveGroupRow(group.rows[source.index].id, destination.droppableId);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -7,7 +7,6 @@ import { Draggable } from 'react-beautiful-dnd';
|
|||||||
import { MouseEventHandler, useState } from 'react';
|
import { MouseEventHandler, useState } from 'react';
|
||||||
import { PopupWindow } from '$app/components/_shared/PopupWindow';
|
import { PopupWindow } from '$app/components/_shared/PopupWindow';
|
||||||
import { TrashSvg } from '$app/components/_shared/svg/TrashSvg';
|
import { TrashSvg } from '$app/components/_shared/svg/TrashSvg';
|
||||||
import { RowBackendService } from '$app/stores/effects/database/row/row_bd_svc';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useAppSelector } from '$app/stores/store';
|
import { useAppSelector } from '$app/stores/store';
|
||||||
|
|
||||||
@ -52,8 +51,7 @@ export const BoardCard = ({
|
|||||||
|
|
||||||
const onDeleteRowClick = async () => {
|
const onDeleteRowClick = async () => {
|
||||||
setShowCardPopup(false);
|
setShowCardPopup(false);
|
||||||
const svc = new RowBackendService(viewId);
|
await controller.deleteRow(rowInfo.row.id);
|
||||||
await svc.deleteRow(rowInfo.row.id);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -73,7 +71,7 @@ export const BoardCard = ({
|
|||||||
<div className={'flex flex-col gap-3'}>
|
<div className={'flex flex-col gap-3'}>
|
||||||
{cells
|
{cells
|
||||||
.filter(
|
.filter(
|
||||||
(cell) => cell.fieldId !== groupByFieldId && databaseStore.fields[cell.cellIdentifier.fieldId].visible
|
(cell) => cell.fieldId !== groupByFieldId && databaseStore.fields[cell.cellIdentifier.fieldId]?.visible
|
||||||
)
|
)
|
||||||
.map((cell, cellIndex) => (
|
.map((cell, cellIndex) => (
|
||||||
<BoardCell
|
<BoardCell
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { ShowMenuSvg } from '../../_shared/svg/ShowMenuSvg';
|
import { ShowMenuSvg } from '../../_shared/svg/ShowMenuSvg';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useAppSelector } from '../../../stores/store';
|
import { useAppSelector } from '$app/stores/store';
|
||||||
import { useLocation } from 'react-router-dom';
|
import { useLocation } from 'react-router-dom';
|
||||||
|
|
||||||
export const Breadcrumbs = ({ menuHidden, onShowMenuClick }: { menuHidden: boolean; onShowMenuClick: () => void }) => {
|
export const Breadcrumbs = ({ menuHidden, onShowMenuClick }: { menuHidden: boolean; onShowMenuClick: () => void }) => {
|
||||||
@ -9,7 +9,6 @@ export const Breadcrumbs = ({ menuHidden, onShowMenuClick }: { menuHidden: boole
|
|||||||
const [activePageId, setActivePageId] = useState<string>('');
|
const [activePageId, setActivePageId] = useState<string>('');
|
||||||
const currentLocation = useLocation();
|
const currentLocation = useLocation();
|
||||||
const pagesStore = useAppSelector((state) => state.pages);
|
const pagesStore = useAppSelector((state) => state.pages);
|
||||||
const foldersStore = useAppSelector((state) => state.folders);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const { pathname } = currentLocation;
|
const { pathname } = currentLocation;
|
||||||
@ -20,10 +19,10 @@ export const Breadcrumbs = ({ menuHidden, onShowMenuClick }: { menuHidden: boole
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const page = pagesStore.find((p) => p.id === activePageId);
|
const page = pagesStore.find((p) => p.id === activePageId);
|
||||||
const folder = foldersStore.find((f) => f.id === page?.folderId);
|
// const folder = foldersStore.find((f) => f.id === page?.parentPageId);
|
||||||
setFolderName(folder?.title ?? '');
|
// setFolderName(folder?.title ?? '');
|
||||||
setPageName(page?.title ?? '');
|
setPageName(page?.title ?? '');
|
||||||
}, [pagesStore, foldersStore, activePageId]);
|
}, [pagesStore, activePageId]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={'flex items-center'}>
|
<div className={'flex items-center'}>
|
||||||
|
@ -1,204 +0,0 @@
|
|||||||
import { foldersActions, IFolder } from '$app_reducers/folders/slice';
|
|
||||||
import { useEffect, useState } from 'react';
|
|
||||||
import { useAppDispatch, useAppSelector } from '$app/stores/store';
|
|
||||||
import { IPage, pagesActions } from '$app_reducers/pages/slice';
|
|
||||||
import { ViewLayoutPB } from '@/services/backend';
|
|
||||||
import { AppBackendService } from '$app/stores/effects/folder/app/app_bd_svc';
|
|
||||||
import { WorkspaceBackendService } from '$app/stores/effects/folder/workspace/workspace_bd_svc';
|
|
||||||
|
|
||||||
import { AppObserver } from '$app/stores/effects/folder/app/app_observer';
|
|
||||||
import { useNavigate } from 'react-router-dom';
|
|
||||||
import { INITIAL_FOLDER_HEIGHT, PAGE_ITEM_HEIGHT } from '../../_shared/constants';
|
|
||||||
|
|
||||||
export const useFolderEvents = (folder: IFolder, pages: IPage[]) => {
|
|
||||||
const appDispatch = useAppDispatch();
|
|
||||||
const workspace = useAppSelector((state) => state.workspace);
|
|
||||||
|
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
// Actions
|
|
||||||
const [showPages, setShowPages] = useState(false);
|
|
||||||
const [showFolderOptions, setShowFolderOptions] = useState(false);
|
|
||||||
const [showNewPageOptions, setShowNewPageOptions] = useState(false);
|
|
||||||
const [showRenamePopup, setShowRenamePopup] = useState(false);
|
|
||||||
|
|
||||||
// UI configurations
|
|
||||||
const [folderHeight, setFolderHeight] = useState(`${INITIAL_FOLDER_HEIGHT}px`);
|
|
||||||
|
|
||||||
// Observers
|
|
||||||
const appObserver = new AppObserver(folder.id);
|
|
||||||
|
|
||||||
// Backend services
|
|
||||||
const appBackendService = new AppBackendService(folder.id);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
void appObserver.subscribe({
|
|
||||||
onViewsChanged: async () => {
|
|
||||||
const result = await appBackendService.getAllViews();
|
|
||||||
if (!result.ok) return;
|
|
||||||
const views = result.val;
|
|
||||||
const updatedPages: IPage[] = views.map((view) => ({
|
|
||||||
id: view.id,
|
|
||||||
folderId: view.parent_view_id,
|
|
||||||
pageType: view.layout,
|
|
||||||
title: view.name,
|
|
||||||
}));
|
|
||||||
appDispatch(pagesActions.didReceivePages({ pages: updatedPages, folderId: folder.id }));
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return () => {
|
|
||||||
// Unsubscribe when the component is unmounted.
|
|
||||||
void appObserver.unsubscribe();
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (showPages) {
|
|
||||||
setFolderHeight(`${INITIAL_FOLDER_HEIGHT + pages.length * PAGE_ITEM_HEIGHT}px`);
|
|
||||||
}
|
|
||||||
}, [pages]);
|
|
||||||
|
|
||||||
const onFolderNameClick = () => {
|
|
||||||
if (showPages) {
|
|
||||||
setFolderHeight(`${INITIAL_FOLDER_HEIGHT}px`);
|
|
||||||
} else {
|
|
||||||
setFolderHeight(`${INITIAL_FOLDER_HEIGHT + pages.length * PAGE_ITEM_HEIGHT}px`);
|
|
||||||
}
|
|
||||||
setShowPages(!showPages);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onFolderOptionsClick = () => {
|
|
||||||
setShowFolderOptions(!showFolderOptions);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onNewPageClick = () => {
|
|
||||||
setShowNewPageOptions(!showNewPageOptions);
|
|
||||||
};
|
|
||||||
|
|
||||||
const startFolderRename = () => {
|
|
||||||
closePopup();
|
|
||||||
setShowRenamePopup(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const changeFolderTitle = async (newTitle: string) => {
|
|
||||||
await appBackendService.update({ name: newTitle });
|
|
||||||
appDispatch(foldersActions.renameFolder({ id: folder.id, newTitle }));
|
|
||||||
};
|
|
||||||
|
|
||||||
const closeRenamePopup = () => {
|
|
||||||
setShowRenamePopup(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const deleteFolder = async () => {
|
|
||||||
closePopup();
|
|
||||||
await appBackendService.delete();
|
|
||||||
appDispatch(foldersActions.deleteFolder({ id: folder.id }));
|
|
||||||
};
|
|
||||||
|
|
||||||
const duplicateFolder = async () => {
|
|
||||||
closePopup();
|
|
||||||
const workspaceBackendService = new WorkspaceBackendService(workspace.id ?? '');
|
|
||||||
const newApp = await workspaceBackendService.createApp({
|
|
||||||
name: folder.title,
|
|
||||||
});
|
|
||||||
appDispatch(foldersActions.addFolder({ id: newApp.id, title: folder.title }));
|
|
||||||
};
|
|
||||||
|
|
||||||
const closePopup = () => {
|
|
||||||
setShowFolderOptions(false);
|
|
||||||
setShowNewPageOptions(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onAddNewDocumentPage = async () => {
|
|
||||||
closePopup();
|
|
||||||
const newView = await appBackendService.createView({
|
|
||||||
name: 'New Document 1',
|
|
||||||
layoutType: ViewLayoutPB.Document,
|
|
||||||
});
|
|
||||||
try {
|
|
||||||
appDispatch(
|
|
||||||
pagesActions.addPage({
|
|
||||||
folderId: folder.id,
|
|
||||||
pageType: ViewLayoutPB.Document,
|
|
||||||
title: newView.name,
|
|
||||||
id: newView.id,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
setShowPages(true);
|
|
||||||
|
|
||||||
navigate(`/page/document/${newView.id}`);
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const onAddNewBoardPage = async () => {
|
|
||||||
closePopup();
|
|
||||||
const newView = await appBackendService.createView({
|
|
||||||
name: 'New Board 1',
|
|
||||||
layoutType: ViewLayoutPB.Board,
|
|
||||||
});
|
|
||||||
|
|
||||||
setShowPages(true);
|
|
||||||
|
|
||||||
appDispatch(
|
|
||||||
pagesActions.addPage({
|
|
||||||
folderId: folder.id,
|
|
||||||
pageType: ViewLayoutPB.Board,
|
|
||||||
title: newView.name,
|
|
||||||
id: newView.id,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
navigate(`/page/board/${newView.id}`);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onAddNewGridPage = async () => {
|
|
||||||
closePopup();
|
|
||||||
const newView = await appBackendService.createView({
|
|
||||||
name: 'New Grid 1',
|
|
||||||
layoutType: ViewLayoutPB.Grid,
|
|
||||||
});
|
|
||||||
|
|
||||||
setShowPages(true);
|
|
||||||
|
|
||||||
appDispatch(
|
|
||||||
pagesActions.addPage({
|
|
||||||
folderId: folder.id,
|
|
||||||
pageType: ViewLayoutPB.Grid,
|
|
||||||
title: newView.name,
|
|
||||||
id: newView.id,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
navigate(`/page/grid/${newView.id}`);
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
appDispatch(foldersActions.setShowPages({ id: folder.id, showPages: showPages }));
|
|
||||||
}, [showPages]);
|
|
||||||
|
|
||||||
return {
|
|
||||||
showPages,
|
|
||||||
onFolderNameClick,
|
|
||||||
showFolderOptions,
|
|
||||||
onFolderOptionsClick,
|
|
||||||
showNewPageOptions,
|
|
||||||
onNewPageClick,
|
|
||||||
|
|
||||||
showRenamePopup,
|
|
||||||
startFolderRename,
|
|
||||||
changeFolderTitle,
|
|
||||||
closeRenamePopup,
|
|
||||||
deleteFolder,
|
|
||||||
duplicateFolder,
|
|
||||||
|
|
||||||
onAddNewDocumentPage,
|
|
||||||
onAddNewBoardPage,
|
|
||||||
onAddNewGridPage,
|
|
||||||
|
|
||||||
closePopup,
|
|
||||||
folderHeight,
|
|
||||||
};
|
|
||||||
};
|
|
@ -1,118 +0,0 @@
|
|||||||
import { Details2Svg } from '../../_shared/svg/Details2Svg';
|
|
||||||
import AddSvg from '../../_shared/svg/AddSvg';
|
|
||||||
import { NavItemOptionsPopup } from './NavItemOptionsPopup';
|
|
||||||
import { NewPagePopup } from './NewPagePopup';
|
|
||||||
import { IFolder } from '$app_reducers/folders/slice';
|
|
||||||
import { useFolderEvents } from './FolderItem.hooks';
|
|
||||||
import { IPage } from '$app_reducers/pages/slice';
|
|
||||||
import { PageItem } from './PageItem';
|
|
||||||
import { Button } from '../../_shared/Button';
|
|
||||||
import { RenamePopup } from './RenamePopup';
|
|
||||||
import { useEffect, useRef, useState } from 'react';
|
|
||||||
import { DropDownShowSvg } from '../../_shared/svg/DropDownShowSvg';
|
|
||||||
import { ANIMATION_DURATION } from '../../_shared/constants';
|
|
||||||
|
|
||||||
export const FolderItem = ({
|
|
||||||
folder,
|
|
||||||
pages,
|
|
||||||
onPageClick,
|
|
||||||
}: {
|
|
||||||
folder: IFolder;
|
|
||||||
pages: IPage[];
|
|
||||||
onPageClick: (page: IPage) => void;
|
|
||||||
}) => {
|
|
||||||
const {
|
|
||||||
showPages,
|
|
||||||
onFolderNameClick,
|
|
||||||
showFolderOptions,
|
|
||||||
onFolderOptionsClick,
|
|
||||||
showNewPageOptions,
|
|
||||||
onNewPageClick,
|
|
||||||
|
|
||||||
showRenamePopup,
|
|
||||||
startFolderRename,
|
|
||||||
changeFolderTitle,
|
|
||||||
closeRenamePopup,
|
|
||||||
deleteFolder,
|
|
||||||
duplicateFolder,
|
|
||||||
|
|
||||||
onAddNewDocumentPage,
|
|
||||||
onAddNewBoardPage,
|
|
||||||
onAddNewGridPage,
|
|
||||||
|
|
||||||
closePopup,
|
|
||||||
folderHeight,
|
|
||||||
} = useFolderEvents(folder, pages);
|
|
||||||
|
|
||||||
const [popupY, setPopupY] = useState(0);
|
|
||||||
|
|
||||||
const el = useRef<HTMLDivElement>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (el.current) {
|
|
||||||
const { top } = el.current.getBoundingClientRect();
|
|
||||||
setPopupY(top);
|
|
||||||
}
|
|
||||||
}, [showFolderOptions, showNewPageOptions, showRenamePopup]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div ref={el}>
|
|
||||||
<div
|
|
||||||
className={`my-2 overflow-hidden transition-all`}
|
|
||||||
style={{ height: folderHeight, transitionDuration: `${ANIMATION_DURATION}ms` }}
|
|
||||||
>
|
|
||||||
<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={'flex items-center'}>
|
|
||||||
<Button size={'box-small-transparent'} onClick={() => onFolderOptionsClick()}>
|
|
||||||
<Details2Svg></Details2Svg>
|
|
||||||
</Button>
|
|
||||||
<Button size={'box-small-transparent'} onClick={() => onNewPageClick()}>
|
|
||||||
<AddSvg></AddSvg>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</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()}
|
|
||||||
top={popupY - 124 + 40}
|
|
||||||
></NavItemOptionsPopup>
|
|
||||||
)}
|
|
||||||
{showNewPageOptions && (
|
|
||||||
<NewPagePopup
|
|
||||||
onDocumentClick={() => onAddNewDocumentPage()}
|
|
||||||
onBoardClick={() => onAddNewBoardPage()}
|
|
||||||
onGridClick={() => onAddNewGridPage()}
|
|
||||||
onClose={() => closePopup()}
|
|
||||||
top={popupY - 124 + 40}
|
|
||||||
></NewPagePopup>
|
|
||||||
)}
|
|
||||||
{showRenamePopup && (
|
|
||||||
<RenamePopup
|
|
||||||
value={folder.title}
|
|
||||||
onChange={(newTitle) => changeFolderTitle(newTitle)}
|
|
||||||
onClose={closeRenamePopup}
|
|
||||||
top={popupY - 124 + 40}
|
|
||||||
></RenamePopup>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
@ -0,0 +1,226 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { useAppDispatch, useAppSelector } from '$app/stores/store';
|
||||||
|
import { IPage, pagesActions } from '$app_reducers/pages/slice';
|
||||||
|
import { ViewLayoutPB } from '@/services/backend';
|
||||||
|
import { WorkspaceBackendService } from '$app/stores/effects/folder/workspace/workspace_bd_svc';
|
||||||
|
|
||||||
|
import { useLocation, useNavigate } from 'react-router-dom';
|
||||||
|
import { INITIAL_FOLDER_HEIGHT, PAGE_ITEM_HEIGHT } from '../../_shared/constants';
|
||||||
|
|
||||||
|
import { ViewBackendService } from '$app/stores/effects/folder/view/view_bd_svc';
|
||||||
|
import { ViewObserver } from '$app/stores/effects/folder/view/view_observer';
|
||||||
|
|
||||||
|
export const useNavItem = (page: IPage) => {
|
||||||
|
const appDispatch = useAppDispatch();
|
||||||
|
const workspace = useAppSelector((state) => state.workspace);
|
||||||
|
const currentLocation = useLocation();
|
||||||
|
const [activePageId, setActivePageId] = useState<string>('');
|
||||||
|
const pages = useAppSelector((state) => state.pages);
|
||||||
|
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
const [showPageOptions, setShowPageOptions] = useState(false);
|
||||||
|
const [showNewPageOptions, setShowNewPageOptions] = useState(false);
|
||||||
|
const [showRenamePopup, setShowRenamePopup] = useState(false);
|
||||||
|
|
||||||
|
// UI configurations
|
||||||
|
const [folderHeight, setFolderHeight] = useState(`${INITIAL_FOLDER_HEIGHT}px`);
|
||||||
|
|
||||||
|
// backend
|
||||||
|
const service = new ViewBackendService(page.id);
|
||||||
|
const observer = new ViewObserver(page.id);
|
||||||
|
|
||||||
|
const loadInsidePages = async () => {
|
||||||
|
const result = await service.getChildViews();
|
||||||
|
if (!result.ok) return;
|
||||||
|
const views = result.val;
|
||||||
|
const updatedPages: IPage[] = views.map<IPage>((view) => ({
|
||||||
|
parentPageId: page.id,
|
||||||
|
id: view.id,
|
||||||
|
pageType: view.layout,
|
||||||
|
title: view.name,
|
||||||
|
showPagesInside: false,
|
||||||
|
}));
|
||||||
|
appDispatch(pagesActions.addInsidePages({ currentPageId: page.id, insidePages: updatedPages }));
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
void loadInsidePages();
|
||||||
|
void observer.subscribe({
|
||||||
|
onChildViewsChanged: () => {
|
||||||
|
void loadInsidePages();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return () => {
|
||||||
|
// Unsubscribe when the component is unmounted.
|
||||||
|
void observer.unsubscribe();
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const { pathname } = currentLocation;
|
||||||
|
const parts = pathname.split('/');
|
||||||
|
const pageId = parts[parts.length - 1];
|
||||||
|
setActivePageId(pageId);
|
||||||
|
}, [currentLocation]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (page.showPagesInside) {
|
||||||
|
setFolderHeight(`${PAGE_ITEM_HEIGHT + getChildCount(page) * PAGE_ITEM_HEIGHT}px`);
|
||||||
|
} else {
|
||||||
|
setFolderHeight(`${PAGE_ITEM_HEIGHT}px`);
|
||||||
|
}
|
||||||
|
}, [page, pages]);
|
||||||
|
|
||||||
|
// recursively get all unfolded child pages
|
||||||
|
const getChildCount: (startPage: IPage) => number = (startPage: IPage) => {
|
||||||
|
let count = 0;
|
||||||
|
count = pages.filter((p) => p.parentPageId === startPage.id).length;
|
||||||
|
pages
|
||||||
|
.filter((p) => p.parentPageId === startPage.id)
|
||||||
|
.forEach((p) => {
|
||||||
|
if (p.showPagesInside) {
|
||||||
|
count += getChildCount(p);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return count;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onUnfoldClick = () => {
|
||||||
|
appDispatch(pagesActions.toggleShowPages({ id: page.id }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const onPageOptionsClick = () => {
|
||||||
|
setShowPageOptions(!showPageOptions);
|
||||||
|
};
|
||||||
|
|
||||||
|
const startPageRename = () => {
|
||||||
|
setShowRenamePopup(true);
|
||||||
|
closePopup();
|
||||||
|
};
|
||||||
|
|
||||||
|
const onNewPageClick = () => {
|
||||||
|
setShowNewPageOptions(!showNewPageOptions);
|
||||||
|
};
|
||||||
|
|
||||||
|
const changePageTitle = async (newTitle: string) => {
|
||||||
|
await service.update({ name: newTitle });
|
||||||
|
appDispatch(pagesActions.renamePage({ id: page.id, newTitle }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeRenamePopup = () => {
|
||||||
|
setShowRenamePopup(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const deletePage = async () => {
|
||||||
|
closePopup();
|
||||||
|
await service.delete();
|
||||||
|
appDispatch(pagesActions.deletePage({ id: page.id }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const duplicatePage = async () => {
|
||||||
|
closePopup();
|
||||||
|
await service.duplicate();
|
||||||
|
};
|
||||||
|
|
||||||
|
const closePopup = () => {
|
||||||
|
setShowPageOptions(false);
|
||||||
|
setShowNewPageOptions(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onPageClick = (eventPage: IPage) => {
|
||||||
|
const pageTypeRoute = (() => {
|
||||||
|
switch (eventPage.pageType) {
|
||||||
|
case ViewLayoutPB.Document:
|
||||||
|
return 'document';
|
||||||
|
case ViewLayoutPB.Grid:
|
||||||
|
return 'grid';
|
||||||
|
case ViewLayoutPB.Board:
|
||||||
|
return 'board';
|
||||||
|
default:
|
||||||
|
return 'document';
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
navigate(`/page/${pageTypeRoute}/${eventPage.id}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onAddNewPage = async (pageType: ViewLayoutPB) => {
|
||||||
|
closePopup();
|
||||||
|
if (!workspace?.id) return;
|
||||||
|
|
||||||
|
let newPageName = '';
|
||||||
|
let pageTypeRoute = '';
|
||||||
|
|
||||||
|
switch (pageType) {
|
||||||
|
case ViewLayoutPB.Document:
|
||||||
|
newPageName = 'Document Page 1';
|
||||||
|
pageTypeRoute = 'document';
|
||||||
|
break;
|
||||||
|
case ViewLayoutPB.Grid:
|
||||||
|
newPageName = 'Grid Page 1';
|
||||||
|
pageTypeRoute = 'grid';
|
||||||
|
break;
|
||||||
|
case ViewLayoutPB.Board:
|
||||||
|
newPageName = 'Board Page 1';
|
||||||
|
pageTypeRoute = 'board';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
newPageName = 'Document Page 1';
|
||||||
|
pageTypeRoute = 'document';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const workspaceService = new WorkspaceBackendService(workspace.id);
|
||||||
|
const newViewResult = await workspaceService.createView({
|
||||||
|
name: newPageName,
|
||||||
|
layoutType: pageType,
|
||||||
|
parentViewId: page.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (newViewResult.ok) {
|
||||||
|
const newView = newViewResult.val;
|
||||||
|
if (!page.showPagesInside) {
|
||||||
|
appDispatch(pagesActions.toggleShowPages({ id: page.id }));
|
||||||
|
}
|
||||||
|
|
||||||
|
appDispatch(
|
||||||
|
pagesActions.addPage({
|
||||||
|
parentPageId: page.id,
|
||||||
|
pageType,
|
||||||
|
title: newView.name,
|
||||||
|
id: newView.id,
|
||||||
|
showPagesInside: false,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
navigate(`/page/${pageTypeRoute}/${newView.id}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
onUnfoldClick,
|
||||||
|
onNewPageClick,
|
||||||
|
onPageOptionsClick,
|
||||||
|
startPageRename,
|
||||||
|
|
||||||
|
changePageTitle,
|
||||||
|
closeRenamePopup,
|
||||||
|
closePopup,
|
||||||
|
|
||||||
|
showNewPageOptions,
|
||||||
|
showPageOptions,
|
||||||
|
showRenamePopup,
|
||||||
|
|
||||||
|
deletePage,
|
||||||
|
duplicatePage,
|
||||||
|
|
||||||
|
onPageClick,
|
||||||
|
|
||||||
|
onAddNewPage,
|
||||||
|
|
||||||
|
folderHeight,
|
||||||
|
activePageId,
|
||||||
|
};
|
||||||
|
};
|
@ -0,0 +1,126 @@
|
|||||||
|
import { Details2Svg } from '../../_shared/svg/Details2Svg';
|
||||||
|
import AddSvg from '../../_shared/svg/AddSvg';
|
||||||
|
import { NavItemOptionsPopup } from './NavItemOptionsPopup';
|
||||||
|
import { NewPagePopup } from './NewPagePopup';
|
||||||
|
import { IPage } from '$app_reducers/pages/slice';
|
||||||
|
import { Button } from '../../_shared/Button';
|
||||||
|
import { RenamePopup } from './RenamePopup';
|
||||||
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
|
import { DropDownShowSvg } from '../../_shared/svg/DropDownShowSvg';
|
||||||
|
import { ANIMATION_DURATION, PAGE_ITEM_HEIGHT } from '../../_shared/constants';
|
||||||
|
import { useNavItem } from '$app/components/layout/NavigationPanel/NavItem.hooks';
|
||||||
|
import { useAppSelector } from '$app/stores/store';
|
||||||
|
import { ViewLayoutPB } from '@/services/backend';
|
||||||
|
|
||||||
|
export const NavItem = ({ page }: { page: IPage }) => {
|
||||||
|
const pages = useAppSelector((state) => state.pages);
|
||||||
|
const {
|
||||||
|
onUnfoldClick,
|
||||||
|
onNewPageClick,
|
||||||
|
onPageOptionsClick,
|
||||||
|
startPageRename,
|
||||||
|
|
||||||
|
changePageTitle,
|
||||||
|
closeRenamePopup,
|
||||||
|
closePopup,
|
||||||
|
|
||||||
|
showNewPageOptions,
|
||||||
|
showPageOptions,
|
||||||
|
showRenamePopup,
|
||||||
|
|
||||||
|
deletePage,
|
||||||
|
duplicatePage,
|
||||||
|
|
||||||
|
onAddNewPage,
|
||||||
|
|
||||||
|
folderHeight,
|
||||||
|
activePageId,
|
||||||
|
|
||||||
|
onPageClick,
|
||||||
|
} = useNavItem(page);
|
||||||
|
|
||||||
|
const [popupY, setPopupY] = useState(0);
|
||||||
|
|
||||||
|
const el = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (el.current) {
|
||||||
|
const { top } = el.current.getBoundingClientRect();
|
||||||
|
setPopupY(top);
|
||||||
|
}
|
||||||
|
}, [showPageOptions, showNewPageOptions, showRenamePopup]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={el}>
|
||||||
|
<div
|
||||||
|
className={`overflow-hidden transition-all`}
|
||||||
|
style={{ height: folderHeight, transitionDuration: `${ANIMATION_DURATION}ms` }}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{ height: PAGE_ITEM_HEIGHT }}
|
||||||
|
className={`flex cursor-pointer items-center justify-between rounded-lg px-4 hover:bg-surface-2 ${
|
||||||
|
activePageId === page.id ? 'bg-surface-2' : ''
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className={'flex h-full min-w-0 flex-1 items-center'}>
|
||||||
|
<button
|
||||||
|
onClick={() => onUnfoldClick()}
|
||||||
|
className={`mr-2 h-5 w-5 transition-transform duration-200 ${page.showPagesInside && 'rotate-180'}`}
|
||||||
|
>
|
||||||
|
<DropDownShowSvg></DropDownShowSvg>
|
||||||
|
</button>
|
||||||
|
<div
|
||||||
|
onClick={() => onPageClick(page)}
|
||||||
|
className={
|
||||||
|
'flex h-full min-w-0 flex-1 items-center overflow-hidden overflow-ellipsis whitespace-nowrap text-left'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{page.title}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={'flex items-center'}>
|
||||||
|
<Button size={'box-small-transparent'} onClick={() => onPageOptionsClick()}>
|
||||||
|
<Details2Svg></Details2Svg>
|
||||||
|
</Button>
|
||||||
|
<Button size={'box-small-transparent'} onClick={() => onNewPageClick()}>
|
||||||
|
<AddSvg></AddSvg>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={'pl-4'}>
|
||||||
|
{useMemo(() => pages.filter((insidePage) => insidePage.parentPageId === page.id), [pages, page]).map(
|
||||||
|
(insidePage, insideIndex) => (
|
||||||
|
<NavItem key={insideIndex} page={insidePage}></NavItem>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{showPageOptions && (
|
||||||
|
<NavItemOptionsPopup
|
||||||
|
onRenameClick={() => startPageRename()}
|
||||||
|
onDeleteClick={() => deletePage()}
|
||||||
|
onDuplicateClick={() => duplicatePage()}
|
||||||
|
onClose={() => closePopup()}
|
||||||
|
top={popupY - 124 + 40}
|
||||||
|
></NavItemOptionsPopup>
|
||||||
|
)}
|
||||||
|
{showNewPageOptions && (
|
||||||
|
<NewPagePopup
|
||||||
|
onDocumentClick={() => onAddNewPage(ViewLayoutPB.Document)}
|
||||||
|
onBoardClick={() => onAddNewPage(ViewLayoutPB.Board)}
|
||||||
|
onGridClick={() => onAddNewPage(ViewLayoutPB.Grid)}
|
||||||
|
onClose={() => closePopup()}
|
||||||
|
top={popupY - 124 + 40}
|
||||||
|
></NewPagePopup>
|
||||||
|
)}
|
||||||
|
{showRenamePopup && (
|
||||||
|
<RenamePopup
|
||||||
|
value={page.title}
|
||||||
|
onChange={(newTitle) => changePageTitle(newTitle)}
|
||||||
|
onClose={closeRenamePopup}
|
||||||
|
top={popupY - 124 + 40}
|
||||||
|
></RenamePopup>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -1,17 +1,10 @@
|
|||||||
import { useAppSelector } from '$app/stores/store';
|
import { useAppSelector } from '$app/stores/store';
|
||||||
import { useNavigate } from 'react-router-dom';
|
|
||||||
import { IPage } from '$app_reducers/pages/slice';
|
|
||||||
import { ViewLayoutPB } from '@/services/backend';
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
export const useNavigationPanelHooks = function () {
|
export const useNavigationPanelHooks = function () {
|
||||||
const folders = useAppSelector((state) => state.folders);
|
|
||||||
const pages = useAppSelector((state) => state.pages);
|
|
||||||
const width = useAppSelector((state) => state.navigationWidth);
|
const width = useAppSelector((state) => state.navigationWidth);
|
||||||
const [menuHidden, setMenuHidden] = useState(false);
|
const [menuHidden, setMenuHidden] = useState(false);
|
||||||
|
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
const onHideMenuClick = () => {
|
const onHideMenuClick = () => {
|
||||||
setMenuHidden(true);
|
setMenuHidden(true);
|
||||||
};
|
};
|
||||||
@ -20,28 +13,8 @@ export const useNavigationPanelHooks = function () {
|
|||||||
setMenuHidden(false);
|
setMenuHidden(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const onPageClick = (page: IPage) => {
|
|
||||||
const pageTypeRoute = (() => {
|
|
||||||
switch (page.pageType) {
|
|
||||||
case ViewLayoutPB.Document:
|
|
||||||
return 'document';
|
|
||||||
case ViewLayoutPB.Grid:
|
|
||||||
return 'grid';
|
|
||||||
case ViewLayoutPB.Board:
|
|
||||||
return 'board';
|
|
||||||
default:
|
|
||||||
return 'document';
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
|
|
||||||
navigate(`/page/${pageTypeRoute}/${page.id}`);
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
width,
|
width,
|
||||||
folders,
|
|
||||||
pages,
|
|
||||||
onPageClick,
|
|
||||||
menuHidden,
|
menuHidden,
|
||||||
onHideMenuClick,
|
onHideMenuClick,
|
||||||
onShowMenuClick,
|
onShowMenuClick,
|
||||||
|
@ -1,40 +1,27 @@
|
|||||||
import { WorkspaceUser } from '../WorkspaceUser';
|
import { WorkspaceUser } from '../WorkspaceUser';
|
||||||
import { AppLogo } from '../AppLogo';
|
import { AppLogo } from '../AppLogo';
|
||||||
import { FolderItem } from './FolderItem';
|
|
||||||
import { TrashButton } from './TrashButton';
|
import { TrashButton } from './TrashButton';
|
||||||
import { NewFolderButton } from './NewFolderButton';
|
import { NewViewButton } from './NewViewButton';
|
||||||
import { NavigationResizer } from './NavigationResizer';
|
import { NavigationResizer } from './NavigationResizer';
|
||||||
import { IFolder } from '$app_reducers/folders/slice';
|
|
||||||
import { IPage } from '$app_reducers/pages/slice';
|
import { IPage } from '$app_reducers/pages/slice';
|
||||||
import { useLocation, useNavigate } from 'react-router-dom';
|
import { useLocation, useNavigate } from 'react-router-dom';
|
||||||
import React, { useEffect, useRef, useState } from 'react';
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
import { useAppSelector } from '$app/stores/store';
|
import { useAppSelector } from '$app/stores/store';
|
||||||
import {
|
import { NavItem } from '$app/components/layout/NavigationPanel/NavItem';
|
||||||
ANIMATION_DURATION,
|
import { ANIMATION_DURATION, NAV_PANEL_MINIMUM_WIDTH, PAGE_ITEM_HEIGHT } from '../../_shared/constants';
|
||||||
FOLDER_MARGIN,
|
|
||||||
INITIAL_FOLDER_HEIGHT,
|
|
||||||
NAV_PANEL_MINIMUM_WIDTH,
|
|
||||||
PAGE_ITEM_HEIGHT,
|
|
||||||
} from '../../_shared/constants';
|
|
||||||
|
|
||||||
export const NavigationPanel = ({
|
export const NavigationPanel = ({
|
||||||
onHideMenuClick,
|
onHideMenuClick,
|
||||||
menuHidden,
|
menuHidden,
|
||||||
width,
|
width,
|
||||||
folders,
|
|
||||||
pages,
|
|
||||||
onPageClick,
|
|
||||||
}: {
|
}: {
|
||||||
onHideMenuClick: () => void;
|
onHideMenuClick: () => void;
|
||||||
menuHidden: boolean;
|
menuHidden: boolean;
|
||||||
width: number;
|
width: number;
|
||||||
folders: IFolder[];
|
|
||||||
pages: IPage[];
|
|
||||||
onPageClick: (page: IPage) => void;
|
|
||||||
}) => {
|
}) => {
|
||||||
const el = useRef<HTMLDivElement>(null);
|
const el = useRef<HTMLDivElement>(null);
|
||||||
const foldersStore = useAppSelector((state) => state.folders);
|
const pages = useAppSelector((state) => state.pages);
|
||||||
const pagesStore = useAppSelector((state) => state.pages);
|
const workspace = useAppSelector((state) => state.workspace);
|
||||||
const [activePageId, setActivePageId] = useState<string>('');
|
const [activePageId, setActivePageId] = useState<string>('');
|
||||||
const currentLocation = useLocation();
|
const currentLocation = useLocation();
|
||||||
const [maxHeight, setMaxHeight] = useState(0);
|
const [maxHeight, setMaxHeight] = useState(0);
|
||||||
@ -47,44 +34,8 @@ export const NavigationPanel = ({
|
|||||||
}, [currentLocation]);
|
}, [currentLocation]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setTimeout(() => {
|
setMaxHeight(pages.length * PAGE_ITEM_HEIGHT);
|
||||||
if (!el.current) return;
|
}, [pages]);
|
||||||
if (!activePageId?.length) return;
|
|
||||||
const activePage = pagesStore.find((page) => page.id === activePageId);
|
|
||||||
if (!activePage) return;
|
|
||||||
|
|
||||||
const folderIndex = foldersStore.findIndex((folder) => folder.id === activePage.folderId);
|
|
||||||
if (folderIndex === -1) return;
|
|
||||||
|
|
||||||
let height = 0;
|
|
||||||
for (let i = 0; i < folderIndex; i++) {
|
|
||||||
height += INITIAL_FOLDER_HEIGHT + FOLDER_MARGIN;
|
|
||||||
if (foldersStore[i].showPages) {
|
|
||||||
height += pagesStore.filter((p) => p.folderId === foldersStore[i].id).length * PAGE_ITEM_HEIGHT;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
height += INITIAL_FOLDER_HEIGHT + FOLDER_MARGIN / 2;
|
|
||||||
|
|
||||||
const pageIndex = pagesStore
|
|
||||||
.filter((p) => p.folderId === foldersStore[folderIndex].id)
|
|
||||||
.findIndex((p) => p.id === activePageId);
|
|
||||||
for (let i = 0; i <= pageIndex; i++) {
|
|
||||||
height += PAGE_ITEM_HEIGHT;
|
|
||||||
}
|
|
||||||
|
|
||||||
const elHeight = el.current.getBoundingClientRect().height;
|
|
||||||
const scrollTop = el.current.scrollTop;
|
|
||||||
|
|
||||||
if (scrollTop + elHeight < height || scrollTop > height) {
|
|
||||||
el.current.scrollTo({ top: height - elHeight, behavior: 'smooth' });
|
|
||||||
}
|
|
||||||
}, ANIMATION_DURATION);
|
|
||||||
}, [activePageId]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setMaxHeight(foldersStore.length * (INITIAL_FOLDER_HEIGHT + FOLDER_MARGIN) + pagesStore.length * PAGE_ITEM_HEIGHT);
|
|
||||||
}, [foldersStore, pagesStore]);
|
|
||||||
|
|
||||||
const scrollDown = () => {
|
const scrollDown = () => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@ -113,7 +64,7 @@ export const NavigationPanel = ({
|
|||||||
}}
|
}}
|
||||||
ref={el}
|
ref={el}
|
||||||
>
|
>
|
||||||
<WorkspaceApps folders={folders} pages={pages} onPageClick={onPageClick} />
|
<WorkspaceApps pages={pages.filter((p) => p.parentPageId === workspace.id)} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -130,8 +81,8 @@ export const NavigationPanel = ({
|
|||||||
<TrashButton></TrashButton>
|
<TrashButton></TrashButton>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/*New Folder Button*/}
|
{/*New Root View Button*/}
|
||||||
<NewFolderButton scrollDown={scrollDown}></NewFolderButton>
|
<NewViewButton scrollDown={scrollDown}></NewViewButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<NavigationResizer minWidth={NAV_PANEL_MINIMUM_WIDTH}></NavigationResizer>
|
<NavigationResizer minWidth={NAV_PANEL_MINIMUM_WIDTH}></NavigationResizer>
|
||||||
@ -139,21 +90,10 @@ export const NavigationPanel = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
type AppsContext = {
|
const WorkspaceApps: React.FC<{ pages: IPage[] }> = ({ pages }) => (
|
||||||
folders: IFolder[];
|
|
||||||
pages: IPage[];
|
|
||||||
onPageClick: (page: IPage) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
const WorkspaceApps: React.FC<AppsContext> = ({ folders, pages, onPageClick }) => (
|
|
||||||
<>
|
<>
|
||||||
{folders.map((folder, index) => (
|
{pages.map((page, index) => (
|
||||||
<FolderItem
|
<NavItem key={index} page={page}></NavItem>
|
||||||
key={index}
|
|
||||||
folder={folder}
|
|
||||||
pages={pages.filter((page) => page.folderId === folder.id)}
|
|
||||||
onPageClick={onPageClick}
|
|
||||||
></FolderItem>
|
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -1,20 +0,0 @@
|
|||||||
import { useAppDispatch, useAppSelector } from '$app/stores/store';
|
|
||||||
import { foldersActions } from '$app_reducers/folders/slice';
|
|
||||||
import { WorkspaceBackendService } from '$app/stores/effects/folder/workspace/workspace_bd_svc';
|
|
||||||
|
|
||||||
export const useNewFolder = () => {
|
|
||||||
const appDispatch = useAppDispatch();
|
|
||||||
const workspace = useAppSelector((state) => state.workspace);
|
|
||||||
const workspaceBackendService = new WorkspaceBackendService(workspace.id ?? '');
|
|
||||||
|
|
||||||
const onNewFolder = async () => {
|
|
||||||
const newApp = await workspaceBackendService.createApp({
|
|
||||||
name: 'New Folder 1',
|
|
||||||
});
|
|
||||||
appDispatch(foldersActions.addFolder({ id: newApp.id, title: newApp.name }));
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
onNewFolder,
|
|
||||||
};
|
|
||||||
};
|
|
@ -0,0 +1,45 @@
|
|||||||
|
import { useAppDispatch, useAppSelector } from '$app/stores/store';
|
||||||
|
import { WorkspaceBackendService } from '$app/stores/effects/folder/workspace/workspace_bd_svc';
|
||||||
|
import { ViewLayoutPB } from '@/services/backend';
|
||||||
|
import { pagesActions } from '$app_reducers/pages/slice';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
|
export const useNewRootView = () => {
|
||||||
|
const appDispatch = useAppDispatch();
|
||||||
|
const workspace = useAppSelector((state) => state.workspace);
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const onNewRootView = async () => {
|
||||||
|
if (!workspace.id) return;
|
||||||
|
const workspaceBackendService = new WorkspaceBackendService(workspace.id);
|
||||||
|
|
||||||
|
// in future should show options for new page type
|
||||||
|
const defaultType = ViewLayoutPB.Document;
|
||||||
|
const defaultName = 'Document Page 1';
|
||||||
|
const defaultRoute = 'document';
|
||||||
|
|
||||||
|
const result = await workspaceBackendService.createView({
|
||||||
|
parentViewId: workspace.id,
|
||||||
|
layoutType: defaultType,
|
||||||
|
name: defaultName,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.ok) {
|
||||||
|
const newView = result.val;
|
||||||
|
appDispatch(
|
||||||
|
pagesActions.addPage({
|
||||||
|
parentPageId: workspace.id,
|
||||||
|
id: newView.id,
|
||||||
|
title: newView.name,
|
||||||
|
showPagesInside: false,
|
||||||
|
pageType: defaultType,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
navigate(`/page/${defaultRoute}/${newView.id}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
onNewRootView,
|
||||||
|
};
|
||||||
|
};
|
@ -1,13 +1,13 @@
|
|||||||
import AddSvg from '../../_shared/svg/AddSvg';
|
import AddSvg from '../../_shared/svg/AddSvg';
|
||||||
import { useNewFolder } from './NewFolderButton.hooks';
|
import { useNewRootView } from './NewViewButton.hooks';
|
||||||
|
|
||||||
export const NewFolderButton = ({ scrollDown }: { scrollDown: () => void }) => {
|
export const NewViewButton = ({ scrollDown }: { scrollDown: () => void }) => {
|
||||||
const { onNewFolder } = useNewFolder();
|
const { onNewRootView } = useNewRootView();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
void onNewFolder();
|
void onNewRootView();
|
||||||
scrollDown();
|
scrollDown();
|
||||||
}}
|
}}
|
||||||
className={'flex h-[50px] w-full items-center px-6 hover:bg-surface-2'}
|
className={'flex h-[50px] w-full items-center px-6 hover:bg-surface-2'}
|
||||||
@ -17,7 +17,7 @@ export const NewFolderButton = ({ scrollDown }: { scrollDown: () => void }) => {
|
|||||||
<AddSvg></AddSvg>
|
<AddSvg></AddSvg>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<span>New Folder</span>
|
<span>New View</span>
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
};
|
};
|
@ -1,68 +0,0 @@
|
|||||||
import { IPage, pagesActions } from '$app_reducers/pages/slice';
|
|
||||||
import { useAppDispatch } from '$app/stores/store';
|
|
||||||
import { useEffect, useState } from 'react';
|
|
||||||
import { ViewBackendService } from '$app/stores/effects/folder/view/view_bd_svc';
|
|
||||||
import { useLocation } from 'react-router-dom';
|
|
||||||
import { ViewPB } from '@/services/backend';
|
|
||||||
|
|
||||||
export const usePageEvents = (page: IPage) => {
|
|
||||||
const appDispatch = useAppDispatch();
|
|
||||||
const [showPageOptions, setShowPageOptions] = useState(false);
|
|
||||||
const [showRenamePopup, setShowRenamePopup] = useState(false);
|
|
||||||
const [activePageId, setActivePageId] = useState<string>('');
|
|
||||||
const currentLocation = useLocation();
|
|
||||||
const viewBackendService: ViewBackendService = new ViewBackendService(page.id);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const { pathname } = currentLocation;
|
|
||||||
const parts = pathname.split('/');
|
|
||||||
const pageId = parts[parts.length - 1];
|
|
||||||
setActivePageId(pageId);
|
|
||||||
}, [currentLocation]);
|
|
||||||
|
|
||||||
const onPageOptionsClick = () => {
|
|
||||||
setShowPageOptions(!showPageOptions);
|
|
||||||
};
|
|
||||||
|
|
||||||
const startPageRename = () => {
|
|
||||||
setShowRenamePopup(true);
|
|
||||||
closePopup();
|
|
||||||
};
|
|
||||||
|
|
||||||
const changePageTitle = async (newTitle: string) => {
|
|
||||||
await viewBackendService.update({ name: newTitle });
|
|
||||||
appDispatch(pagesActions.renamePage({ id: page.id, newTitle }));
|
|
||||||
};
|
|
||||||
|
|
||||||
const deletePage = async () => {
|
|
||||||
closePopup();
|
|
||||||
await viewBackendService.delete();
|
|
||||||
appDispatch(pagesActions.deletePage({ id: page.id }));
|
|
||||||
};
|
|
||||||
|
|
||||||
const duplicatePage = async () => {
|
|
||||||
closePopup();
|
|
||||||
await viewBackendService.duplicate(ViewPB.fromObject(page));
|
|
||||||
};
|
|
||||||
|
|
||||||
const closePopup = () => {
|
|
||||||
setShowPageOptions(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const closeRenamePopup = () => {
|
|
||||||
setShowRenamePopup(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
showPageOptions,
|
|
||||||
onPageOptionsClick,
|
|
||||||
showRenamePopup,
|
|
||||||
startPageRename,
|
|
||||||
changePageTitle,
|
|
||||||
deletePage,
|
|
||||||
duplicatePage,
|
|
||||||
closePopup,
|
|
||||||
closeRenamePopup,
|
|
||||||
activePageId,
|
|
||||||
};
|
|
||||||
};
|
|
@ -1,83 +0,0 @@
|
|||||||
import { DocumentSvg } from '../../_shared/svg/DocumentSvg';
|
|
||||||
import { BoardSvg } from '../../_shared/svg/BoardSvg';
|
|
||||||
import { GridSvg } from '../../_shared/svg/GridSvg';
|
|
||||||
import { Details2Svg } from '../../_shared/svg/Details2Svg';
|
|
||||||
import { NavItemOptionsPopup } from './NavItemOptionsPopup';
|
|
||||||
import { IPage } from '$app_reducers/pages/slice';
|
|
||||||
import { Button } from '../../_shared/Button';
|
|
||||||
import { usePageEvents } from './PageItem.hooks';
|
|
||||||
import { RenamePopup } from './RenamePopup';
|
|
||||||
import { ViewLayoutPB } from '@/services/backend';
|
|
||||||
import { useEffect, useRef, useState } from 'react';
|
|
||||||
import { PAGE_ITEM_HEIGHT } from '../../_shared/constants';
|
|
||||||
|
|
||||||
export const PageItem = ({ page, onPageClick }: { page: IPage; onPageClick: () => void }) => {
|
|
||||||
const {
|
|
||||||
showPageOptions,
|
|
||||||
onPageOptionsClick,
|
|
||||||
showRenamePopup,
|
|
||||||
startPageRename,
|
|
||||||
changePageTitle,
|
|
||||||
deletePage,
|
|
||||||
duplicatePage,
|
|
||||||
closePopup,
|
|
||||||
closeRenamePopup,
|
|
||||||
activePageId,
|
|
||||||
} = usePageEvents(page);
|
|
||||||
|
|
||||||
const el = useRef<HTMLDivElement>(null);
|
|
||||||
|
|
||||||
const [popupY, setPopupY] = useState(0);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (el.current) {
|
|
||||||
const { top } = el.current.getBoundingClientRect();
|
|
||||||
setPopupY(top);
|
|
||||||
}
|
|
||||||
}, [showPageOptions, showRenamePopup]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div ref={el}>
|
|
||||||
<div
|
|
||||||
onClick={() => onPageClick()}
|
|
||||||
className={`flex cursor-pointer items-center justify-between rounded-lg pl-8 pr-4 hover:bg-surface-2 ${
|
|
||||||
activePageId === page.id ? 'bg-surface-2' : ''
|
|
||||||
}`}
|
|
||||||
style={{ height: PAGE_ITEM_HEIGHT }}
|
|
||||||
>
|
|
||||||
<button className={'flex min-w-0 flex-1 items-center'}>
|
|
||||||
<i className={'ml-1 mr-1 h-[16px] w-[16px]'}>
|
|
||||||
{page.pageType === ViewLayoutPB.Document && <DocumentSvg></DocumentSvg>}
|
|
||||||
{page.pageType === ViewLayoutPB.Board && <BoardSvg></BoardSvg>}
|
|
||||||
{page.pageType === ViewLayoutPB.Grid && <GridSvg></GridSvg>}
|
|
||||||
</i>
|
|
||||||
<span className={'ml-2 min-w-0 flex-1 overflow-hidden overflow-ellipsis whitespace-nowrap text-left'}>
|
|
||||||
{page.title}
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
<div className={'flex items-center'}>
|
|
||||||
<Button size={'box-small-transparent'} onClick={() => onPageOptionsClick()}>
|
|
||||||
<Details2Svg></Details2Svg>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{showPageOptions && (
|
|
||||||
<NavItemOptionsPopup
|
|
||||||
onRenameClick={() => startPageRename()}
|
|
||||||
onDeleteClick={() => deletePage()}
|
|
||||||
onDuplicateClick={() => duplicatePage()}
|
|
||||||
onClose={() => closePopup()}
|
|
||||||
top={popupY - 124 + 40}
|
|
||||||
></NavItemOptionsPopup>
|
|
||||||
)}
|
|
||||||
{showRenamePopup && (
|
|
||||||
<RenamePopup
|
|
||||||
value={page.title}
|
|
||||||
onChange={(newTitle) => changePageTitle(newTitle)}
|
|
||||||
onClose={closeRenamePopup}
|
|
||||||
top={popupY - 124 + 40}
|
|
||||||
></RenamePopup>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
@ -1,31 +1,17 @@
|
|||||||
import React, { ReactNode, useEffect } from 'react';
|
import React, { ReactNode } from 'react';
|
||||||
import { NavigationPanel } from './NavigationPanel/NavigationPanel';
|
import { NavigationPanel } from './NavigationPanel/NavigationPanel';
|
||||||
import { MainPanel } from './MainPanel';
|
import { MainPanel } from './MainPanel';
|
||||||
import { useNavigationPanelHooks } from './NavigationPanel/NavigationPanel.hooks';
|
import { useNavigationPanelHooks } from './NavigationPanel/NavigationPanel.hooks';
|
||||||
import { useWorkspace } from './Workspace.hooks';
|
import { useWorkspace } from './Workspace.hooks';
|
||||||
import { useAppSelector } from '$app/stores/store';
|
|
||||||
|
|
||||||
export const Screen = ({ children }: { children: ReactNode }) => {
|
export const Screen = ({ children }: { children: ReactNode }) => {
|
||||||
const currentUser = useAppSelector((state) => state.currentUser);
|
useWorkspace();
|
||||||
const { loadWorkspaceItems } = useWorkspace();
|
|
||||||
useEffect(() => {
|
|
||||||
void (async () => {
|
|
||||||
await loadWorkspaceItems();
|
|
||||||
})();
|
|
||||||
}, [currentUser.isAuthenticated]);
|
|
||||||
|
|
||||||
const { width, folders, pages, onPageClick, onHideMenuClick, onShowMenuClick, menuHidden } = useNavigationPanelHooks();
|
const { width, onHideMenuClick, onShowMenuClick, menuHidden } = useNavigationPanelHooks();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='flex h-screen w-screen bg-white text-black'>
|
<div className='flex h-screen w-screen bg-white text-black'>
|
||||||
<NavigationPanel
|
<NavigationPanel onHideMenuClick={onHideMenuClick} width={width} menuHidden={menuHidden}></NavigationPanel>
|
||||||
onHideMenuClick={onHideMenuClick}
|
|
||||||
width={width}
|
|
||||||
folders={folders}
|
|
||||||
pages={pages}
|
|
||||||
onPageClick={onPageClick}
|
|
||||||
menuHidden={menuHidden}
|
|
||||||
></NavigationPanel>
|
|
||||||
|
|
||||||
<MainPanel left={width} menuHidden={menuHidden} onShowMenuClick={onShowMenuClick}>
|
<MainPanel left={width} menuHidden={menuHidden} onShowMenuClick={onShowMenuClick}>
|
||||||
{children}
|
{children}
|
||||||
|
@ -1,52 +1,69 @@
|
|||||||
import { foldersActions } from '$app_reducers/folders/slice';
|
import { foldersActions } from '$app_reducers/folders/slice';
|
||||||
import { useAppDispatch, useAppSelector } from '$app/stores/store';
|
import { useAppDispatch, useAppSelector } from '$app/stores/store';
|
||||||
import { pagesActions } from '$app_reducers/pages/slice';
|
import { IPage, pagesActions } from '$app_reducers/pages/slice';
|
||||||
import { workspaceActions } from '$app_reducers/workspace/slice';
|
import { workspaceActions } from '$app_reducers/workspace/slice';
|
||||||
import { UserBackendService } from '$app/stores/effects/user/user_bd_svc';
|
import { UserBackendService } from '$app/stores/effects/user/user_bd_svc';
|
||||||
import { AppBackendService } from '$app/stores/effects/folder/app/app_bd_svc';
|
import { useEffect, useState } from 'react';
|
||||||
import { Log } from '$app/utils/log';
|
import { WorkspaceBackendService } from '$app/stores/effects/folder/workspace/workspace_bd_svc';
|
||||||
|
|
||||||
export const useWorkspace = () => {
|
export const useWorkspace = () => {
|
||||||
const currentUser = useAppSelector((state) => state.currentUser);
|
const currentUser = useAppSelector((state) => state.currentUser);
|
||||||
|
|
||||||
const appDispatch = useAppDispatch();
|
const appDispatch = useAppDispatch();
|
||||||
|
|
||||||
const userBackendService: UserBackendService = new UserBackendService(currentUser.id ?? 0);
|
const [userService, setUserService] = useState<UserBackendService | null>(null);
|
||||||
|
const [workspaceService, setWorkspaceService] = useState<WorkspaceBackendService | null>(null);
|
||||||
|
const [isReady, setIsReady] = useState(false);
|
||||||
|
|
||||||
const loadWorkspaceItems = async () => {
|
useEffect(() => {
|
||||||
try {
|
if (currentUser.id) {
|
||||||
const workspaceSettingPB = await userBackendService.getCurrentWorkspace();
|
setUserService(new UserBackendService(currentUser.id));
|
||||||
const workspace = workspaceSettingPB.workspace;
|
|
||||||
appDispatch(workspaceActions.updateWorkspace({ id: workspace.id, name: workspace.name }));
|
|
||||||
appDispatch(foldersActions.clearFolders());
|
|
||||||
appDispatch(pagesActions.clearPages());
|
|
||||||
|
|
||||||
const apps = workspace.views;
|
|
||||||
for (const app of apps) {
|
|
||||||
appDispatch(foldersActions.addFolder({ id: app.id, title: app.name }));
|
|
||||||
const service = new AppBackendService(app.id);
|
|
||||||
const result = await service.getAllViews();
|
|
||||||
if (result.ok) {
|
|
||||||
for (const view of result.val) {
|
|
||||||
appDispatch(
|
|
||||||
pagesActions.addPage({ folderId: app.id, id: view.id, pageType: view.layout, title: view.name })
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Log.error('Failed to get views, folderId: ' + app.id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e1) {
|
|
||||||
// create workspace for first start
|
|
||||||
const workspace = await userBackendService.createWorkspace({ name: 'New Workspace', desc: '' });
|
|
||||||
appDispatch(workspaceActions.updateWorkspace({ id: workspace.id, name: workspace.name }));
|
|
||||||
|
|
||||||
appDispatch(foldersActions.clearFolders());
|
|
||||||
appDispatch(pagesActions.clearPages());
|
|
||||||
}
|
}
|
||||||
};
|
}, [currentUser]);
|
||||||
|
|
||||||
return {
|
useEffect(() => {
|
||||||
loadWorkspaceItems,
|
if (!userService) return;
|
||||||
};
|
|
||||||
|
void (async () => {
|
||||||
|
try {
|
||||||
|
const workspaceSettingPB = await userService.getCurrentWorkspace();
|
||||||
|
const workspace = workspaceSettingPB.workspace;
|
||||||
|
appDispatch(workspaceActions.updateWorkspace({ id: workspace.id, name: workspace.name }));
|
||||||
|
appDispatch(foldersActions.clearFolders());
|
||||||
|
appDispatch(pagesActions.clearPages());
|
||||||
|
|
||||||
|
setWorkspaceService(new WorkspaceBackendService(workspace.id));
|
||||||
|
} catch (e1) {
|
||||||
|
// create workspace for first start
|
||||||
|
const workspace = await userService.createWorkspace({ name: 'New Workspace', desc: '' });
|
||||||
|
appDispatch(workspaceActions.updateWorkspace({ id: workspace.id, name: workspace.name }));
|
||||||
|
|
||||||
|
appDispatch(foldersActions.clearFolders());
|
||||||
|
appDispatch(pagesActions.clearPages());
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}, [userService]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!workspaceService) return;
|
||||||
|
void (async () => {
|
||||||
|
const rootViews = await workspaceService.getAllViews();
|
||||||
|
if (rootViews.ok) {
|
||||||
|
appDispatch(
|
||||||
|
pagesActions.addInsidePages({
|
||||||
|
currentPageId: workspaceService.workspaceId,
|
||||||
|
insidePages: rootViews.val.map<IPage>((v) => ({
|
||||||
|
id: v.id,
|
||||||
|
title: v.name,
|
||||||
|
pageType: v.layout,
|
||||||
|
showPagesInside: false,
|
||||||
|
parentPageId: workspaceService.workspaceId,
|
||||||
|
})),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
setIsReady(true);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}, [workspaceService]);
|
||||||
|
|
||||||
|
return {};
|
||||||
};
|
};
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
import {
|
import {
|
||||||
FieldType,
|
FieldType,
|
||||||
|
FlowyError,
|
||||||
SingleSelectTypeOptionPB,
|
SingleSelectTypeOptionPB,
|
||||||
ViewLayoutPB,
|
ViewLayoutPB,
|
||||||
ViewPB,
|
ViewPB,
|
||||||
WorkspaceSettingPB,
|
WorkspaceSettingPB,
|
||||||
} from '../../../services/backend';
|
} from '../../../services/backend';
|
||||||
import { FolderEventGetCurrentWorkspace } from '../../../services/backend/events/flowy-folder2';
|
import { FolderEventGetCurrentWorkspace } from '../../../services/backend/events/flowy-folder2';
|
||||||
import { AppBackendService } from '../../stores/effects/folder/app/app_bd_svc';
|
|
||||||
import { DatabaseController } from '../../stores/effects/database/database_controller';
|
import { DatabaseController } from '../../stores/effects/database/database_controller';
|
||||||
import { RowInfo } from '../../stores/effects/database/row/row_cache';
|
import { RowInfo } from '../../stores/effects/database/row/row_cache';
|
||||||
import { RowController } from '../../stores/effects/database/row/row_controller';
|
import { RowController } from '../../stores/effects/database/row/row_controller';
|
||||||
@ -19,7 +19,7 @@ import {
|
|||||||
TextCellController,
|
TextCellController,
|
||||||
URLCellController,
|
URLCellController,
|
||||||
} from '../../stores/effects/database/cell/controller_builder';
|
} from '../../stores/effects/database/cell/controller_builder';
|
||||||
import { None, Option, Some } from 'ts-results';
|
import { None, Ok, Option, Result, Some } from 'ts-results';
|
||||||
import { TypeOptionBackendService } from '../../stores/effects/database/field/type_option/type_option_bd_svc';
|
import { TypeOptionBackendService } from '../../stores/effects/database/field/type_option/type_option_bd_svc';
|
||||||
import { DatabaseBackendService } from '../../stores/effects/database/database_bd_svc';
|
import { DatabaseBackendService } from '../../stores/effects/database/database_bd_svc';
|
||||||
import { FieldInfo } from '../../stores/effects/database/field/field_controller';
|
import { FieldInfo } from '../../stores/effects/database/field/field_controller';
|
||||||
@ -27,13 +27,20 @@ import { TypeOptionController } from '../../stores/effects/database/field/type_o
|
|||||||
import { makeSingleSelectTypeOptionContext } from '../../stores/effects/database/field/type_option/type_option_context';
|
import { makeSingleSelectTypeOptionContext } from '../../stores/effects/database/field/type_option/type_option_context';
|
||||||
import { SelectOptionBackendService } from '../../stores/effects/database/cell/select_option_bd_svc';
|
import { SelectOptionBackendService } from '../../stores/effects/database/cell/select_option_bd_svc';
|
||||||
import { Log } from '$app/utils/log';
|
import { Log } from '$app/utils/log';
|
||||||
|
import { ViewBackendService } from '$app/stores/effects/folder/view/view_bd_svc';
|
||||||
|
import { WorkspaceBackendService } from '$app/stores/effects/folder/workspace/workspace_bd_svc';
|
||||||
|
|
||||||
// Create a database view for specific layout type
|
// Create a database view for specific layout type
|
||||||
// Do not use it production code. Just for testing
|
// Do not use it production code. Just for testing
|
||||||
export async function createTestDatabaseView(layout: ViewLayoutPB): Promise<ViewPB> {
|
export async function createTestDatabaseView(layout: ViewLayoutPB): Promise<ViewPB> {
|
||||||
const workspaceSetting: WorkspaceSettingPB = await FolderEventGetCurrentWorkspace().then((result) => result.unwrap());
|
const workspaceSetting: WorkspaceSettingPB = await FolderEventGetCurrentWorkspace().then((result) => result.unwrap());
|
||||||
const appService = new AppBackendService(workspaceSetting.workspace.id);
|
const wsSvc = new WorkspaceBackendService(workspaceSetting.workspace.id);
|
||||||
return await appService.createView({ name: 'New Grid', layoutType: layout });
|
const viewRes = await wsSvc.createView({ name: 'New Grid', layoutType: layout });
|
||||||
|
if (viewRes.ok) {
|
||||||
|
return viewRes.val;
|
||||||
|
} else {
|
||||||
|
throw Error(viewRes.val.msg);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function openTestDatabase(viewId: string): Promise<DatabaseController> {
|
export async function openTestDatabase(viewId: string): Promise<DatabaseController> {
|
||||||
|
@ -1,10 +1,15 @@
|
|||||||
import { ViewLayoutPB, WorkspaceSettingPB } from '@/services/backend';
|
import { ViewLayoutPB, WorkspaceSettingPB } from '@/services/backend';
|
||||||
import { FolderEventGetCurrentWorkspace } from '@/services/backend/events/flowy-folder2';
|
import { FolderEventGetCurrentWorkspace } from '@/services/backend/events/flowy-folder2';
|
||||||
import { AppBackendService } from '$app/stores/effects/folder/app/app_bd_svc';
|
import {WorkspaceBackendService} from "$app/stores/effects/folder/workspace/workspace_bd_svc";
|
||||||
|
|
||||||
export async function createTestDocument() {
|
export async function createTestDocument() {
|
||||||
const workspaceSetting: WorkspaceSettingPB = await FolderEventGetCurrentWorkspace().then((result) => result.unwrap());
|
const workspaceSetting: WorkspaceSettingPB = await FolderEventGetCurrentWorkspace().then((result) => result.unwrap());
|
||||||
const app = workspaceSetting.workspace.views[0];
|
const appService = new WorkspaceBackendService(workspaceSetting.workspace.id);
|
||||||
const appService = new AppBackendService(app.id);
|
const result = await appService.createView({ name: 'New Document', layoutType: ViewLayoutPB.Document });
|
||||||
return await appService.createView({ name: 'New Document', layoutType: ViewLayoutPB.Document });
|
if (result.ok) {
|
||||||
|
return result.val;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
throw Error(result.val.msg);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -29,6 +29,7 @@ import {
|
|||||||
TestMoveKanbanBoardRow,
|
TestMoveKanbanBoardRow,
|
||||||
} from './TestGroup';
|
} from './TestGroup';
|
||||||
import { TestCreateDocument } from './TestDocument';
|
import { TestCreateDocument } from './TestDocument';
|
||||||
|
import { TestCreateViews } from '$app/components/tests/TestFolder';
|
||||||
|
|
||||||
export const TestAPI = () => {
|
export const TestAPI = () => {
|
||||||
return (
|
return (
|
||||||
@ -62,6 +63,8 @@ export const TestAPI = () => {
|
|||||||
<TestMoveKanbanBoardColumn></TestMoveKanbanBoardColumn>
|
<TestMoveKanbanBoardColumn></TestMoveKanbanBoardColumn>
|
||||||
<TestCreateKanbanBoardColumn></TestCreateKanbanBoardColumn>
|
<TestCreateKanbanBoardColumn></TestCreateKanbanBoardColumn>
|
||||||
<TestCreateDocument></TestCreateDocument>
|
<TestCreateDocument></TestCreateDocument>
|
||||||
|
{/*Folders*/}
|
||||||
|
<TestCreateViews></TestCreateViews>
|
||||||
</ul>
|
</ul>
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
|
@ -0,0 +1,61 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { UserBackendService } from '$app/stores/effects/user/user_bd_svc';
|
||||||
|
import { useAppSelector } from '$app/stores/store';
|
||||||
|
import { WorkspaceBackendService } from '$app/stores/effects/folder/workspace/workspace_bd_svc';
|
||||||
|
import { ViewLayoutPB, ViewPB } from '@/services/backend';
|
||||||
|
|
||||||
|
const testCreateFolder = async (userId?: number) => {
|
||||||
|
if (!userId) {
|
||||||
|
console.log('user is not logged in');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log('test create views');
|
||||||
|
const userBackendService: UserBackendService = new UserBackendService(userId);
|
||||||
|
const workspaces = await userBackendService.getWorkspaces();
|
||||||
|
if (workspaces.ok) {
|
||||||
|
console.log('workspaces: ', workspaces.val.toObject());
|
||||||
|
}
|
||||||
|
const currentWorkspace = await userBackendService.getCurrentWorkspace();
|
||||||
|
|
||||||
|
const workspaceService = new WorkspaceBackendService(currentWorkspace.workspace.id);
|
||||||
|
const rootViews: ViewPB[] = [];
|
||||||
|
for (let i = 1; i <= 3; i++) {
|
||||||
|
const result = await workspaceService.createView({
|
||||||
|
name: `test board ${i}`,
|
||||||
|
desc: 'test description',
|
||||||
|
layoutType: ViewLayoutPB.Board,
|
||||||
|
});
|
||||||
|
if (result.ok) {
|
||||||
|
rootViews.push(result.val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (let i = 1; i <= 3; i++) {
|
||||||
|
const result = await workspaceService.createView({
|
||||||
|
name: `test board 1 ${i}`,
|
||||||
|
desc: 'test description',
|
||||||
|
layoutType: ViewLayoutPB.Board,
|
||||||
|
parentViewId: rootViews[0].id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const allApps = await workspaceService.getAllViews();
|
||||||
|
console.log(allApps);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const TestCreateViews = () => {
|
||||||
|
const currentUser = useAppSelector((state) => state.currentUser);
|
||||||
|
|
||||||
|
return TestButton('Test create views', testCreateFolder, currentUser.id);
|
||||||
|
};
|
||||||
|
|
||||||
|
const TestButton = (title: string, onClick: (userId?: number) => void, userId?: number) => {
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<div>
|
||||||
|
<button className='rounded-md bg-pink-200 p-4' type='button' onClick={() => onClick(userId)}>
|
||||||
|
{title}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
};
|
@ -30,7 +30,6 @@ import {
|
|||||||
import { SelectOptionCellBackendService } from '$app/stores/effects/database/cell/select_option_bd_svc';
|
import { SelectOptionCellBackendService } from '$app/stores/effects/database/cell/select_option_bd_svc';
|
||||||
import { TypeOptionController } from '$app/stores/effects/database/field/type_option/type_option_controller';
|
import { TypeOptionController } from '$app/stores/effects/database/field/type_option/type_option_controller';
|
||||||
import { None, Some } from 'ts-results';
|
import { None, Some } from 'ts-results';
|
||||||
import { RowBackendService } from '$app/stores/effects/database/row/row_bd_svc';
|
|
||||||
import {
|
import {
|
||||||
makeDateTypeOptionContext,
|
makeDateTypeOptionContext,
|
||||||
makeNumberTypeOptionContext,
|
makeNumberTypeOptionContext,
|
||||||
@ -250,7 +249,7 @@ async function testCreateRow() {
|
|||||||
await databaseController.open().then((result) => result.unwrap());
|
await databaseController.open().then((result) => result.unwrap());
|
||||||
await assertNumberOfRows(view.id, 3);
|
await assertNumberOfRows(view.id, 3);
|
||||||
|
|
||||||
// Create a row from a DatabaseController or create using the RowBackendService
|
// Create a row from a DatabaseController
|
||||||
await databaseController.createRow();
|
await databaseController.createRow();
|
||||||
await assertNumberOfRows(view.id, 4);
|
await assertNumberOfRows(view.id, 4);
|
||||||
await databaseController.dispose();
|
await databaseController.dispose();
|
||||||
@ -262,8 +261,7 @@ async function testDeleteRow() {
|
|||||||
await databaseController.open().then((result) => result.unwrap());
|
await databaseController.open().then((result) => result.unwrap());
|
||||||
|
|
||||||
const rows = databaseController.databaseViewCache.rowInfos;
|
const rows = databaseController.databaseViewCache.rowInfos;
|
||||||
const svc = new RowBackendService(view.id);
|
await databaseController.deleteRow(rows[0].row.id);
|
||||||
await svc.deleteRow(rows[0].row.id);
|
|
||||||
await assertNumberOfRows(view.id, 2);
|
await assertNumberOfRows(view.id, 2);
|
||||||
|
|
||||||
// Wait the databaseViewCache get the change notification and
|
// Wait the databaseViewCache get the change notification and
|
||||||
|
@ -101,7 +101,7 @@ async function moveKanbanBoardRow() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const row = firstGroup.rowAtIndex(0).unwrap();
|
const row = firstGroup.rowAtIndex(0).unwrap();
|
||||||
await databaseController.moveRow(row.id, secondGroup.groupId);
|
await databaseController.moveGroupRow(row.id, secondGroup.groupId);
|
||||||
|
|
||||||
assert(firstGroup.rows.length === 2);
|
assert(firstGroup.rows.length === 2);
|
||||||
await assertNumberOfRowsInGroup(view.id, firstGroup.groupId, 2);
|
await assertNumberOfRowsInGroup(view.id, firstGroup.groupId, 2);
|
||||||
|
@ -3,9 +3,9 @@ import { CellCache, CellCacheKey } from './cell_cache';
|
|||||||
import { CellDataLoader } from './data_parser';
|
import { CellDataLoader } from './data_parser';
|
||||||
import { CellDataPersistence } from './data_persistence';
|
import { CellDataPersistence } from './data_persistence';
|
||||||
import { FieldBackendService, TypeOptionParser } from '../field/field_bd_svc';
|
import { FieldBackendService, TypeOptionParser } from '../field/field_bd_svc';
|
||||||
import { ChangeNotifier } from '../../../../utils/change_notifier';
|
import { ChangeNotifier } from '$app/utils/change_notifier';
|
||||||
import { CellObserver } from './cell_observer';
|
import { CellObserver } from './cell_observer';
|
||||||
import { Log } from '../../../../utils/log';
|
import { Log } from '$app/utils/log';
|
||||||
import { Err, None, Ok, Option, Some } from 'ts-results';
|
import { Err, None, Ok, Option, Some } from 'ts-results';
|
||||||
import { DatabaseFieldObserver } from '../field/field_observer';
|
import { DatabaseFieldObserver } from '../field/field_observer';
|
||||||
|
|
||||||
@ -48,14 +48,14 @@ export class CellController<T, D> {
|
|||||||
|
|
||||||
/// 2.Listen on the field event and load the cell data if needed.
|
/// 2.Listen on the field event and load the cell data if needed.
|
||||||
void this.fieldNotifier.subscribe({
|
void this.fieldNotifier.subscribe({
|
||||||
onFieldChanged: () => {
|
onFieldChanged: async () => {
|
||||||
this.subscribeCallbacks?.onFieldChanged?.();
|
|
||||||
/// reloadOnFieldChanged should be true if you need to load the data when the corresponding field is changed
|
/// reloadOnFieldChanged should be true if you need to load the data when the corresponding field is changed
|
||||||
/// For example:
|
/// For example:
|
||||||
/// ¥12 -> $12
|
/// ¥12 -> $12
|
||||||
if (this.cellDataLoader.reloadOnFieldChanged) {
|
if (this.cellDataLoader.reloadOnFieldChanged) {
|
||||||
void this._loadCellData();
|
await this._loadCellData();
|
||||||
}
|
}
|
||||||
|
this.subscribeCallbacks?.onFieldChanged?.();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -97,24 +97,24 @@ export class CellController<T, D> {
|
|||||||
return cellData;
|
return cellData;
|
||||||
};
|
};
|
||||||
|
|
||||||
private _loadCellData = () => {
|
private _loadCellData = async () => {
|
||||||
return this.cellDataLoader.loadData().then((result) => {
|
const result = await this.cellDataLoader.loadData();
|
||||||
if (result.ok) {
|
if (result.ok) {
|
||||||
const cellData = result.val;
|
const cellData = result.val;
|
||||||
if (cellData.some) {
|
if (cellData.some) {
|
||||||
this.cellCache.insert(this.cacheKey, cellData.val);
|
this.cellCache.insert(this.cacheKey, cellData.val);
|
||||||
this.cellDataNotifier.cellData = cellData;
|
this.cellDataNotifier.cellData = cellData;
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.cellCache.remove(this.cacheKey);
|
|
||||||
this.cellDataNotifier.cellData = None;
|
|
||||||
}
|
}
|
||||||
});
|
} else {
|
||||||
|
this.cellCache.remove(this.cacheKey);
|
||||||
|
this.cellDataNotifier.cellData = None;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
dispose = async () => {
|
dispose = async () => {
|
||||||
await this.cellObserver.unsubscribe();
|
await this.cellObserver.unsubscribe();
|
||||||
await this.fieldNotifier.unsubscribe();
|
await this.fieldNotifier.unsubscribe();
|
||||||
|
this.cellDataNotifier.unsubscribe();
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
DatabaseEventCreateRow,
|
DatabaseEventCreateRow,
|
||||||
|
DatabaseEventDeleteRow,
|
||||||
|
DatabaseEventDuplicateRow,
|
||||||
DatabaseEventGetDatabase,
|
DatabaseEventGetDatabase,
|
||||||
DatabaseEventGetDatabaseSetting,
|
DatabaseEventGetDatabaseSetting,
|
||||||
DatabaseEventGetFields,
|
DatabaseEventGetFields,
|
||||||
@ -14,6 +16,7 @@ import {
|
|||||||
MoveGroupPayloadPB,
|
MoveGroupPayloadPB,
|
||||||
MoveGroupRowPayloadPB,
|
MoveGroupRowPayloadPB,
|
||||||
MoveRowPayloadPB,
|
MoveRowPayloadPB,
|
||||||
|
RowIdPB,
|
||||||
} from '@/services/backend/events/flowy-database2';
|
} from '@/services/backend/events/flowy-database2';
|
||||||
import {
|
import {
|
||||||
GetFieldPayloadPB,
|
GetFieldPayloadPB,
|
||||||
@ -64,6 +67,16 @@ export class DatabaseBackendService {
|
|||||||
return DatabaseEventCreateRow(payload);
|
return DatabaseEventCreateRow(payload);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
duplicateRow = async (rowId: string) => {
|
||||||
|
const payload = RowIdPB.fromObject({ view_id: this.viewId, row_id: rowId });
|
||||||
|
return DatabaseEventDuplicateRow(payload);
|
||||||
|
};
|
||||||
|
|
||||||
|
deleteRow = async (rowId: string) => {
|
||||||
|
const payload = RowIdPB.fromObject({ view_id: this.viewId, row_id: rowId });
|
||||||
|
return DatabaseEventDeleteRow(payload);
|
||||||
|
};
|
||||||
|
|
||||||
/// Move the row from one group to another group
|
/// Move the row from one group to another group
|
||||||
/// [toRowId] is used to locate the moving row location.
|
/// [toRowId] is used to locate the moving row location.
|
||||||
moveGroupRow = (fromRowId: string, toGroupId: string, toRowId?: string) => {
|
moveGroupRow = (fromRowId: string, toGroupId: string, toRowId?: string) => {
|
||||||
|
@ -89,11 +89,19 @@ export class DatabaseController {
|
|||||||
return this.backendService.createRow();
|
return this.backendService.createRow();
|
||||||
};
|
};
|
||||||
|
|
||||||
moveRow = (rowId: string, groupId: string) => {
|
duplicateRow = async (rowId: string) => {
|
||||||
|
return this.backendService.duplicateRow(rowId);
|
||||||
|
};
|
||||||
|
|
||||||
|
deleteRow = async (rowId: string) => {
|
||||||
|
return this.backendService.deleteRow(rowId);
|
||||||
|
};
|
||||||
|
|
||||||
|
moveGroupRow = (rowId: string, groupId: string) => {
|
||||||
return this.backendService.moveGroupRow(rowId, groupId);
|
return this.backendService.moveGroupRow(rowId, groupId);
|
||||||
};
|
};
|
||||||
|
|
||||||
exchangeRow = async (fromRowId: string, toGroupId: string, toRowId?: string) => {
|
exchangeGroupRow = async (fromRowId: string, toGroupId: string, toRowId?: string) => {
|
||||||
await this.backendService.moveGroupRow(fromRowId, toGroupId, toRowId);
|
await this.backendService.moveGroupRow(fromRowId, toGroupId, toRowId);
|
||||||
await this.loadGroup();
|
await this.loadGroup();
|
||||||
};
|
};
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import { Log } from "$app/utils/log";
|
import { Log } from '$app/utils/log';
|
||||||
import { DatabaseBackendService } from "../database_bd_svc";
|
import { DatabaseBackendService } from '../database_bd_svc';
|
||||||
import { DatabaseFieldChangesetObserver } from "./field_observer";
|
import { DatabaseFieldChangesetObserver } from './field_observer';
|
||||||
import { FieldIdPB, FieldPB, IndexFieldPB } from "@/services/backend";
|
import { FieldIdPB, FieldPB, IndexFieldPB } from '@/services/backend';
|
||||||
import { ChangeNotifier } from "$app/utils/change_notifier";
|
import { ChangeNotifier } from '$app/utils/change_notifier';
|
||||||
|
|
||||||
export class FieldController {
|
export class FieldController {
|
||||||
private backendService: DatabaseBackendService;
|
private backendService: DatabaseBackendService;
|
||||||
@ -53,7 +53,7 @@ export class FieldController {
|
|||||||
} else {
|
} else {
|
||||||
Log.error(result.val);
|
Log.error(result.val);
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -122,6 +122,5 @@ class NumOfFieldsNotifier extends ChangeNotifier<FieldInfo[]> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class FieldInfo {
|
export class FieldInfo {
|
||||||
constructor(public readonly field: FieldPB) {
|
constructor(public readonly field: FieldPB) {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,32 +0,0 @@
|
|||||||
import { CreateRowPayloadPB, RowIdPB } from '@/services/backend';
|
|
||||||
import {
|
|
||||||
DatabaseEventCreateRow,
|
|
||||||
DatabaseEventDeleteRow,
|
|
||||||
DatabaseEventDuplicateRow,
|
|
||||||
DatabaseEventGetRow,
|
|
||||||
} from '@/services/backend/events/flowy-database2';
|
|
||||||
|
|
||||||
export class RowBackendService {
|
|
||||||
constructor(public readonly viewId: string) {}
|
|
||||||
|
|
||||||
// Create a row below the row with rowId
|
|
||||||
createRow = (rowId: string) => {
|
|
||||||
const payload = CreateRowPayloadPB.fromObject({ view_id: this.viewId, start_row_id: rowId });
|
|
||||||
return DatabaseEventCreateRow(payload);
|
|
||||||
};
|
|
||||||
|
|
||||||
deleteRow = (rowId: string) => {
|
|
||||||
const payload = RowIdPB.fromObject({ view_id: this.viewId, row_id: rowId });
|
|
||||||
return DatabaseEventDeleteRow(payload);
|
|
||||||
};
|
|
||||||
|
|
||||||
duplicateRow = (rowId: string) => {
|
|
||||||
const payload = RowIdPB.fromObject({ view_id: this.viewId, row_id: rowId });
|
|
||||||
return DatabaseEventDuplicateRow(payload);
|
|
||||||
};
|
|
||||||
|
|
||||||
getRow = (rowId: string) => {
|
|
||||||
const payload = RowIdPB.fromObject({ view_id: this.viewId, row_id: rowId });
|
|
||||||
return DatabaseEventGetRow(payload);
|
|
||||||
};
|
|
||||||
}
|
|
@ -1,105 +0,0 @@
|
|||||||
import {
|
|
||||||
FolderEventCreateView,
|
|
||||||
FolderEventDeleteView,
|
|
||||||
FolderEventMoveView,
|
|
||||||
FolderEventReadView,
|
|
||||||
FolderEventUpdateView,
|
|
||||||
ViewLayoutPB,
|
|
||||||
} from '@/services/backend/events/flowy-folder2';
|
|
||||||
import {
|
|
||||||
CreateViewPayloadPB,
|
|
||||||
RepeatedViewIdPB,
|
|
||||||
ViewPB,
|
|
||||||
MoveViewPayloadPB,
|
|
||||||
FlowyError,
|
|
||||||
ViewIdPB,
|
|
||||||
UpdateViewPayloadPB,
|
|
||||||
} from '@/services/backend';
|
|
||||||
import { None, Result, Some } from 'ts-results';
|
|
||||||
|
|
||||||
export class AppBackendService {
|
|
||||||
constructor(public readonly appId: string) {}
|
|
||||||
|
|
||||||
getApp = () => {
|
|
||||||
const payload = ViewIdPB.fromObject({ value: this.appId });
|
|
||||||
return FolderEventReadView(payload);
|
|
||||||
};
|
|
||||||
|
|
||||||
createView = async (params: {
|
|
||||||
name: string;
|
|
||||||
desc?: string;
|
|
||||||
layoutType: ViewLayoutPB;
|
|
||||||
/// The initial data should be the JSON of the document
|
|
||||||
/// For example: {"document":{"type":"editor","children":[]}}
|
|
||||||
initialData?: string;
|
|
||||||
}) => {
|
|
||||||
const encoder = new TextEncoder();
|
|
||||||
const payload = CreateViewPayloadPB.fromObject({
|
|
||||||
parent_view_id: this.appId,
|
|
||||||
name: params.name,
|
|
||||||
desc: params.desc || '',
|
|
||||||
layout: params.layoutType,
|
|
||||||
initial_data: encoder.encode(params.initialData || ''),
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await FolderEventCreateView(payload);
|
|
||||||
|
|
||||||
if (result.ok) {
|
|
||||||
return result.val;
|
|
||||||
} else {
|
|
||||||
throw new Error(result.val.msg);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
getAllViews = (): Promise<Result<ViewPB[], FlowyError>> => {
|
|
||||||
const payload = ViewIdPB.fromObject({ value: this.appId });
|
|
||||||
return FolderEventReadView(payload).then((result) => {
|
|
||||||
return result.map((app) => app.child_views);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
getView = async (viewId: string) => {
|
|
||||||
const result = await this.getAllViews();
|
|
||||||
if (result.ok) {
|
|
||||||
const target = result.val.find((view) => view.id === viewId);
|
|
||||||
if (target !== undefined) {
|
|
||||||
return Some(target);
|
|
||||||
} else {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
update = async (params: { name: string }) => {
|
|
||||||
const payload = UpdateViewPayloadPB.fromObject({ view_id: this.appId, name: params.name });
|
|
||||||
const result = await FolderEventUpdateView(payload);
|
|
||||||
if (!result.ok) {
|
|
||||||
throw new Error(result.val.msg);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
delete = async () => {
|
|
||||||
const payload = RepeatedViewIdPB.fromObject({ items: [this.appId] });
|
|
||||||
const result = await FolderEventDeleteView(payload);
|
|
||||||
if (!result.ok) {
|
|
||||||
throw new Error(result.val.msg);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
deleteView = (viewId: string) => {
|
|
||||||
const payload = RepeatedViewIdPB.fromObject({ items: [viewId] });
|
|
||||||
return FolderEventDeleteView(payload);
|
|
||||||
};
|
|
||||||
|
|
||||||
moveView = (params: { view_id: string; fromIndex: number; toIndex: number }) => {
|
|
||||||
const payload = MoveViewPayloadPB.fromObject({
|
|
||||||
view_id: params.view_id,
|
|
||||||
from: params.fromIndex,
|
|
||||||
to: params.toIndex,
|
|
||||||
});
|
|
||||||
|
|
||||||
return FolderEventMoveView(payload);
|
|
||||||
};
|
|
||||||
}
|
|
@ -1,34 +0,0 @@
|
|||||||
import { FolderNotification } from '@/services/backend';
|
|
||||||
import { ChangeNotifier } from '$app/utils/change_notifier';
|
|
||||||
import { FolderNotificationObserver } from '../notifications/observer';
|
|
||||||
|
|
||||||
export class AppObserver {
|
|
||||||
_viewsNotifier = new ChangeNotifier<void>();
|
|
||||||
_listener?: FolderNotificationObserver;
|
|
||||||
|
|
||||||
constructor(public readonly appId: string) {}
|
|
||||||
|
|
||||||
subscribe = async (callbacks: { onViewsChanged: () => void }) => {
|
|
||||||
this._viewsNotifier?.observer?.subscribe(callbacks.onViewsChanged);
|
|
||||||
this._listener = new FolderNotificationObserver({
|
|
||||||
viewId: this.appId,
|
|
||||||
parserHandler: (notification, result) => {
|
|
||||||
switch (notification) {
|
|
||||||
case FolderNotification.DidUpdateChildViews:
|
|
||||||
if (result.ok) {
|
|
||||||
this._viewsNotifier?.notify();
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
await this._listener.start();
|
|
||||||
};
|
|
||||||
|
|
||||||
unsubscribe = async () => {
|
|
||||||
this._viewsNotifier.unsubscribe();
|
|
||||||
await this._listener?.stop();
|
|
||||||
};
|
|
||||||
}
|
|
@ -1,13 +1,25 @@
|
|||||||
import { UpdateViewPayloadPB, RepeatedViewIdPB, ViewPB } from '@/services/backend';
|
import { UpdateViewPayloadPB, RepeatedViewIdPB, ViewPB, FlowyError, ViewIdPB } from '@/services/backend';
|
||||||
import {
|
import {
|
||||||
FolderEventDeleteView,
|
FolderEventDeleteView,
|
||||||
FolderEventDuplicateView,
|
FolderEventDuplicateView,
|
||||||
|
FolderEventReadView,
|
||||||
FolderEventUpdateView,
|
FolderEventUpdateView,
|
||||||
} from '@/services/backend/events/flowy-folder2';
|
} from '@/services/backend/events/flowy-folder2';
|
||||||
|
import { Ok, Result } from 'ts-results';
|
||||||
|
|
||||||
export class ViewBackendService {
|
export class ViewBackendService {
|
||||||
constructor(public readonly viewId: string) {}
|
constructor(public readonly viewId: string) {}
|
||||||
|
|
||||||
|
getChildViews = async (): Promise<Result<ViewPB[], FlowyError>> => {
|
||||||
|
const payload = ViewIdPB.fromObject({ value: this.viewId });
|
||||||
|
const result = await FolderEventReadView(payload);
|
||||||
|
if (result.ok) {
|
||||||
|
return Ok(result.val.child_views);
|
||||||
|
} else {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
update = (params: { name?: string; desc?: string }) => {
|
update = (params: { name?: string; desc?: string }) => {
|
||||||
const payload = UpdateViewPayloadPB.fromObject({ view_id: this.viewId });
|
const payload = UpdateViewPayloadPB.fromObject({ view_id: this.viewId });
|
||||||
|
|
||||||
@ -26,7 +38,12 @@ export class ViewBackendService {
|
|||||||
return FolderEventDeleteView(payload);
|
return FolderEventDeleteView(payload);
|
||||||
};
|
};
|
||||||
|
|
||||||
duplicate = (view: ViewPB) => {
|
duplicate = async () => {
|
||||||
return FolderEventDuplicateView(view);
|
const view = await FolderEventReadView(ViewIdPB.fromObject({ value: this.viewId }));
|
||||||
|
if (view.ok) {
|
||||||
|
return FolderEventDuplicateView(view.val);
|
||||||
|
} else {
|
||||||
|
return view;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { Ok, Result } from "ts-results";
|
import { Ok, Result } from 'ts-results';
|
||||||
import { DeletedViewPB, FolderNotification, ViewPB, FlowyError } from "@/services/backend";
|
import { DeletedViewPB, FolderNotification, ViewPB, FlowyError } from '@/services/backend';
|
||||||
import { ChangeNotifier } from "$app/utils/change_notifier";
|
import { ChangeNotifier } from '$app/utils/change_notifier';
|
||||||
import { FolderNotificationObserver } from "../notifications/observer";
|
import { FolderNotificationObserver } from '../notifications/observer';
|
||||||
|
|
||||||
type DeleteViewNotifyValue = Result<ViewPB, FlowyError>;
|
type DeleteViewNotifyValue = Result<ViewPB, FlowyError>;
|
||||||
type UpdateViewNotifyValue = Result<ViewPB, FlowyError>;
|
type UpdateViewNotifyValue = Result<ViewPB, FlowyError>;
|
||||||
@ -12,17 +12,18 @@ export class ViewObserver {
|
|||||||
private _deleteViewNotifier = new ChangeNotifier<DeleteViewNotifyValue>();
|
private _deleteViewNotifier = new ChangeNotifier<DeleteViewNotifyValue>();
|
||||||
private _updateViewNotifier = new ChangeNotifier<UpdateViewNotifyValue>();
|
private _updateViewNotifier = new ChangeNotifier<UpdateViewNotifyValue>();
|
||||||
private _restoreViewNotifier = new ChangeNotifier<RestoreViewNotifyValue>();
|
private _restoreViewNotifier = new ChangeNotifier<RestoreViewNotifyValue>();
|
||||||
private _moveToTashNotifier = new ChangeNotifier<MoveToTrashViewNotifyValue>();
|
private _moveToTrashNotifier = new ChangeNotifier<MoveToTrashViewNotifyValue>();
|
||||||
|
private _childViewsNotifier = new ChangeNotifier<void>();
|
||||||
private _listener?: FolderNotificationObserver;
|
private _listener?: FolderNotificationObserver;
|
||||||
|
|
||||||
constructor(public readonly viewId: string) {
|
constructor(public readonly viewId: string) {}
|
||||||
}
|
|
||||||
|
|
||||||
subscribe = async (callbacks: {
|
subscribe = async (callbacks: {
|
||||||
onViewUpdate?: (value: UpdateViewNotifyValue) => void;
|
onViewUpdate?: (value: UpdateViewNotifyValue) => void;
|
||||||
onViewDelete?: (value: DeleteViewNotifyValue) => void;
|
onViewDelete?: (value: DeleteViewNotifyValue) => void;
|
||||||
onViewRestored?: (value: RestoreViewNotifyValue) => void;
|
onViewRestored?: (value: RestoreViewNotifyValue) => void;
|
||||||
onViewMoveToTrash?: (value: MoveToTrashViewNotifyValue) => void;
|
onViewMoveToTrash?: (value: MoveToTrashViewNotifyValue) => void;
|
||||||
|
onChildViewsChanged?: () => void;
|
||||||
}) => {
|
}) => {
|
||||||
if (callbacks.onViewDelete !== undefined) {
|
if (callbacks.onViewDelete !== undefined) {
|
||||||
this._deleteViewNotifier.observer?.subscribe(callbacks.onViewDelete);
|
this._deleteViewNotifier.observer?.subscribe(callbacks.onViewDelete);
|
||||||
@ -37,7 +38,11 @@ export class ViewObserver {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (callbacks.onViewMoveToTrash !== undefined) {
|
if (callbacks.onViewMoveToTrash !== undefined) {
|
||||||
this._moveToTashNotifier.observer?.subscribe(callbacks.onViewMoveToTrash);
|
this._moveToTrashNotifier.observer?.subscribe(callbacks.onViewMoveToTrash);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (callbacks.onChildViewsChanged !== undefined) {
|
||||||
|
this._childViewsNotifier.observer?.subscribe(callbacks.onChildViewsChanged);
|
||||||
}
|
}
|
||||||
|
|
||||||
this._listener = new FolderNotificationObserver({
|
this._listener = new FolderNotificationObserver({
|
||||||
@ -67,15 +72,20 @@ export class ViewObserver {
|
|||||||
break;
|
break;
|
||||||
case FolderNotification.DidMoveViewToTrash:
|
case FolderNotification.DidMoveViewToTrash:
|
||||||
if (result.ok) {
|
if (result.ok) {
|
||||||
this._moveToTashNotifier.notify(Ok(DeletedViewPB.deserializeBinary(result.val)));
|
this._moveToTrashNotifier.notify(Ok(DeletedViewPB.deserializeBinary(result.val)));
|
||||||
} else {
|
} else {
|
||||||
this._moveToTashNotifier.notify(result);
|
this._moveToTrashNotifier.notify(result);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case FolderNotification.DidUpdateChildViews:
|
||||||
|
if (result.ok) {
|
||||||
|
this._childViewsNotifier?.notify();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
await this._listener.start();
|
await this._listener.start();
|
||||||
};
|
};
|
||||||
@ -84,7 +94,8 @@ export class ViewObserver {
|
|||||||
this._deleteViewNotifier.unsubscribe();
|
this._deleteViewNotifier.unsubscribe();
|
||||||
this._updateViewNotifier.unsubscribe();
|
this._updateViewNotifier.unsubscribe();
|
||||||
this._restoreViewNotifier.unsubscribe();
|
this._restoreViewNotifier.unsubscribe();
|
||||||
this._moveToTashNotifier.unsubscribe();
|
this._moveToTrashNotifier.unsubscribe();
|
||||||
|
this._childViewsNotifier.unsubscribe();
|
||||||
await this._listener?.stop();
|
await this._listener?.stop();
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
import { Err, Ok } from 'ts-results';
|
import { Err, Ok, Result } from 'ts-results';
|
||||||
import {
|
import {
|
||||||
FolderEventCreateView,
|
FolderEventCreateView,
|
||||||
FolderEventMoveView,
|
FolderEventMoveView,
|
||||||
FolderEventReadWorkspaceViews,
|
FolderEventReadWorkspaceViews,
|
||||||
FolderEventReadAllWorkspaces,
|
FolderEventReadAllWorkspaces,
|
||||||
|
ViewPB,
|
||||||
} from '@/services/backend/events/flowy-folder2';
|
} from '@/services/backend/events/flowy-folder2';
|
||||||
import { CreateViewPayloadPB, FlowyError, MoveViewPayloadPB, ViewLayoutPB, WorkspaceIdPB } from '@/services/backend';
|
import { CreateViewPayloadPB, FlowyError, MoveViewPayloadPB, ViewLayoutPB, WorkspaceIdPB } from '@/services/backend';
|
||||||
import assert from 'assert';
|
import assert from 'assert';
|
||||||
@ -11,20 +12,25 @@ import assert from 'assert';
|
|||||||
export class WorkspaceBackendService {
|
export class WorkspaceBackendService {
|
||||||
constructor(public readonly workspaceId: string) {}
|
constructor(public readonly workspaceId: string) {}
|
||||||
|
|
||||||
createApp = async (params: { name: string; desc?: string }) => {
|
createView = async (params: {
|
||||||
|
name: string;
|
||||||
|
desc?: string;
|
||||||
|
layoutType: ViewLayoutPB;
|
||||||
|
parentViewId?: string;
|
||||||
|
/// The initial data should be the JSON of the document
|
||||||
|
/// For example: {"document":{"type":"editor","children":[]}}
|
||||||
|
initialData?: string;
|
||||||
|
}) => {
|
||||||
|
const encoder = new TextEncoder();
|
||||||
const payload = CreateViewPayloadPB.fromObject({
|
const payload = CreateViewPayloadPB.fromObject({
|
||||||
parent_view_id: this.workspaceId,
|
parent_view_id: params.parentViewId ?? this.workspaceId,
|
||||||
name: params.name,
|
name: params.name,
|
||||||
desc: params.desc || '',
|
desc: params.desc || '',
|
||||||
layout: ViewLayoutPB.Document,
|
layout: params.layoutType,
|
||||||
|
initial_data: encoder.encode(params.initialData || ''),
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = await FolderEventCreateView(payload);
|
return FolderEventCreateView(payload);
|
||||||
if (result.ok) {
|
|
||||||
return result.val;
|
|
||||||
} else {
|
|
||||||
throw new Error(result.val.msg);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
getWorkspace = () => {
|
getWorkspace = () => {
|
||||||
@ -44,14 +50,19 @@ export class WorkspaceBackendService {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
getApps = () => {
|
getAllViews: () => Promise<Result<ViewPB[], FlowyError>> = async () => {
|
||||||
const payload = WorkspaceIdPB.fromObject({ value: this.workspaceId });
|
const payload = WorkspaceIdPB.fromObject({ value: this.workspaceId });
|
||||||
return FolderEventReadWorkspaceViews(payload).then((result) => result.map((val) => val.items));
|
const result = await FolderEventReadWorkspaceViews(payload);
|
||||||
|
if (result.ok) {
|
||||||
|
return Ok(result.val.items);
|
||||||
|
} else {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
moveApp = (params: { appId: string; fromIndex: number; toIndex: number }) => {
|
moveView = (params: { viewId: string; fromIndex: number; toIndex: number }) => {
|
||||||
const payload = MoveViewPayloadPB.fromObject({
|
const payload = MoveViewPayloadPB.fromObject({
|
||||||
view_id: params.appId,
|
view_id: params.viewId,
|
||||||
from: params.fromIndex,
|
from: params.fromIndex,
|
||||||
to: params.toIndex,
|
to: params.toIndex,
|
||||||
});
|
});
|
||||||
|
@ -1,26 +0,0 @@
|
|||||||
import { FlowyError, FolderNotification } from '@/services/backend';
|
|
||||||
import { NotificationParser, OnNotificationError } from '@/services/backend/notifications';
|
|
||||||
import { Result } from 'ts-results';
|
|
||||||
|
|
||||||
declare type FolderNotificationCallback = (ty: FolderNotification, payload: Result<Uint8Array, FlowyError>) => void;
|
|
||||||
|
|
||||||
export class FolderNotificationParser extends NotificationParser<FolderNotification> {
|
|
||||||
constructor(params: { id?: string; callback: FolderNotificationCallback; onError?: OnNotificationError }) {
|
|
||||||
super(
|
|
||||||
params.callback,
|
|
||||||
(ty) => {
|
|
||||||
const notification = FolderNotification[ty];
|
|
||||||
if (isFolderNotification(notification)) {
|
|
||||||
return FolderNotification[notification];
|
|
||||||
} else {
|
|
||||||
return FolderNotification.Unknown;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
params.id
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const isFolderNotification = (notification: string): notification is keyof typeof FolderNotification => {
|
|
||||||
return Object.values(FolderNotification).indexOf(notification) !== -1;
|
|
||||||
};
|
|
@ -5,7 +5,8 @@ export interface IPage {
|
|||||||
id: string;
|
id: string;
|
||||||
title: string;
|
title: string;
|
||||||
pageType: ViewLayoutPB;
|
pageType: ViewLayoutPB;
|
||||||
folderId: string;
|
parentPageId: string;
|
||||||
|
showPagesInside: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialState: IPage[] = [];
|
const initialState: IPage[] = [];
|
||||||
@ -14,12 +15,19 @@ export const pagesSlice = createSlice({
|
|||||||
name: 'pages',
|
name: 'pages',
|
||||||
initialState: initialState,
|
initialState: initialState,
|
||||||
reducers: {
|
reducers: {
|
||||||
didReceivePages(state, action: PayloadAction<{ pages: IPage[]; folderId: string }>) {
|
addInsidePages(state, action: PayloadAction<{ insidePages: IPage[]; currentPageId: string }>) {
|
||||||
return state.filter((page) => page.folderId !== action.payload.folderId).concat(action.payload.pages);
|
return state
|
||||||
|
.filter((page) => page.parentPageId !== action.payload.currentPageId)
|
||||||
|
.concat(action.payload.insidePages);
|
||||||
},
|
},
|
||||||
addPage(state, action: PayloadAction<IPage>) {
|
addPage(state, action: PayloadAction<IPage>) {
|
||||||
state.push(action.payload);
|
state.push(action.payload);
|
||||||
},
|
},
|
||||||
|
toggleShowPages(state, action: PayloadAction<{ id: string }>) {
|
||||||
|
return state.map<IPage>((page: IPage) =>
|
||||||
|
page.id === action.payload.id ? { ...page, showPagesInside: !page.showPagesInside } : page
|
||||||
|
);
|
||||||
|
},
|
||||||
renamePage(state, action: PayloadAction<{ id: string; newTitle: string }>) {
|
renamePage(state, action: PayloadAction<{ id: string; newTitle: string }>) {
|
||||||
return state.map<IPage>((page: IPage) =>
|
return state.map<IPage>((page: IPage) =>
|
||||||
page.id === action.payload.id ? { ...page, title: action.payload.newTitle } : page
|
page.id === action.payload.id ? { ...page, title: action.payload.newTitle } : page
|
||||||
|
@ -7,7 +7,6 @@ import {
|
|||||||
ListenerEffectAPI,
|
ListenerEffectAPI,
|
||||||
addListener,
|
addListener,
|
||||||
} from '@reduxjs/toolkit';
|
} from '@reduxjs/toolkit';
|
||||||
import { foldersSlice } from './reducers/folders/slice';
|
|
||||||
import { pagesSlice } from './reducers/pages/slice';
|
import { pagesSlice } from './reducers/pages/slice';
|
||||||
import { navigationWidthSlice } from './reducers/navigation-width/slice';
|
import { navigationWidthSlice } from './reducers/navigation-width/slice';
|
||||||
import { currentUserSlice } from './reducers/current-user/slice';
|
import { currentUserSlice } from './reducers/current-user/slice';
|
||||||
@ -25,7 +24,6 @@ const listenerMiddlewareInstance = createListenerMiddleware({
|
|||||||
|
|
||||||
const store = configureStore({
|
const store = configureStore({
|
||||||
reducer: {
|
reducer: {
|
||||||
[foldersSlice.name]: foldersSlice.reducer,
|
|
||||||
[pagesSlice.name]: pagesSlice.reducer,
|
[pagesSlice.name]: pagesSlice.reducer,
|
||||||
[activePageIdSlice.name]: activePageIdSlice.reducer,
|
[activePageIdSlice.name]: activePageIdSlice.reducer,
|
||||||
[navigationWidthSlice.name]: navigationWidthSlice.reducer,
|
[navigationWidthSlice.name]: navigationWidthSlice.reducer,
|
||||||
|
@ -0,0 +1,46 @@
|
|||||||
|
import { Log } from '$app/utils/log';
|
||||||
|
|
||||||
|
export class AsyncQueue<T> {
|
||||||
|
private queue: T[] = [];
|
||||||
|
private isProcessing = false;
|
||||||
|
private executeFunction: (item: T) => Promise<void>;
|
||||||
|
|
||||||
|
constructor(executeFunction: (item: T) => Promise<void>) {
|
||||||
|
this.executeFunction = executeFunction;
|
||||||
|
}
|
||||||
|
|
||||||
|
enqueue(item: T): void {
|
||||||
|
this.queue.push(item);
|
||||||
|
this.processQueue();
|
||||||
|
}
|
||||||
|
|
||||||
|
private processQueue(): void {
|
||||||
|
if (this.isProcessing || this.queue.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const item = this.queue.shift();
|
||||||
|
this.isProcessing = true;
|
||||||
|
|
||||||
|
const executeFn = async (item: T) => {
|
||||||
|
try {
|
||||||
|
await this.processItem(item);
|
||||||
|
} catch (error) {
|
||||||
|
Log.error('queue processing error:', error);
|
||||||
|
} finally {
|
||||||
|
this.isProcessing = false;
|
||||||
|
this.processQueue();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
executeFn(item!);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async processItem(item: T): Promise<void> {
|
||||||
|
try {
|
||||||
|
await this.executeFunction(item);
|
||||||
|
} catch (error) {
|
||||||
|
Log.error('queue processing error:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -25,6 +25,8 @@ export abstract class AFNotificationObserver<T> {
|
|||||||
|
|
||||||
async stop() {
|
async stop() {
|
||||||
if (this._listener !== undefined) {
|
if (this._listener !== undefined) {
|
||||||
|
// call the unlisten function before setting it to undefined
|
||||||
|
this._listener();
|
||||||
this._listener = undefined;
|
this._listener = undefined;
|
||||||
}
|
}
|
||||||
this.parser = null;
|
this.parser = null;
|
||||||
|
Loading…
Reference in New Issue
Block a user