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:
Askarbek Zadauly 2023-06-23 08:17:50 +06:00 committed by GitHub
parent 9834eccc7b
commit eee32110f4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
38 changed files with 775 additions and 960 deletions

View File

@ -201,7 +201,7 @@ export const EditRow = ({
}}
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'}>
<CloseSvg></CloseSvg>
</button>
@ -209,7 +209,7 @@ export const EditRow = ({
<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={'pl-12 pb-4'}>
<div className={'pb-4 pl-12'}>
<button className={'flex items-center gap-2 p-4'}>
<i className={'h-5 w-5'}>
<ImageSvg></ImageSvg>
@ -229,7 +229,9 @@ export const EditRow = ({
}`}
>
{cells
.filter((cell) => databaseStore.fields[cell.cellIdentifier.fieldId].visible)
.filter((cell) => {
return databaseStore.fields[cell.cellIdentifier.fieldId]?.visible;
})
.map((cell, cellIndex) => (
<EditCellWrapper
index={cellIndex}

View File

@ -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 { databaseActions, DatabaseFieldMap, IDatabaseColumn } from '$app/stores/reducers/database/slice';
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 { DatabaseGroupController } from '$app/stores/effects/database/group/group_controller';
import { OnDragEndResponder } from 'react-beautiful-dnd';
import { AsyncQueue } from '$app/utils/async_queue';
export const useDatabase = (viewId: string, type?: ViewLayoutPB) => {
const dispatch = useAppDispatch();
@ -24,25 +25,30 @@ export const useDatabase = (viewId: string, type?: ViewLayoutPB) => {
return () => void c.dispose();
}, [viewId]);
const loadFields = async (fieldInfos: readonly FieldInfo[]) => {
const fields: DatabaseFieldMap = {};
const columns: IDatabaseColumn[] = [];
const loadFields = useCallback(
async (fieldInfos: readonly FieldInfo[]) => {
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 fieldPB = fieldInfo.field;
columns.push({
fieldId: fieldPB.id,
sort: 'none',
visible: fieldPB.visibility,
});
const field = await loadField(viewId, fieldInfo, dispatch);
fields[field.fieldId] = field;
}
dispatch(databaseActions.updateFields({ fields }));
dispatch(databaseActions.updateColumns({ columns }));
},
[viewId, dispatch]
);
const field = await loadField(viewId, fieldInfo, dispatch);
fields[field.fieldId] = field;
}
dispatch(databaseActions.updateFields({ fields }));
dispatch(databaseActions.updateColumns({ columns }));
};
const queue = useMemo(() => {
return new AsyncQueue<readonly FieldInfo[]>(loadFields);
}, [loadFields]);
useEffect(() => {
void (async () => {
@ -53,7 +59,7 @@ export const useDatabase = (viewId: string, type?: ViewLayoutPB) => {
setRows([...rowInfos]);
},
onFieldsChanged: (fieldInfos) => {
void loadFields(fieldInfos);
queue.enqueue(fieldInfos);
},
});
@ -76,7 +82,7 @@ export const useDatabase = (viewId: string, type?: ViewLayoutPB) => {
return () => {
void controller?.dispose();
};
}, [controller]);
}, [controller, queue]);
const onNewRowClick = async (index: number) => {
if (!groups) return;
@ -95,7 +101,7 @@ export const useDatabase = (viewId: string, type?: ViewLayoutPB) => {
if (source.droppableId === destination?.droppableId) {
// move inside the block (group)
await controller.exchangeRow(
await controller.exchangeGroupRow(
group.rows[source.index].id,
destination.droppableId,
group.rows[destination.index].id
@ -103,7 +109,7 @@ export const useDatabase = (viewId: string, type?: ViewLayoutPB) => {
} else {
// move to different block (group)
if (!destination?.droppableId) return;
await controller.moveRow(group.rows[source.index].id, destination.droppableId);
await controller.moveGroupRow(group.rows[source.index].id, destination.droppableId);
}
};

View File

@ -7,7 +7,6 @@ import { Draggable } from 'react-beautiful-dnd';
import { MouseEventHandler, useState } from 'react';
import { PopupWindow } from '$app/components/_shared/PopupWindow';
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 { useAppSelector } from '$app/stores/store';
@ -52,8 +51,7 @@ export const BoardCard = ({
const onDeleteRowClick = async () => {
setShowCardPopup(false);
const svc = new RowBackendService(viewId);
await svc.deleteRow(rowInfo.row.id);
await controller.deleteRow(rowInfo.row.id);
};
return (
@ -73,7 +71,7 @@ export const BoardCard = ({
<div className={'flex flex-col gap-3'}>
{cells
.filter(
(cell) => cell.fieldId !== groupByFieldId && databaseStore.fields[cell.cellIdentifier.fieldId].visible
(cell) => cell.fieldId !== groupByFieldId && databaseStore.fields[cell.cellIdentifier.fieldId]?.visible
)
.map((cell, cellIndex) => (
<BoardCell

View File

@ -1,6 +1,6 @@
import { ShowMenuSvg } from '../../_shared/svg/ShowMenuSvg';
import { useEffect, useState } from 'react';
import { useAppSelector } from '../../../stores/store';
import { useAppSelector } from '$app/stores/store';
import { useLocation } from 'react-router-dom';
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 currentLocation = useLocation();
const pagesStore = useAppSelector((state) => state.pages);
const foldersStore = useAppSelector((state) => state.folders);
useEffect(() => {
const { pathname } = currentLocation;
@ -20,10 +19,10 @@ export const Breadcrumbs = ({ menuHidden, onShowMenuClick }: { menuHidden: boole
useEffect(() => {
const page = pagesStore.find((p) => p.id === activePageId);
const folder = foldersStore.find((f) => f.id === page?.folderId);
setFolderName(folder?.title ?? '');
// const folder = foldersStore.find((f) => f.id === page?.parentPageId);
// setFolderName(folder?.title ?? '');
setPageName(page?.title ?? '');
}, [pagesStore, foldersStore, activePageId]);
}, [pagesStore, activePageId]);
return (
<div className={'flex items-center'}>

View File

@ -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,
};
};

View File

@ -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>
);
};

View File

@ -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,
};
};

View File

@ -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>
);
};

View File

@ -1,17 +1,10 @@
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';
export const useNavigationPanelHooks = function () {
const folders = useAppSelector((state) => state.folders);
const pages = useAppSelector((state) => state.pages);
const width = useAppSelector((state) => state.navigationWidth);
const [menuHidden, setMenuHidden] = useState(false);
const navigate = useNavigate();
const onHideMenuClick = () => {
setMenuHidden(true);
};
@ -20,28 +13,8 @@ export const useNavigationPanelHooks = function () {
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 {
width,
folders,
pages,
onPageClick,
menuHidden,
onHideMenuClick,
onShowMenuClick,

View File

@ -1,40 +1,27 @@
import { WorkspaceUser } from '../WorkspaceUser';
import { AppLogo } from '../AppLogo';
import { FolderItem } from './FolderItem';
import { TrashButton } from './TrashButton';
import { NewFolderButton } from './NewFolderButton';
import { NewViewButton } from './NewViewButton';
import { NavigationResizer } from './NavigationResizer';
import { IFolder } from '$app_reducers/folders/slice';
import { IPage } from '$app_reducers/pages/slice';
import { useLocation, useNavigate } from 'react-router-dom';
import React, { useEffect, useRef, useState } from 'react';
import { useAppSelector } from '$app/stores/store';
import {
ANIMATION_DURATION,
FOLDER_MARGIN,
INITIAL_FOLDER_HEIGHT,
NAV_PANEL_MINIMUM_WIDTH,
PAGE_ITEM_HEIGHT,
} from '../../_shared/constants';
import { NavItem } from '$app/components/layout/NavigationPanel/NavItem';
import { ANIMATION_DURATION, NAV_PANEL_MINIMUM_WIDTH, PAGE_ITEM_HEIGHT } from '../../_shared/constants';
export const NavigationPanel = ({
onHideMenuClick,
menuHidden,
width,
folders,
pages,
onPageClick,
}: {
onHideMenuClick: () => void;
menuHidden: boolean;
width: number;
folders: IFolder[];
pages: IPage[];
onPageClick: (page: IPage) => void;
}) => {
const el = useRef<HTMLDivElement>(null);
const foldersStore = useAppSelector((state) => state.folders);
const pagesStore = useAppSelector((state) => state.pages);
const pages = useAppSelector((state) => state.pages);
const workspace = useAppSelector((state) => state.workspace);
const [activePageId, setActivePageId] = useState<string>('');
const currentLocation = useLocation();
const [maxHeight, setMaxHeight] = useState(0);
@ -47,44 +34,8 @@ export const NavigationPanel = ({
}, [currentLocation]);
useEffect(() => {
setTimeout(() => {
if (!el.current) return;
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]);
setMaxHeight(pages.length * PAGE_ITEM_HEIGHT);
}, [pages]);
const scrollDown = () => {
setTimeout(() => {
@ -113,7 +64,7 @@ export const NavigationPanel = ({
}}
ref={el}
>
<WorkspaceApps folders={folders} pages={pages} onPageClick={onPageClick} />
<WorkspaceApps pages={pages.filter((p) => p.parentPageId === workspace.id)} />
</div>
</div>
</div>
@ -130,8 +81,8 @@ export const NavigationPanel = ({
<TrashButton></TrashButton>
</div>
{/*New Folder Button*/}
<NewFolderButton scrollDown={scrollDown}></NewFolderButton>
{/*New Root View Button*/}
<NewViewButton scrollDown={scrollDown}></NewViewButton>
</div>
</div>
<NavigationResizer minWidth={NAV_PANEL_MINIMUM_WIDTH}></NavigationResizer>
@ -139,21 +90,10 @@ export const NavigationPanel = ({
);
};
type AppsContext = {
folders: IFolder[];
pages: IPage[];
onPageClick: (page: IPage) => void;
};
const WorkspaceApps: React.FC<AppsContext> = ({ folders, pages, onPageClick }) => (
const WorkspaceApps: React.FC<{ pages: IPage[] }> = ({ pages }) => (
<>
{folders.map((folder, index) => (
<FolderItem
key={index}
folder={folder}
pages={pages.filter((page) => page.folderId === folder.id)}
onPageClick={onPageClick}
></FolderItem>
{pages.map((page, index) => (
<NavItem key={index} page={page}></NavItem>
))}
</>
);

View File

@ -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,
};
};

View File

@ -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,
};
};

View File

@ -1,13 +1,13 @@
import AddSvg from '../../_shared/svg/AddSvg';
import { useNewFolder } from './NewFolderButton.hooks';
import { useNewRootView } from './NewViewButton.hooks';
export const NewFolderButton = ({ scrollDown }: { scrollDown: () => void }) => {
const { onNewFolder } = useNewFolder();
export const NewViewButton = ({ scrollDown }: { scrollDown: () => void }) => {
const { onNewRootView } = useNewRootView();
return (
<button
onClick={() => {
void onNewFolder();
void onNewRootView();
scrollDown();
}}
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>
</div>
</div>
<span>New Folder</span>
<span>New View</span>
</button>
);
};

View File

@ -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,
};
};

View File

@ -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>
);
};

View File

@ -1,31 +1,17 @@
import React, { ReactNode, useEffect } from 'react';
import React, { ReactNode } from 'react';
import { NavigationPanel } from './NavigationPanel/NavigationPanel';
import { MainPanel } from './MainPanel';
import { useNavigationPanelHooks } from './NavigationPanel/NavigationPanel.hooks';
import { useWorkspace } from './Workspace.hooks';
import { useAppSelector } from '$app/stores/store';
export const Screen = ({ children }: { children: ReactNode }) => {
const currentUser = useAppSelector((state) => state.currentUser);
const { loadWorkspaceItems } = useWorkspace();
useEffect(() => {
void (async () => {
await loadWorkspaceItems();
})();
}, [currentUser.isAuthenticated]);
useWorkspace();
const { width, folders, pages, onPageClick, onHideMenuClick, onShowMenuClick, menuHidden } = useNavigationPanelHooks();
const { width, onHideMenuClick, onShowMenuClick, menuHidden } = useNavigationPanelHooks();
return (
<div className='flex h-screen w-screen bg-white text-black'>
<NavigationPanel
onHideMenuClick={onHideMenuClick}
width={width}
folders={folders}
pages={pages}
onPageClick={onPageClick}
menuHidden={menuHidden}
></NavigationPanel>
<NavigationPanel onHideMenuClick={onHideMenuClick} width={width} menuHidden={menuHidden}></NavigationPanel>
<MainPanel left={width} menuHidden={menuHidden} onShowMenuClick={onShowMenuClick}>
{children}

View File

@ -1,52 +1,69 @@
import { foldersActions } from '$app_reducers/folders/slice';
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 { UserBackendService } from '$app/stores/effects/user/user_bd_svc';
import { AppBackendService } from '$app/stores/effects/folder/app/app_bd_svc';
import { Log } from '$app/utils/log';
import { useEffect, useState } from 'react';
import { WorkspaceBackendService } from '$app/stores/effects/folder/workspace/workspace_bd_svc';
export const useWorkspace = () => {
const currentUser = useAppSelector((state) => state.currentUser);
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 () => {
try {
const workspaceSettingPB = await userBackendService.getCurrentWorkspace();
const workspace = workspaceSettingPB.workspace;
appDispatch(workspaceActions.updateWorkspace({ id: workspace.id, name: workspace.name }));
appDispatch(foldersActions.clearFolders());
appDispatch(pagesActions.clearPages());
const apps = workspace.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());
useEffect(() => {
if (currentUser.id) {
setUserService(new UserBackendService(currentUser.id));
}
};
}, [currentUser]);
return {
loadWorkspaceItems,
};
useEffect(() => {
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 {};
};

View File

@ -1,12 +1,12 @@
import {
FieldType,
FlowyError,
SingleSelectTypeOptionPB,
ViewLayoutPB,
ViewPB,
WorkspaceSettingPB,
} from '../../../services/backend';
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 { RowInfo } from '../../stores/effects/database/row/row_cache';
import { RowController } from '../../stores/effects/database/row/row_controller';
@ -19,7 +19,7 @@ import {
TextCellController,
URLCellController,
} 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 { DatabaseBackendService } from '../../stores/effects/database/database_bd_svc';
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 { SelectOptionBackendService } from '../../stores/effects/database/cell/select_option_bd_svc';
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
// Do not use it production code. Just for testing
export async function createTestDatabaseView(layout: ViewLayoutPB): Promise<ViewPB> {
const workspaceSetting: WorkspaceSettingPB = await FolderEventGetCurrentWorkspace().then((result) => result.unwrap());
const appService = new AppBackendService(workspaceSetting.workspace.id);
return await appService.createView({ name: 'New Grid', layoutType: layout });
const wsSvc = new WorkspaceBackendService(workspaceSetting.workspace.id);
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> {

View File

@ -1,10 +1,15 @@
import { ViewLayoutPB, WorkspaceSettingPB } from '@/services/backend';
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() {
const workspaceSetting: WorkspaceSettingPB = await FolderEventGetCurrentWorkspace().then((result) => result.unwrap());
const app = workspaceSetting.workspace.views[0];
const appService = new AppBackendService(app.id);
return await appService.createView({ name: 'New Document', layoutType: ViewLayoutPB.Document });
const appService = new WorkspaceBackendService(workspaceSetting.workspace.id);
const result = await appService.createView({ name: 'New Document', layoutType: ViewLayoutPB.Document });
if (result.ok) {
return result.val;
}
else {
throw Error(result.val.msg);
}
}

View File

@ -29,6 +29,7 @@ import {
TestMoveKanbanBoardRow,
} from './TestGroup';
import { TestCreateDocument } from './TestDocument';
import { TestCreateViews } from '$app/components/tests/TestFolder';
export const TestAPI = () => {
return (
@ -62,6 +63,8 @@ export const TestAPI = () => {
<TestMoveKanbanBoardColumn></TestMoveKanbanBoardColumn>
<TestCreateKanbanBoardColumn></TestCreateKanbanBoardColumn>
<TestCreateDocument></TestCreateDocument>
{/*Folders*/}
<TestCreateViews></TestCreateViews>
</ul>
</React.Fragment>
);

View File

@ -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>
);
};

View File

@ -30,7 +30,6 @@ import {
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 { None, Some } from 'ts-results';
import { RowBackendService } from '$app/stores/effects/database/row/row_bd_svc';
import {
makeDateTypeOptionContext,
makeNumberTypeOptionContext,
@ -250,7 +249,7 @@ async function testCreateRow() {
await databaseController.open().then((result) => result.unwrap());
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 assertNumberOfRows(view.id, 4);
await databaseController.dispose();
@ -262,8 +261,7 @@ async function testDeleteRow() {
await databaseController.open().then((result) => result.unwrap());
const rows = databaseController.databaseViewCache.rowInfos;
const svc = new RowBackendService(view.id);
await svc.deleteRow(rows[0].row.id);
await databaseController.deleteRow(rows[0].row.id);
await assertNumberOfRows(view.id, 2);
// Wait the databaseViewCache get the change notification and

View File

@ -101,7 +101,7 @@ async function moveKanbanBoardRow() {
});
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);
await assertNumberOfRowsInGroup(view.id, firstGroup.groupId, 2);

View File

@ -3,9 +3,9 @@ import { CellCache, CellCacheKey } from './cell_cache';
import { CellDataLoader } from './data_parser';
import { CellDataPersistence } from './data_persistence';
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 { Log } from '../../../../utils/log';
import { Log } from '$app/utils/log';
import { Err, None, Ok, Option, Some } from 'ts-results';
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.
void this.fieldNotifier.subscribe({
onFieldChanged: () => {
this.subscribeCallbacks?.onFieldChanged?.();
onFieldChanged: async () => {
/// reloadOnFieldChanged should be true if you need to load the data when the corresponding field is changed
/// For example:
/// ¥12 -> $12
if (this.cellDataLoader.reloadOnFieldChanged) {
void this._loadCellData();
await this._loadCellData();
}
this.subscribeCallbacks?.onFieldChanged?.();
},
});
}
@ -97,24 +97,24 @@ export class CellController<T, D> {
return cellData;
};
private _loadCellData = () => {
return this.cellDataLoader.loadData().then((result) => {
if (result.ok) {
const cellData = result.val;
if (cellData.some) {
this.cellCache.insert(this.cacheKey, cellData.val);
this.cellDataNotifier.cellData = cellData;
}
} else {
this.cellCache.remove(this.cacheKey);
this.cellDataNotifier.cellData = None;
private _loadCellData = async () => {
const result = await this.cellDataLoader.loadData();
if (result.ok) {
const cellData = result.val;
if (cellData.some) {
this.cellCache.insert(this.cacheKey, cellData.val);
this.cellDataNotifier.cellData = cellData;
}
});
} else {
this.cellCache.remove(this.cacheKey);
this.cellDataNotifier.cellData = None;
}
};
dispose = async () => {
await this.cellObserver.unsubscribe();
await this.fieldNotifier.unsubscribe();
this.cellDataNotifier.unsubscribe();
};
}

View File

@ -1,5 +1,7 @@
import {
DatabaseEventCreateRow,
DatabaseEventDeleteRow,
DatabaseEventDuplicateRow,
DatabaseEventGetDatabase,
DatabaseEventGetDatabaseSetting,
DatabaseEventGetFields,
@ -14,6 +16,7 @@ import {
MoveGroupPayloadPB,
MoveGroupRowPayloadPB,
MoveRowPayloadPB,
RowIdPB,
} from '@/services/backend/events/flowy-database2';
import {
GetFieldPayloadPB,
@ -64,6 +67,16 @@ export class DatabaseBackendService {
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
/// [toRowId] is used to locate the moving row location.
moveGroupRow = (fromRowId: string, toGroupId: string, toRowId?: string) => {

View File

@ -89,11 +89,19 @@ export class DatabaseController {
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);
};
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.loadGroup();
};

View File

@ -1,8 +1,8 @@
import { Log } from "$app/utils/log";
import { DatabaseBackendService } from "../database_bd_svc";
import { DatabaseFieldChangesetObserver } from "./field_observer";
import { FieldIdPB, FieldPB, IndexFieldPB } from "@/services/backend";
import { ChangeNotifier } from "$app/utils/change_notifier";
import { Log } from '$app/utils/log';
import { DatabaseBackendService } from '../database_bd_svc';
import { DatabaseFieldChangesetObserver } from './field_observer';
import { FieldIdPB, FieldPB, IndexFieldPB } from '@/services/backend';
import { ChangeNotifier } from '$app/utils/change_notifier';
export class FieldController {
private backendService: DatabaseBackendService;
@ -53,7 +53,7 @@ export class FieldController {
} else {
Log.error(result.val);
}
}
},
});
};
@ -122,6 +122,5 @@ class NumOfFieldsNotifier extends ChangeNotifier<FieldInfo[]> {
}
export class FieldInfo {
constructor(public readonly field: FieldPB) {
}
constructor(public readonly field: FieldPB) {}
}

View File

@ -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);
};
}

View File

@ -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);
};
}

View File

@ -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();
};
}

View File

@ -1,13 +1,25 @@
import { UpdateViewPayloadPB, RepeatedViewIdPB, ViewPB } from '@/services/backend';
import { UpdateViewPayloadPB, RepeatedViewIdPB, ViewPB, FlowyError, ViewIdPB } from '@/services/backend';
import {
FolderEventDeleteView,
FolderEventDuplicateView,
FolderEventReadView,
FolderEventUpdateView,
} from '@/services/backend/events/flowy-folder2';
import { Ok, Result } from 'ts-results';
export class ViewBackendService {
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 }) => {
const payload = UpdateViewPayloadPB.fromObject({ view_id: this.viewId });
@ -26,7 +38,12 @@ export class ViewBackendService {
return FolderEventDeleteView(payload);
};
duplicate = (view: ViewPB) => {
return FolderEventDuplicateView(view);
duplicate = async () => {
const view = await FolderEventReadView(ViewIdPB.fromObject({ value: this.viewId }));
if (view.ok) {
return FolderEventDuplicateView(view.val);
} else {
return view;
}
};
}

View File

@ -1,7 +1,7 @@
import { Ok, Result } from "ts-results";
import { DeletedViewPB, FolderNotification, ViewPB, FlowyError } from "@/services/backend";
import { ChangeNotifier } from "$app/utils/change_notifier";
import { FolderNotificationObserver } from "../notifications/observer";
import { Ok, Result } from 'ts-results';
import { DeletedViewPB, FolderNotification, ViewPB, FlowyError } from '@/services/backend';
import { ChangeNotifier } from '$app/utils/change_notifier';
import { FolderNotificationObserver } from '../notifications/observer';
type DeleteViewNotifyValue = Result<ViewPB, FlowyError>;
type UpdateViewNotifyValue = Result<ViewPB, FlowyError>;
@ -12,17 +12,18 @@ export class ViewObserver {
private _deleteViewNotifier = new ChangeNotifier<DeleteViewNotifyValue>();
private _updateViewNotifier = new ChangeNotifier<UpdateViewNotifyValue>();
private _restoreViewNotifier = new ChangeNotifier<RestoreViewNotifyValue>();
private _moveToTashNotifier = new ChangeNotifier<MoveToTrashViewNotifyValue>();
private _moveToTrashNotifier = new ChangeNotifier<MoveToTrashViewNotifyValue>();
private _childViewsNotifier = new ChangeNotifier<void>();
private _listener?: FolderNotificationObserver;
constructor(public readonly viewId: string) {
}
constructor(public readonly viewId: string) {}
subscribe = async (callbacks: {
onViewUpdate?: (value: UpdateViewNotifyValue) => void;
onViewDelete?: (value: DeleteViewNotifyValue) => void;
onViewRestored?: (value: RestoreViewNotifyValue) => void;
onViewMoveToTrash?: (value: MoveToTrashViewNotifyValue) => void;
onChildViewsChanged?: () => void;
}) => {
if (callbacks.onViewDelete !== undefined) {
this._deleteViewNotifier.observer?.subscribe(callbacks.onViewDelete);
@ -37,7 +38,11 @@ export class ViewObserver {
}
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({
@ -67,15 +72,20 @@ export class ViewObserver {
break;
case FolderNotification.DidMoveViewToTrash:
if (result.ok) {
this._moveToTashNotifier.notify(Ok(DeletedViewPB.deserializeBinary(result.val)));
this._moveToTrashNotifier.notify(Ok(DeletedViewPB.deserializeBinary(result.val)));
} else {
this._moveToTashNotifier.notify(result);
this._moveToTrashNotifier.notify(result);
}
break;
case FolderNotification.DidUpdateChildViews:
if (result.ok) {
this._childViewsNotifier?.notify();
}
break;
default:
break;
}
}
},
});
await this._listener.start();
};
@ -84,7 +94,8 @@ export class ViewObserver {
this._deleteViewNotifier.unsubscribe();
this._updateViewNotifier.unsubscribe();
this._restoreViewNotifier.unsubscribe();
this._moveToTashNotifier.unsubscribe();
this._moveToTrashNotifier.unsubscribe();
this._childViewsNotifier.unsubscribe();
await this._listener?.stop();
};
}

View File

@ -1,9 +1,10 @@
import { Err, Ok } from 'ts-results';
import { Err, Ok, Result } from 'ts-results';
import {
FolderEventCreateView,
FolderEventMoveView,
FolderEventReadWorkspaceViews,
FolderEventReadAllWorkspaces,
ViewPB,
} from '@/services/backend/events/flowy-folder2';
import { CreateViewPayloadPB, FlowyError, MoveViewPayloadPB, ViewLayoutPB, WorkspaceIdPB } from '@/services/backend';
import assert from 'assert';
@ -11,20 +12,25 @@ import assert from 'assert';
export class WorkspaceBackendService {
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({
parent_view_id: this.workspaceId,
parent_view_id: params.parentViewId ?? this.workspaceId,
name: params.name,
desc: params.desc || '',
layout: ViewLayoutPB.Document,
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);
}
return FolderEventCreateView(payload);
};
getWorkspace = () => {
@ -44,14 +50,19 @@ export class WorkspaceBackendService {
});
};
getApps = () => {
getAllViews: () => Promise<Result<ViewPB[], FlowyError>> = async () => {
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({
view_id: params.appId,
view_id: params.viewId,
from: params.fromIndex,
to: params.toIndex,
});

View File

@ -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;
};

View File

@ -5,7 +5,8 @@ export interface IPage {
id: string;
title: string;
pageType: ViewLayoutPB;
folderId: string;
parentPageId: string;
showPagesInside: boolean;
}
const initialState: IPage[] = [];
@ -14,12 +15,19 @@ export const pagesSlice = createSlice({
name: 'pages',
initialState: initialState,
reducers: {
didReceivePages(state, action: PayloadAction<{ pages: IPage[]; folderId: string }>) {
return state.filter((page) => page.folderId !== action.payload.folderId).concat(action.payload.pages);
addInsidePages(state, action: PayloadAction<{ insidePages: IPage[]; currentPageId: string }>) {
return state
.filter((page) => page.parentPageId !== action.payload.currentPageId)
.concat(action.payload.insidePages);
},
addPage(state, action: PayloadAction<IPage>) {
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 }>) {
return state.map<IPage>((page: IPage) =>
page.id === action.payload.id ? { ...page, title: action.payload.newTitle } : page

View File

@ -7,7 +7,6 @@ import {
ListenerEffectAPI,
addListener,
} from '@reduxjs/toolkit';
import { foldersSlice } from './reducers/folders/slice';
import { pagesSlice } from './reducers/pages/slice';
import { navigationWidthSlice } from './reducers/navigation-width/slice';
import { currentUserSlice } from './reducers/current-user/slice';
@ -25,7 +24,6 @@ const listenerMiddlewareInstance = createListenerMiddleware({
const store = configureStore({
reducer: {
[foldersSlice.name]: foldersSlice.reducer,
[pagesSlice.name]: pagesSlice.reducer,
[activePageIdSlice.name]: activePageIdSlice.reducer,
[navigationWidthSlice.name]: navigationWidthSlice.reducer,

View File

@ -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);
}
}
}

View File

@ -25,6 +25,8 @@ export abstract class AFNotificationObserver<T> {
async stop() {
if (this._listener !== undefined) {
// call the unlisten function before setting it to undefined
this._listener();
this._listener = undefined;
}
this.parser = null;