diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/EditRow.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/EditRow.tsx index 673aab5687..be470662e6 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/EditRow.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/EditRow.tsx @@ -201,7 +201,7 @@ export const EditRow = ({ }} className={`relative flex h-[90%] w-[70%] flex-col gap-8 rounded-xl bg-white `} > -
onCloseClick()} className={'absolute top-1 right-1'}> +
onCloseClick()} className={'absolute right-1 top-1'}> @@ -209,7 +209,7 @@ export const EditRow = ({
-
+
-
- - -
-
- - {pages.map((page, index) => ( - onPageClick(page)}> - ))} -
- {showFolderOptions && ( - startFolderRename()} - onDeleteClick={() => deleteFolder()} - onDuplicateClick={() => duplicateFolder()} - onClose={() => closePopup()} - top={popupY - 124 + 40} - > - )} - {showNewPageOptions && ( - onAddNewDocumentPage()} - onBoardClick={() => onAddNewBoardPage()} - onGridClick={() => onAddNewGridPage()} - onClose={() => closePopup()} - top={popupY - 124 + 40} - > - )} - {showRenamePopup && ( - changeFolderTitle(newTitle)} - onClose={closeRenamePopup} - top={popupY - 124 + 40} - > - )} -
- ); -}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/layout/NavigationPanel/NavItem.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/layout/NavigationPanel/NavItem.hooks.ts new file mode 100644 index 0000000000..29fddfdee4 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/layout/NavigationPanel/NavItem.hooks.ts @@ -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(''); + 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((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, + }; +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/layout/NavigationPanel/NavItem.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/layout/NavigationPanel/NavItem.tsx new file mode 100644 index 0000000000..de37ef8b64 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/layout/NavigationPanel/NavItem.tsx @@ -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(null); + + useEffect(() => { + if (el.current) { + const { top } = el.current.getBoundingClientRect(); + setPopupY(top); + } + }, [showPageOptions, showNewPageOptions, showRenamePopup]); + + return ( +
+
+
+
+ +
onPageClick(page)} + className={ + 'flex h-full min-w-0 flex-1 items-center overflow-hidden overflow-ellipsis whitespace-nowrap text-left' + } + > + {page.title} +
+
+
+ + +
+
+
+ {useMemo(() => pages.filter((insidePage) => insidePage.parentPageId === page.id), [pages, page]).map( + (insidePage, insideIndex) => ( + + ) + )} +
+
+ {showPageOptions && ( + startPageRename()} + onDeleteClick={() => deletePage()} + onDuplicateClick={() => duplicatePage()} + onClose={() => closePopup()} + top={popupY - 124 + 40} + > + )} + {showNewPageOptions && ( + onAddNewPage(ViewLayoutPB.Document)} + onBoardClick={() => onAddNewPage(ViewLayoutPB.Board)} + onGridClick={() => onAddNewPage(ViewLayoutPB.Grid)} + onClose={() => closePopup()} + top={popupY - 124 + 40} + > + )} + {showRenamePopup && ( + changePageTitle(newTitle)} + onClose={closeRenamePopup} + top={popupY - 124 + 40} + > + )} +
+ ); +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/layout/NavigationPanel/NavigationPanel.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/layout/NavigationPanel/NavigationPanel.hooks.ts index 60543ddea7..e5df126fff 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/layout/NavigationPanel/NavigationPanel.hooks.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/layout/NavigationPanel/NavigationPanel.hooks.ts @@ -1,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, diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/layout/NavigationPanel/NavigationPanel.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/layout/NavigationPanel/NavigationPanel.tsx index 2808cef8b7..bd3c7b9cdb 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/layout/NavigationPanel/NavigationPanel.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/layout/NavigationPanel/NavigationPanel.tsx @@ -1,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(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(''); 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} > - + p.parentPageId === workspace.id)} />
@@ -130,8 +81,8 @@ export const NavigationPanel = ({ - {/*New Folder Button*/} - + {/*New Root View Button*/} + @@ -139,21 +90,10 @@ export const NavigationPanel = ({ ); }; -type AppsContext = { - folders: IFolder[]; - pages: IPage[]; - onPageClick: (page: IPage) => void; -}; - -const WorkspaceApps: React.FC = ({ folders, pages, onPageClick }) => ( +const WorkspaceApps: React.FC<{ pages: IPage[] }> = ({ pages }) => ( <> - {folders.map((folder, index) => ( - page.folderId === folder.id)} - onPageClick={onPageClick} - > + {pages.map((page, index) => ( + ))} ); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/layout/NavigationPanel/NewFolderButton.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/layout/NavigationPanel/NewFolderButton.hooks.ts deleted file mode 100644 index 2c9fc11a79..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/components/layout/NavigationPanel/NewFolderButton.hooks.ts +++ /dev/null @@ -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, - }; -}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/layout/NavigationPanel/NewViewButton.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/layout/NavigationPanel/NewViewButton.hooks.ts new file mode 100644 index 0000000000..d640d765dd --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/layout/NavigationPanel/NewViewButton.hooks.ts @@ -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, + }; +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/layout/NavigationPanel/NewFolderButton.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/layout/NavigationPanel/NewViewButton.tsx similarity index 63% rename from frontend/appflowy_tauri/src/appflowy_app/components/layout/NavigationPanel/NewFolderButton.tsx rename to frontend/appflowy_tauri/src/appflowy_app/components/layout/NavigationPanel/NewViewButton.tsx index 7c1df82c26..7a51e17855 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/layout/NavigationPanel/NewFolderButton.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/layout/NavigationPanel/NewViewButton.tsx @@ -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 ( ); }; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/layout/NavigationPanel/PageItem.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/layout/NavigationPanel/PageItem.hooks.ts deleted file mode 100644 index 1b21ce46db..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/components/layout/NavigationPanel/PageItem.hooks.ts +++ /dev/null @@ -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(''); - 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, - }; -}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/layout/NavigationPanel/PageItem.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/layout/NavigationPanel/PageItem.tsx deleted file mode 100644 index e9f5e79d00..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/components/layout/NavigationPanel/PageItem.tsx +++ /dev/null @@ -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(null); - - const [popupY, setPopupY] = useState(0); - - useEffect(() => { - if (el.current) { - const { top } = el.current.getBoundingClientRect(); - setPopupY(top); - } - }, [showPageOptions, showRenamePopup]); - - return ( -
-
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 }} - > - -
- -
-
- {showPageOptions && ( - startPageRename()} - onDeleteClick={() => deletePage()} - onDuplicateClick={() => duplicatePage()} - onClose={() => closePopup()} - top={popupY - 124 + 40} - > - )} - {showRenamePopup && ( - changePageTitle(newTitle)} - onClose={closeRenamePopup} - top={popupY - 124 + 40} - > - )} -
- ); -}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/layout/Screen.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/layout/Screen.tsx index a8aad7cac7..950f960921 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/layout/Screen.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/layout/Screen.tsx @@ -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 (
- + {children} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/layout/Workspace.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/layout/Workspace.hooks.ts index fd76e8594e..80f0c75b03 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/layout/Workspace.hooks.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/layout/Workspace.hooks.ts @@ -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(null); + const [workspaceService, setWorkspaceService] = useState(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((v) => ({ + id: v.id, + title: v.name, + pageType: v.layout, + showPagesInside: false, + parentPageId: workspaceService.workspaceId, + })), + }) + ); + setIsReady(true); + } + })(); + }, [workspaceService]); + + return {}; }; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/tests/DatabaseTestHelper.ts b/frontend/appflowy_tauri/src/appflowy_app/components/tests/DatabaseTestHelper.ts index 8712fc8035..920a6aaa69 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/tests/DatabaseTestHelper.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/tests/DatabaseTestHelper.ts @@ -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 { 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 { diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/tests/DocumentTestHelper.ts b/frontend/appflowy_tauri/src/appflowy_app/components/tests/DocumentTestHelper.ts index f43f22e5a2..20eadbef18 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/tests/DocumentTestHelper.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/tests/DocumentTestHelper.ts @@ -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); + } } diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/tests/TestAPI.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/tests/TestAPI.tsx index aca8ebb3c2..57e6d91b73 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/tests/TestAPI.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/tests/TestAPI.tsx @@ -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 = () => { + {/*Folders*/} + ); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/tests/TestFolder.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/tests/TestFolder.tsx new file mode 100644 index 0000000000..ec7c2a1c55 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/tests/TestFolder.tsx @@ -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 ( + +
+ +
+
+ ); +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/tests/TestGrid.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/tests/TestGrid.tsx index 8d0ebd04ce..d4345b6760 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/tests/TestGrid.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/tests/TestGrid.tsx @@ -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 diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/tests/TestGroup.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/tests/TestGroup.tsx index b612d9ab9c..31ea758924 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/tests/TestGroup.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/tests/TestGroup.tsx @@ -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); diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/cell/cell_controller.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/cell/cell_controller.ts index aa0c75e919..d569c547ae 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/cell/cell_controller.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/cell/cell_controller.ts @@ -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 { /// 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 { 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(); }; } diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/database_bd_svc.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/database_bd_svc.ts index 4b17184ffc..cd3f125753 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/database_bd_svc.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/database_bd_svc.ts @@ -1,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) => { diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/database_controller.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/database_controller.ts index 953acf1538..dd98b41747 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/database_controller.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/database_controller.ts @@ -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(); }; diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/field/field_controller.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/field/field_controller.ts index 1cebae3ee0..46902ddef4 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/field/field_controller.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/field/field_controller.ts @@ -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 { } export class FieldInfo { - constructor(public readonly field: FieldPB) { - } + constructor(public readonly field: FieldPB) {} } diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/row/row_bd_svc.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/row/row_bd_svc.ts deleted file mode 100644 index 59770a34e2..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/row/row_bd_svc.ts +++ /dev/null @@ -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); - }; -} diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/folder/app/app_bd_svc.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/folder/app/app_bd_svc.ts deleted file mode 100644 index 7b2675726f..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/folder/app/app_bd_svc.ts +++ /dev/null @@ -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> => { - 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); - }; -} diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/folder/app/app_observer.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/folder/app/app_observer.ts deleted file mode 100644 index 5c59f4eb61..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/folder/app/app_observer.ts +++ /dev/null @@ -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(); - _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(); - }; -} diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/folder/view/view_bd_svc.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/folder/view/view_bd_svc.ts index ec98fb08ce..b867222e3c 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/folder/view/view_bd_svc.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/folder/view/view_bd_svc.ts @@ -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> => { + 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; + } }; } diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/folder/view/view_observer.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/folder/view/view_observer.ts index 9a5f687c53..f4c24c0d62 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/folder/view/view_observer.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/folder/view/view_observer.ts @@ -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; type UpdateViewNotifyValue = Result; @@ -12,17 +12,18 @@ export class ViewObserver { private _deleteViewNotifier = new ChangeNotifier(); private _updateViewNotifier = new ChangeNotifier(); private _restoreViewNotifier = new ChangeNotifier(); - private _moveToTashNotifier = new ChangeNotifier(); + private _moveToTrashNotifier = new ChangeNotifier(); + private _childViewsNotifier = new ChangeNotifier(); 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(); }; } diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/folder/workspace/workspace_bd_svc.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/folder/workspace/workspace_bd_svc.ts index 33cc4a76cb..fe58300a90 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/folder/workspace/workspace_bd_svc.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/folder/workspace/workspace_bd_svc.ts @@ -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> = 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, }); diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/folders/notifications/parser.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/folders/notifications/parser.ts deleted file mode 100644 index 27c091458b..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/folders/notifications/parser.ts +++ /dev/null @@ -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) => void; - -export class FolderNotificationParser extends NotificationParser { - 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; -}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/pages/slice.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/pages/slice.ts index 7ef7ba06ec..f6d07dafa1 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/pages/slice.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/pages/slice.ts @@ -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) { state.push(action.payload); }, + toggleShowPages(state, action: PayloadAction<{ id: string }>) { + return state.map((page: IPage) => + page.id === action.payload.id ? { ...page, showPagesInside: !page.showPagesInside } : page + ); + }, renamePage(state, action: PayloadAction<{ id: string; newTitle: string }>) { return state.map((page: IPage) => page.id === action.payload.id ? { ...page, title: action.payload.newTitle } : page diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/store.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/store.ts index 03c81a6427..09793408d7 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/stores/store.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/store.ts @@ -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, diff --git a/frontend/appflowy_tauri/src/appflowy_app/utils/async_queue.ts b/frontend/appflowy_tauri/src/appflowy_app/utils/async_queue.ts new file mode 100644 index 0000000000..394985ee21 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/utils/async_queue.ts @@ -0,0 +1,46 @@ +import { Log } from '$app/utils/log'; + +export class AsyncQueue { + private queue: T[] = []; + private isProcessing = false; + private executeFunction: (item: T) => Promise; + + constructor(executeFunction: (item: T) => Promise) { + 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 { + try { + await this.executeFunction(item); + } catch (error) { + Log.error('queue processing error:', error); + } + } +} diff --git a/frontend/appflowy_tauri/src/services/backend/notifications/observer.ts b/frontend/appflowy_tauri/src/services/backend/notifications/observer.ts index c6f951288e..8366be3729 100644 --- a/frontend/appflowy_tauri/src/services/backend/notifications/observer.ts +++ b/frontend/appflowy_tauri/src/services/backend/notifications/observer.ts @@ -25,6 +25,8 @@ export abstract class AFNotificationObserver { async stop() { if (this._listener !== undefined) { + // call the unlisten function before setting it to undefined + this._listener(); this._listener = undefined; } this.parser = null;