feat(core): workbench system (#5837)

This commit is contained in:
EYHN
2024-02-27 11:14:07 +07:00
committed by GitHub
parent 5cd488fe1d
commit 606397e319
31 changed files with 651 additions and 268 deletions

View File

@@ -1,7 +1,7 @@
import { ArrowDownSmallIcon } from '@blocksuite/icons'; import { ArrowDownSmallIcon } from '@blocksuite/icons';
import clsx from 'clsx'; import clsx from 'clsx';
import React from 'react'; import React from 'react';
import type { LinkProps } from 'react-router-dom'; import type { To } from 'react-router-dom';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import * as styles from './index.css'; import * as styles from './index.css';
@@ -17,9 +17,10 @@ export interface MenuItemProps extends React.HTMLAttributes<HTMLDivElement> {
postfix?: React.ReactElement; postfix?: React.ReactElement;
} }
export interface MenuLinkItemProps export interface MenuLinkItemProps extends MenuItemProps {
extends MenuItemProps, to: To;
Pick<LinkProps, 'to'> {} linkComponent?: React.ComponentType<{ to: To; className: string }>;
}
const stopPropagation: React.MouseEventHandler = e => { const stopPropagation: React.MouseEventHandler = e => {
e.stopPropagation(); e.stopPropagation();
@@ -89,13 +90,13 @@ export const MenuItem = React.forwardRef<HTMLDivElement, MenuItemProps>(
MenuItem.displayName = 'MenuItem'; MenuItem.displayName = 'MenuItem';
export const MenuLinkItem = React.forwardRef<HTMLDivElement, MenuLinkItemProps>( export const MenuLinkItem = React.forwardRef<HTMLDivElement, MenuLinkItemProps>(
({ to, ...props }, ref) => { ({ to, linkComponent: LinkComponent = Link, ...props }, ref) => {
return ( return (
<Link to={to} className={styles.linkItemRoot}> <LinkComponent to={to} className={styles.linkItemRoot}>
{/* The <a> element rendered by Link does not generate display box due to `display: contents` style */} {/* The <a> element rendered by Link does not generate display box due to `display: contents` style */}
{/* Thus ref is passed to MenuItem instead of Link */} {/* Thus ref is passed to MenuItem instead of Link */}
<MenuItem ref={ref} {...props}></MenuItem> <MenuItem ref={ref} {...props}></MenuItem>
</Link> </LinkComponent>
); );
} }
); );

View File

@@ -63,6 +63,7 @@
"foxact": "^0.2.31", "foxact": "^0.2.31",
"fractional-indexing": "^3.2.0", "fractional-indexing": "^3.2.0",
"graphql": "^16.8.1", "graphql": "^16.8.1",
"history": "^5.3.0",
"idb": "^8.0.0", "idb": "^8.0.0",
"image-blob-reduce": "^4.1.0", "image-blob-reduce": "^4.1.0",
"intl-segmenter-polyfill-rs": "^0.1.7", "intl-segmenter-polyfill-rs": "^0.1.7",

View File

@@ -1,5 +1,5 @@
import { Page, WorkspaceListService } from '@toeverything/infra'; import { WorkspaceListService } from '@toeverything/infra';
import { useService, useServiceOptional } from '@toeverything/infra/di'; import { useService } from '@toeverything/infra/di';
import { useLiveData } from '@toeverything/infra/livedata'; import { useLiveData } from '@toeverything/infra/livedata';
import { useEffect } from 'react'; import { useEffect } from 'react';
import { useLocation, useParams } from 'react-router-dom'; import { useLocation, useParams } from 'react-router-dom';
@@ -16,7 +16,6 @@ export const DumpInfo = (_props: DumpInfoProps) => {
const currentWorkspace = useLiveData( const currentWorkspace = useLiveData(
useService(CurrentWorkspaceService).currentWorkspace useService(CurrentWorkspaceService).currentWorkspace
); );
const currentPage = useServiceOptional(Page);
const path = location.pathname; const path = location.pathname;
const query = useParams(); const query = useParams();
useEffect(() => { useEffect(() => {
@@ -24,9 +23,8 @@ export const DumpInfo = (_props: DumpInfoProps) => {
path, path,
query, query,
currentWorkspaceId: currentWorkspace?.id, currentWorkspaceId: currentWorkspace?.id,
currentPageId: currentPage?.id,
workspaceList, workspaceList,
}); });
}, [path, query, currentWorkspace, workspaceList, currentPage?.id]); }, [path, query, currentWorkspace, workspaceList]);
return null; return null;
}; };

View File

@@ -2,8 +2,8 @@ import { Checkbox } from '@affine/component';
import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { useDraggable } from '@dnd-kit/core'; import { useDraggable } from '@dnd-kit/core';
import { type PropsWithChildren, useCallback, useMemo } from 'react'; import { type PropsWithChildren, useCallback, useMemo } from 'react';
import { Link } from 'react-router-dom';
import { WorkbenchLink } from '../../../modules/workbench/workbench-link';
import type { DraggableTitleCellData, PageListItemProps } from '../types'; import type { DraggableTitleCellData, PageListItemProps } from '../types';
import { ColWrapper, formatDate, stopPropagation } from '../utils'; import { ColWrapper, formatDate, stopPropagation } from '../utils';
import * as styles from './page-list-item.css'; import * as styles from './page-list-item.css';
@@ -235,14 +235,14 @@ function PageListItemWrapper({
'data-dragging': isDragging, 'data-dragging': isDragging,
onClick: handleClick, onClick: handleClick,
}), }),
[pageId, draggable, isDragging, onClick, to, handleClick] [pageId, draggable, onClick, to, isDragging, handleClick]
); );
if (to) { if (to) {
return ( return (
<Link {...commonProps} to={to}> <WorkbenchLink {...commonProps} to={to}>
{children} {children}
</Link> </WorkbenchLink>
); );
} else { } else {
return <div {...commonProps}>{children}</div>; return <div {...commonProps}>{children}</div>;

View File

@@ -321,10 +321,7 @@ function pageMetaToListItemProp(
), ),
createDate: new Date(item.createDate), createDate: new Date(item.createDate),
updatedDate: item.updatedDate ? new Date(item.updatedDate) : undefined, updatedDate: item.updatedDate ? new Date(item.updatedDate) : undefined,
to: to: props.rowAsLink && !props.selectable ? `/${item.id}` : undefined,
props.rowAsLink && !props.selectable
? `/workspace/${props.blockSuiteWorkspace.id}/${item.id}`
: undefined,
onClick: props.selectable ? toggleSelection : undefined, onClick: props.selectable ? toggleSelection : undefined,
icon: ( icon: (
<UnifiedPageIcon <UnifiedPageIcon
@@ -372,7 +369,7 @@ function collectionMetaToListItemProp(
title: item.title, title: item.title,
to: to:
props.rowAsLink && !props.selectable props.rowAsLink && !props.selectable
? `/workspace/${props.blockSuiteWorkspace.id}/collection/${item.id}` ? `/collection/${item.id}`
: undefined, : undefined,
onClick: props.selectable ? toggleSelection : undefined, onClick: props.selectable ? toggleSelection : undefined,
icon: <ViewLayersIcon />, icon: <ViewLayersIcon />,
@@ -407,10 +404,7 @@ function tagMetaToListItemProp(
const itemProps: TagListItemProps = { const itemProps: TagListItemProps = {
tagId: item.id, tagId: item.id,
title: item.title, title: item.title,
to: to: props.rowAsLink && !props.selectable ? `/tag/${item.id}` : undefined,
props.rowAsLink && !props.selectable
? `/workspace/${props.blockSuiteWorkspace.id}/tag/${item.id}`
: undefined,
onClick: props.selectable ? toggleSelection : undefined, onClick: props.selectable ? toggleSelection : undefined,
color: item.color, color: item.color,
pageCount: item.pageCount, pageCount: item.pageCount,

View File

@@ -17,11 +17,12 @@ import * as Collapsible from '@radix-ui/react-collapsible';
import { useService } from '@toeverything/infra'; import { useService } from '@toeverything/infra';
import { useLiveData } from '@toeverything/infra/livedata'; import { useLiveData } from '@toeverything/infra/livedata';
import { useCallback, useMemo, useState } from 'react'; import { useCallback, useMemo, useState } from 'react';
import { useLocation } from 'react-router-dom';
import { useAllPageListConfig } from '../../../../hooks/affine/use-all-page-list-config'; import { useAllPageListConfig } from '../../../../hooks/affine/use-all-page-list-config';
import { getDropItemId } from '../../../../hooks/affine/use-sidebar-drag'; import { getDropItemId } from '../../../../hooks/affine/use-sidebar-drag';
import { useBlockSuitePageMeta } from '../../../../hooks/use-block-suite-page-meta'; import { useBlockSuitePageMeta } from '../../../../hooks/use-block-suite-page-meta';
import { Workbench } from '../../../../modules/workbench';
import { WorkbenchLink } from '../../../../modules/workbench/workbench-link';
import type { CollectionsListProps } from '../index'; import type { CollectionsListProps } from '../index';
import { Page } from './page'; import { Page } from './page';
import * as styles from './styles.css'; import * as styles from './styles.css';
@@ -88,9 +89,9 @@ const CollectionRenderer = ({
}; };
return filterPage(collection, pageData); return filterPage(collection, pageData);
}); });
const location = useLocation(); const location = useLiveData(useService(Workbench).location);
const currentPath = location.pathname.split('?')[0]; const currentPath = location.pathname;
const path = `/workspace/${workspace.id}/collection/${collection.id}`; const path = `/collection/${collection.id}`;
const onRename = useCallback( const onRename = useCallback(
(name: string) => { (name: string) => {
@@ -115,6 +116,7 @@ const CollectionRenderer = ({
active={isOver || currentPath === path} active={isOver || currentPath === path}
icon={<AnimatedCollectionsIcon closed={isOver} />} icon={<AnimatedCollectionsIcon closed={isOver} />}
to={path} to={path}
linkComponent={WorkbenchLink}
postfix={ postfix={
<div <div
onClick={stopPropagation} onClick={stopPropagation}

View File

@@ -19,7 +19,7 @@ import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { FolderIcon, SettingsIcon } from '@blocksuite/icons'; import { FolderIcon, SettingsIcon } from '@blocksuite/icons';
import { type Page } from '@blocksuite/store'; import { type Page } from '@blocksuite/store';
import { useDroppable } from '@dnd-kit/core'; import { useDroppable } from '@dnd-kit/core';
import { useService, type Workspace } from '@toeverything/infra'; import { useLiveData, useService, type Workspace } from '@toeverything/infra';
import { useAtom, useAtomValue } from 'jotai'; import { useAtom, useAtomValue } from 'jotai';
import { nanoid } from 'nanoid'; import { nanoid } from 'nanoid';
import type { HTMLAttributes, ReactElement } from 'react'; import type { HTMLAttributes, ReactElement } from 'react';
@@ -34,6 +34,7 @@ import { getDropItemId } from '../../hooks/affine/use-sidebar-drag';
import { useTrashModalHelper } from '../../hooks/affine/use-trash-modal-helper'; import { useTrashModalHelper } from '../../hooks/affine/use-trash-modal-helper';
import { useRegisterBrowserHistoryCommands } from '../../hooks/use-browser-history-commands'; import { useRegisterBrowserHistoryCommands } from '../../hooks/use-browser-history-commands';
import { useNavigateHelper } from '../../hooks/use-navigate-helper'; import { useNavigateHelper } from '../../hooks/use-navigate-helper';
import { Workbench } from '../../modules/workbench';
import { WorkspaceSubPath } from '../../shared'; import { WorkspaceSubPath } from '../../shared';
import { import {
createEmptyCollection, createEmptyCollection,
@@ -57,7 +58,6 @@ export type RootAppSidebarProps = {
currentWorkspace: Workspace; currentWorkspace: Workspace;
openPage: (pageId: string) => void; openPage: (pageId: string) => void;
createPage: () => Page; createPage: () => Page;
currentPath: string;
paths: { paths: {
all: (workspaceId: string) => string; all: (workspaceId: string) => string;
trash: (workspaceId: string) => string; trash: (workspaceId: string) => string;
@@ -98,7 +98,6 @@ export const RootAppSidebar = ({
currentWorkspace, currentWorkspace,
openPage, openPage,
createPage, createPage,
currentPath,
paths, paths,
onOpenQuickSearchModal, onOpenQuickSearchModal,
onOpenSettingModal, onOpenSettingModal,
@@ -111,6 +110,7 @@ export const RootAppSidebar = ({
openWorkspaceListModalAtom openWorkspaceListModalAtom
); );
const generalShortcutsInfo = useGeneralShortcuts(); const generalShortcutsInfo = useGeneralShortcuts();
const currentPath = useLiveData(useService(Workbench).location).pathname;
const onClickNewPage = useAsyncCallback(async () => { const onClickNewPage = useAsyncCallback(async () => {
const page = createPage(); const page = createPage();
@@ -193,21 +193,9 @@ export const RootAppSidebar = ({
}); });
}, [blockSuiteWorkspace.id, collection, navigateHelper, open]); }, [blockSuiteWorkspace.id, collection, navigateHelper, open]);
const allPageActive = useMemo(() => { const allPageActive = currentPath === '/all';
if (
currentPath.startsWith(`/workspace/${currentWorkspaceId}/collection/`) ||
currentPath.startsWith(`/workspace/${currentWorkspaceId}/tag/`)
) {
return true;
}
return currentPath === paths.all(currentWorkspaceId);
}, [currentPath, currentWorkspaceId, paths]);
const trashActive = useMemo(() => { const trashActive = currentPath === '/trash';
return (
currentPath === paths.trash(currentWorkspaceId) || trashDroppable.isOver
);
}, [currentPath, currentWorkspaceId, paths, trashDroppable.isOver]);
return ( return (
<AppSidebar <AppSidebar

View File

@@ -1,21 +1,23 @@
import type { WorkspaceSubPath } from '@affine/core/shared'; import type { WorkspaceSubPath } from '@affine/core/shared';
import { useCallback, useMemo } from 'react'; import { useCallback, useMemo } from 'react';
import { import { type NavigateOptions, type To, useLocation } from 'react-router-dom';
type NavigateOptions,
useLocation, import { router } from '../router';
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
useNavigate,
} from 'react-router-dom';
export enum RouteLogic { export enum RouteLogic {
REPLACE = 'replace', REPLACE = 'replace',
PUSH = 'push', PUSH = 'push',
} }
function navigate(to: To, option?: { replace?: boolean }) {
router.navigate(to, option).catch(err => {
console.error('Failed to navigate', err);
});
}
// todo: add a name -> path helper in the results // todo: add a name -> path helper in the results
export function useNavigateHelper() { export function useNavigateHelper() {
const location = useLocation(); const location = useLocation();
const navigate = useNavigate();
const jumpToPage = useCallback( const jumpToPage = useCallback(
( (
@@ -27,7 +29,7 @@ export function useNavigateHelper() {
replace: logic === RouteLogic.REPLACE, replace: logic === RouteLogic.REPLACE,
}); });
}, },
[navigate] []
); );
const jumpToPageBlock = useCallback( const jumpToPageBlock = useCallback(
( (
@@ -40,7 +42,7 @@ export function useNavigateHelper() {
replace: logic === RouteLogic.REPLACE, replace: logic === RouteLogic.REPLACE,
}); });
}, },
[navigate] []
); );
const jumpToCollections = useCallback( const jumpToCollections = useCallback(
(workspaceId: string, logic: RouteLogic = RouteLogic.PUSH) => { (workspaceId: string, logic: RouteLogic = RouteLogic.PUSH) => {
@@ -48,7 +50,7 @@ export function useNavigateHelper() {
replace: logic === RouteLogic.REPLACE, replace: logic === RouteLogic.REPLACE,
}); });
}, },
[navigate] []
); );
const jumpToTags = useCallback( const jumpToTags = useCallback(
(workspaceId: string, logic: RouteLogic = RouteLogic.PUSH) => { (workspaceId: string, logic: RouteLogic = RouteLogic.PUSH) => {
@@ -56,7 +58,7 @@ export function useNavigateHelper() {
replace: logic === RouteLogic.REPLACE, replace: logic === RouteLogic.REPLACE,
}); });
}, },
[navigate] []
); );
const jumpToTag = useCallback( const jumpToTag = useCallback(
( (
@@ -68,7 +70,7 @@ export function useNavigateHelper() {
replace: logic === RouteLogic.REPLACE, replace: logic === RouteLogic.REPLACE,
}); });
}, },
[navigate] []
); );
const jumpToCollection = useCallback( const jumpToCollection = useCallback(
( (
@@ -80,7 +82,7 @@ export function useNavigateHelper() {
replace: logic === RouteLogic.REPLACE, replace: logic === RouteLogic.REPLACE,
}); });
}, },
[navigate] []
); );
const jumpToPublicWorkspacePage = useCallback( const jumpToPublicWorkspacePage = useCallback(
( (
@@ -92,7 +94,7 @@ export function useNavigateHelper() {
replace: logic === RouteLogic.REPLACE, replace: logic === RouteLogic.REPLACE,
}); });
}, },
[navigate] []
); );
const jumpToSubPath = useCallback( const jumpToSubPath = useCallback(
( (
@@ -104,7 +106,7 @@ export function useNavigateHelper() {
replace: logic === RouteLogic.REPLACE, replace: logic === RouteLogic.REPLACE,
}); });
}, },
[navigate] []
); );
const isPublicWorkspace = useMemo(() => { const isPublicWorkspace = useMemo(() => {
@@ -122,31 +124,22 @@ export function useNavigateHelper() {
[jumpToPage, jumpToPublicWorkspacePage, isPublicWorkspace] [jumpToPage, jumpToPublicWorkspacePage, isPublicWorkspace]
); );
const jumpToIndex = useCallback( const jumpToIndex = useCallback((logic: RouteLogic = RouteLogic.PUSH) => {
(logic: RouteLogic = RouteLogic.PUSH) => {
return navigate('/', { return navigate('/', {
replace: logic === RouteLogic.REPLACE, replace: logic === RouteLogic.REPLACE,
}); });
}, }, []);
[navigate]
);
const jumpTo404 = useCallback( const jumpTo404 = useCallback((logic: RouteLogic = RouteLogic.PUSH) => {
(logic: RouteLogic = RouteLogic.PUSH) => {
return navigate('/404', { return navigate('/404', {
replace: logic === RouteLogic.REPLACE, replace: logic === RouteLogic.REPLACE,
}); });
}, }, []);
[navigate] const jumpToExpired = useCallback((logic: RouteLogic = RouteLogic.PUSH) => {
);
const jumpToExpired = useCallback(
(logic: RouteLogic = RouteLogic.PUSH) => {
return navigate('/expired', { return navigate('/expired', {
replace: logic === RouteLogic.REPLACE, replace: logic === RouteLogic.REPLACE,
}); });
}, }, []);
[navigate]
);
const jumpToSignIn = useCallback( const jumpToSignIn = useCallback(
( (
logic: RouteLogic = RouteLogic.PUSH, logic: RouteLogic = RouteLogic.PUSH,
@@ -157,7 +150,7 @@ export function useNavigateHelper() {
...otherOptions, ...otherOptions,
}); });
}, },
[navigate] []
); );
return useMemo( return useMemo(

View File

@@ -20,7 +20,7 @@ import { useService } from '@toeverything/infra/di';
import { useAtom, useAtomValue, useSetAtom } from 'jotai'; import { useAtom, useAtomValue, useSetAtom } from 'jotai';
import type { PropsWithChildren, ReactNode } from 'react'; import type { PropsWithChildren, ReactNode } from 'react';
import { lazy, Suspense, useCallback, useEffect, useState } from 'react'; import { lazy, Suspense, useCallback, useEffect, useState } from 'react';
import { useLocation, useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import { Map as YMap } from 'yjs'; import { Map as YMap } from 'yjs';
import { openQuickSearchModalAtom, openSettingModalAtom } from '../atoms'; import { openQuickSearchModalAtom, openSettingModalAtom } from '../atoms';
@@ -151,7 +151,6 @@ export const WorkspaceLayoutInner = ({ children }: PropsWithChildren) => {
const handleDragEnd = useSidebarDrag(); const handleDragEnd = useSidebarDrag();
const { appSettings } = useAppSettingHelper(); const { appSettings } = useAppSettingHelper();
const location = useLocation();
const { pageId } = useParams(); const { pageId } = useParams();
// todo: refactor this that the root layout do not need to check route state // todo: refactor this that the root layout do not need to check route state
@@ -182,7 +181,6 @@ export const WorkspaceLayoutInner = ({ children }: PropsWithChildren) => {
[currentWorkspace, openPage] [currentWorkspace, openPage]
)} )}
createPage={handleCreatePage} createPage={handleCreatePage}
currentPath={location.pathname.split('?')[0]}
paths={pathGenerator} paths={pathGenerator}
/> />
</Suspense> </Suspense>

View File

@@ -1,16 +1,11 @@
import type { Page } from '@toeverything/infra';
import { import {
LiveData,
ServiceCollection,
type ServiceProvider, type ServiceProvider,
ServiceProviderContext, ServiceProviderContext,
useLiveData, useLiveData,
useService, useService,
useServiceOptional,
} from '@toeverything/infra'; } from '@toeverything/infra';
import type React from 'react'; import type React from 'react';
import { CurrentPageService } from '../../page';
import { CurrentWorkspaceService } from '../../workspace'; import { CurrentWorkspaceService } from '../../workspace';
export const GlobalScopeProvider: React.FC< export const GlobalScopeProvider: React.FC<
@@ -24,18 +19,8 @@ export const GlobalScopeProvider: React.FC<
currentWorkspaceService.currentWorkspace currentWorkspaceService.currentWorkspace
)?.services; )?.services;
const currentPageService = useServiceOptional(CurrentPageService, {
provider: workspaceProvider ?? ServiceCollection.EMPTY.provider(),
});
const pageProvider = useLiveData(
currentPageService?.currentPage ?? new LiveData<Page | null>(null)
)?.services;
return ( return (
<ServiceProviderContext.Provider <ServiceProviderContext.Provider value={workspaceProvider ?? rootProvider}>
value={pageProvider ?? workspaceProvider ?? rootProvider}
>
{children} {children}
</ServiceProviderContext.Provider> </ServiceProviderContext.Provider>
); );

View File

@@ -1,24 +0,0 @@
import type { Page } from '@toeverything/infra';
import { LiveData } from '@toeverything/infra/livedata';
/**
* service to manage current page
*/
export class CurrentPageService {
currentPage = new LiveData<Page | null>(null);
/**
* open page, current page will be set to the page
* @param page
*/
openPage(page: Page) {
this.currentPage.next(page);
}
/**
* close current page, current page will be null
*/
closePage() {
this.currentPage.next(null);
}
}

View File

@@ -1 +0,0 @@
export * from './current-page';

View File

@@ -11,7 +11,7 @@ import {
LocalStorageGlobalCache, LocalStorageGlobalCache,
LocalStorageGlobalState, LocalStorageGlobalState,
} from './infra-web/storage'; } from './infra-web/storage';
import { CurrentPageService } from './page'; import { Workbench } from './workbench';
import { import {
CurrentWorkspaceService, CurrentWorkspaceService,
WorkspaceLegacyProperties, WorkspaceLegacyProperties,
@@ -22,7 +22,7 @@ export function configureBusinessServices(services: ServiceCollection) {
services.add(CurrentWorkspaceService); services.add(CurrentWorkspaceService);
services services
.scope(WorkspaceScope) .scope(WorkspaceScope)
.add(CurrentPageService) .add(Workbench)
.add(WorkspacePropertiesAdapter, [Workspace]) .add(WorkspacePropertiesAdapter, [Workspace])
.add(CollectionService, [Workspace]) .add(CollectionService, [Workspace])
.add(WorkspaceLegacyProperties, [Workspace]); .add(WorkspaceLegacyProperties, [Workspace]);

View File

@@ -0,0 +1,105 @@
import { useLiveData } from '@toeverything/infra/livedata';
import { type Location } from 'history';
import { useEffect } from 'react';
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
import { useLocation, useNavigate } from 'react-router-dom';
import type { Workbench } from './workbench';
/**
* This hook binds the workbench to the browser router.
* It listens to the active view and updates the browser location accordingly.
* It also listens to the browser location and updates the active view accordingly.
*
* The history of the active view and the browser are two different stacks.
*
* In the browser, we use browser history as the criterion, and view history is not very important.
* So our synchronization strategy is as follows:
*
* 1. When the active view history changed, we update the browser history, based on the update action.
* - If the update action is PUSH, we navigate to the new location.
* - If the update action is REPLACE, we replace the current location.
* 2. When the browser location changed, we update the active view history just in PUSH action.
* 3. To avoid infinite loop, we add a state to the location to indicate the source of the change.
*/
export function useBindWorkbenchToBrowserRouter(
workbench: Workbench,
basename: string
) {
const navigate = useNavigate();
const browserLocation = useLocation();
const view = useLiveData(workbench.activeView);
useEffect(() => {
return view.history.listen(update => {
if (update.action === 'POP') {
// This is because the history of view and browser are two different stacks,
// the POP action cannot be synchronized.
throw new Error('POP view history is not allowed on browser');
}
if (update.location.state === 'fromBrowser') {
return;
}
const newBrowserLocation = viewLocationToBrowserLocation(
update.location,
basename
);
if (locationIsEqual(browserLocation, newBrowserLocation)) {
return;
}
navigate(newBrowserLocation, {
state: 'fromView',
replace: update.action === 'REPLACE',
});
});
}, [basename, browserLocation, navigate, view]);
useEffect(() => {
const newLocation = browserLocationToViewLocation(
browserLocation,
basename
);
if (newLocation === null) {
return;
}
view.history.push(newLocation, 'fromBrowser');
}, [basename, browserLocation, view]);
}
function browserLocationToViewLocation(
location: Location,
basename: string
): Location | null {
if (!location.pathname.startsWith(basename)) {
return null;
}
return {
...location,
pathname: location.pathname.slice(basename.length),
};
}
function viewLocationToBrowserLocation(
location: Location,
basename: string
): Location {
return {
...location,
pathname: `${basename}${location.pathname}`,
};
}
function locationIsEqual(a: Location, b: Location) {
return (
a.hash === b.hash &&
a.pathname === b.pathname &&
a.search === b.search &&
a.state === b.state
);
}

View File

@@ -0,0 +1,49 @@
import { type Location } from 'history';
import { useEffect } from 'react';
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
import { useLocation } from 'react-router-dom';
import type { Workbench } from './workbench';
/**
* This hook binds the workbench to the browser router.
*
* It listens to the browser location and updates the active view accordingly.
*
* In desktop, we not really care about the browser history, we only listen it,
* and never modify it.
*
* REPLACE and POP action in browser history is not supported.
* To do these actions, you should use the workbench API.
*/
export function useBindWorkbenchToDesktopRouter(
workbench: Workbench,
basename: string
) {
const browserLocation = useLocation();
useEffect(() => {
const newLocation = browserLocationToViewLocation(
browserLocation,
basename
);
if (newLocation === null) {
return;
}
workbench.open(newLocation);
}, [basename, browserLocation, workbench]);
}
function browserLocationToViewLocation(
location: Location,
basename: string
): Location | null {
if (!location.pathname.startsWith(basename)) {
return null;
}
return {
...location,
pathname: location.pathname.slice(basename.length),
};
}

View File

@@ -0,0 +1,2 @@
export * from './view';
export * from './workbench';

View File

@@ -0,0 +1 @@
export * from './view';

View File

@@ -0,0 +1,38 @@
import { useLiveData } from '@toeverything/infra/livedata';
import { useEffect, useMemo } from 'react';
import {
createMemoryRouter,
RouterProvider,
UNSAFE_LocationContext,
UNSAFE_RouteContext,
} from 'react-router-dom';
import { viewRoutes } from '../../../router';
import type { View } from './view';
export const ViewRoot = ({ view }: { view: View }) => {
const viewRouter = useMemo(() => createMemoryRouter(viewRoutes), []);
const location = useLiveData(view.location);
useEffect(() => {
viewRouter.navigate(location).catch(err => {
console.error('navigate error', err);
});
}, [location, view, viewRouter]);
// https://github.com/remix-run/react-router/issues/7375#issuecomment-975431736
return (
<UNSAFE_LocationContext.Provider value={null as any}>
<UNSAFE_RouteContext.Provider
value={{
outlet: null,
matches: [],
isDataRoute: false,
}}
>
<RouterProvider router={viewRouter} />
</UNSAFE_RouteContext.Provider>
</UNSAFE_LocationContext.Provider>
);
};

View File

@@ -0,0 +1,33 @@
import { LiveData } from '@toeverything/infra';
import type { Location, To } from 'history';
import { createMemoryHistory } from 'history';
import { nanoid } from 'nanoid';
import { Observable } from 'rxjs';
export class View {
id = nanoid();
history = createMemoryHistory();
location = LiveData.from<Location>(
new Observable(subscriber => {
subscriber.next(this.history.location);
return this.history.listen(update => {
subscriber.next(update.location);
});
}),
this.history.location
);
push(path: To) {
this.history.push(path);
}
go(n: number) {
this.history.go(n);
}
replace(path: To) {
this.history.replace(path);
}
}

View File

@@ -0,0 +1,35 @@
import { useService } from '@toeverything/infra/di';
import type { To } from 'history';
import { useCallback } from 'react';
import { Workbench } from './workbench';
export const WorkbenchLink = ({
to,
children,
onClick,
...other
}: React.PropsWithChildren<
{ to: To } & React.HTMLProps<HTMLAnchorElement>
>) => {
const workbench = useService(Workbench);
const handleClick = useCallback(
(event: React.MouseEvent<HTMLAnchorElement>) => {
event.preventDefault();
// TODO: open this when multi view control is implemented
// if (environment.isDesktop && (event.ctrlKey || event.metaKey)) {
// workbench.open(to, { at: 'beside' });
// } else {
workbench.open(to);
// }
onClick?.(event);
},
[onClick, to, workbench]
);
return (
<a {...other} href="#" onClick={handleClick}>
{children}
</a>
);
};

View File

@@ -0,0 +1,10 @@
import { style } from '@vanilla-extract/css';
export const workbenchRootContainer = style({
display: 'flex',
height: '100%',
});
export const workbenchViewContainer = style({
flex: 1,
});

View File

@@ -0,0 +1,54 @@
import { useService } from '@toeverything/infra/di';
import { useLiveData } from '@toeverything/infra/livedata';
import { useCallback } from 'react';
import { useLocation } from 'react-router-dom';
import { useBindWorkbenchToBrowserRouter } from './browser-adapter';
import { useBindWorkbenchToDesktopRouter } from './desktop-adapter';
import type { View } from './view';
import { ViewRoot } from './view/view-root';
import { Workbench } from './workbench';
import {
workbenchRootContainer,
workbenchViewContainer,
} from './workbench-root.css';
const useAdapter = environment.isDesktop
? useBindWorkbenchToDesktopRouter
: useBindWorkbenchToBrowserRouter;
export const WorkbenchRoot = () => {
const workbench = useService(Workbench);
// for debugging
(window as any).workbench = workbench;
const views = useLiveData(workbench.views);
const location = useLocation();
const basename = location.pathname.match(/\/workspace\/[^/]+/g)?.[0] ?? '/';
useAdapter(workbench, basename);
return (
<div className={workbenchRootContainer}>
{views.map((view, index) => (
<WorkbenchView key={view.id} view={view} index={index} />
))}
</div>
);
};
const WorkbenchView = ({ view, index }: { view: View; index: number }) => {
const workbench = useService(Workbench);
const handleOnFocus = useCallback(() => {
workbench.active(index);
}, [workbench, index]);
return (
<div className={workbenchViewContainer} onMouseDownCapture={handleOnFocus}>
<ViewRoot key={view.id} view={view} />
</div>
);
};

View File

@@ -0,0 +1,101 @@
import { Unreachable } from '@affine/env/constant';
import { LiveData } from '@toeverything/infra';
import type { To } from 'history';
import { combineLatest, map, switchMap } from 'rxjs';
import { View } from './view';
export type WorkbenchPosition = 'beside' | 'active' | number;
export class Workbench {
readonly views = new LiveData([new View()]);
activeViewIndex = new LiveData(0);
activeView = LiveData.from(
combineLatest([this.views, this.activeViewIndex]).pipe(
map(([views, index]) => views[index])
),
this.views.value[this.activeViewIndex.value]
);
location = LiveData.from(
this.activeView.pipe(switchMap(view => view.location)),
this.views.value[this.activeViewIndex.value].history.location
);
active(index: number) {
this.activeViewIndex.next(index);
}
createView(at: WorkbenchPosition = 'beside') {
const view = new View();
const newViews = [...this.views.value];
newViews.splice(this.indexAt(at), 0, view);
this.views.next(newViews);
return newViews.indexOf(view);
}
open(
to: To,
{
at = 'active',
replaceHistory = false,
}: { at?: WorkbenchPosition; replaceHistory?: boolean } = {}
) {
let view = this.viewAt(at);
if (!view) {
const newIndex = this.createView(at);
view = this.viewAt(newIndex);
if (!view) {
throw new Unreachable();
}
}
if (replaceHistory) {
view.history.replace(to);
} else {
view.history.push(to);
}
}
openPage(pageId: string) {
this.open(`/${pageId}`);
}
openCollections() {
this.open('/collection');
}
openCollection(collectionId: string) {
this.open(`/collection/${collectionId}`);
}
openAll() {
this.open('/all');
}
openTrash() {
this.open('/trash');
}
openTags() {
this.open('/tag');
}
openTag(tagId: string) {
this.open(`/tag/${tagId}`);
}
viewAt(positionIndex: WorkbenchPosition): View | undefined {
return this.views.value[this.indexAt(positionIndex)];
}
private indexAt(positionIndex: WorkbenchPosition): number {
if (positionIndex === 'active') {
return this.activeViewIndex.value;
}
if (positionIndex === 'beside') {
return this.activeViewIndex.value + 1;
}
return positionIndex;
}
}

View File

@@ -10,24 +10,24 @@ import {
StaticBlobStorage, StaticBlobStorage,
} from '@affine/workspace-impl'; } from '@affine/workspace-impl';
import { Logo1Icon } from '@blocksuite/icons'; import { Logo1Icon } from '@blocksuite/icons';
import type { Page } from '@toeverything/infra';
import { import {
EmptyBlobStorage, EmptyBlobStorage,
LocalBlobStorage, LocalBlobStorage,
LocalSyncStorage, LocalSyncStorage,
Page,
PageManager, PageManager,
type PageMode, type PageMode,
ReadonlyMappingSyncStorage, ReadonlyMappingSyncStorage,
RemoteBlobStorage, RemoteBlobStorage,
ServiceProviderContext,
useLiveData, useLiveData,
useService, useService,
useServiceOptional,
WorkspaceIdContext, WorkspaceIdContext,
WorkspaceManager, WorkspaceManager,
WorkspaceScope, WorkspaceScope,
} from '@toeverything/infra'; } from '@toeverything/infra';
import { noop } from 'foxact/noop'; import { noop } from 'foxact/noop';
import { useEffect } from 'react'; import { useEffect, useState } from 'react';
import type { LoaderFunction } from 'react-router-dom'; import type { LoaderFunction } from 'react-router-dom';
import { import {
isRouteErrorResponse, isRouteErrorResponse,
@@ -39,7 +39,6 @@ import {
import { AppContainer } from '../../components/affine/app-container'; import { AppContainer } from '../../components/affine/app-container';
import { PageDetailEditor } from '../../components/page-detail-editor'; import { PageDetailEditor } from '../../components/page-detail-editor';
import { SharePageNotFoundError } from '../../components/share-page-not-found-error'; import { SharePageNotFoundError } from '../../components/share-page-not-found-error';
import { CurrentPageService } from '../../modules/page';
import { CurrentWorkspaceService } from '../../modules/workspace'; import { CurrentWorkspaceService } from '../../modules/workspace';
import * as styles from './share-detail-page.css'; import * as styles from './share-detail-page.css';
import { ShareFooter } from './share-footer'; import { ShareFooter } from './share-footer';
@@ -131,6 +130,7 @@ export const Component = () => {
const currentWorkspace = useService(CurrentWorkspaceService); const currentWorkspace = useService(CurrentWorkspaceService);
const t = useAFFiNEI18N(); const t = useAFFiNEI18N();
const [page, setPage] = useState<Page | null>(null);
useEffect(() => { useEffect(() => {
// create a workspace for share page // create a workspace for share page
@@ -167,10 +167,8 @@ export const Component = () => {
true true
); );
const currentPage = workspace.services.get(CurrentPageService);
currentWorkspace.openWorkspace(workspace); currentWorkspace.openWorkspace(workspace);
currentPage.openPage(page); setPage(page);
}) })
.catch(err => { .catch(err => {
console.error(err); console.error(err);
@@ -184,7 +182,6 @@ export const Component = () => {
workspaceManager, workspaceManager,
]); ]);
const page = useServiceOptional(Page);
const pageTitle = useLiveData(page?.title); const pageTitle = useLiveData(page?.title);
usePageDocumentTitle(pageTitle); usePageDocumentTitle(pageTitle);
@@ -195,6 +192,7 @@ export const Component = () => {
} }
return ( return (
<ServiceProviderContext.Provider value={page.services}>
<AppContainer> <AppContainer>
<MainContainer> <MainContainer>
<div className={styles.root}> <div className={styles.root}>
@@ -234,6 +232,7 @@ export const Component = () => {
</div> </div>
</MainContainer> </MainContainer>
</AppContainer> </AppContainer>
</ServiceProviderContext.Provider>
); );
}; };

View File

@@ -18,8 +18,8 @@ import {
Page, Page,
PageManager, PageManager,
PageRecordList, PageRecordList,
ServiceProviderContext,
useLiveData, useLiveData,
useServiceOptional,
} from '@toeverything/infra'; } from '@toeverything/infra';
import { appSettingAtom, Workspace } from '@toeverything/infra'; import { appSettingAtom, Workspace } from '@toeverything/infra';
import { useService } from '@toeverything/infra'; import { useService } from '@toeverything/infra';
@@ -31,6 +31,7 @@ import {
useCallback, useCallback,
useEffect, useEffect,
useMemo, useMemo,
useState,
} from 'react'; } from 'react';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import type { Map as YMap } from 'yjs'; import type { Map as YMap } from 'yjs';
@@ -46,7 +47,6 @@ import { TopTip } from '../../../components/top-tip';
import { useRegisterBlocksuiteEditorCommands } from '../../../hooks/affine/use-register-blocksuite-editor-commands'; import { useRegisterBlocksuiteEditorCommands } from '../../../hooks/affine/use-register-blocksuite-editor-commands';
import { usePageDocumentTitle } from '../../../hooks/use-global-state'; import { usePageDocumentTitle } from '../../../hooks/use-global-state';
import { useNavigateHelper } from '../../../hooks/use-navigate-helper'; import { useNavigateHelper } from '../../../hooks/use-navigate-helper';
import { CurrentPageService } from '../../../modules/page';
import { performanceRenderLogger } from '../../../shared'; import { performanceRenderLogger } from '../../../shared';
import { PageNotFound } from '../../404'; import { PageNotFound } from '../../404';
import * as styles from './detail-page.css'; import * as styles from './detail-page.css';
@@ -270,28 +270,19 @@ export const DetailPage = ({ pageId }: { pageId: string }): ReactElement => {
); );
const pageManager = useService(PageManager); const pageManager = useService(PageManager);
const currentPageService = useService(CurrentPageService);
const [page, setPage] = useState<Page | null>(null);
useEffect(() => { useEffect(() => {
if (!pageRecord) { if (!pageRecord) {
return; return;
} }
const { page, release } = pageManager.open(pageRecord.id); const { page, release } = pageManager.open(pageRecord.id);
currentPageService.openPage(page); setPage(page);
return () => { return () => {
currentPageService.closePage();
release(); release();
}; };
}, [currentPageService, pageManager, pageRecord]); }, [pageManager, pageRecord]);
const page = useServiceOptional(Page);
const currentWorkspace = useService(Workspace);
// set sync engine priority target
useEffect(() => {
currentWorkspace.setPriorityRule(id => id.endsWith(pageId));
}, [pageId, currentWorkspace]);
const jumpOnce = useLiveData(pageRecord?.meta.map(meta => meta.jumpOnce)); const jumpOnce = useLiveData(pageRecord?.meta.map(meta => meta.jumpOnce));
@@ -310,7 +301,11 @@ export const DetailPage = ({ pageId }: { pageId: string }): ReactElement => {
return <PageDetailSkeleton key="current-page-is-null" />; return <PageDetailSkeleton key="current-page-is-null" />;
} }
return <DetailPageImpl />; return (
<ServiceProviderContext.Provider value={page.services}>
<DetailPageImpl />
</ServiceProviderContext.Provider>
);
}; };
export const Component = () => { export const Component = () => {

View File

@@ -5,7 +5,11 @@ import {
WorkspaceListService, WorkspaceListService,
WorkspaceManager, WorkspaceManager,
} from '@toeverything/infra'; } from '@toeverything/infra';
import { useService, useServiceOptional } from '@toeverything/infra/di'; import {
ServiceProviderContext,
useService,
useServiceOptional,
} from '@toeverything/infra/di';
import { useLiveData } from '@toeverything/infra/livedata'; import { useLiveData } from '@toeverything/infra/livedata';
import { import {
type ReactElement, type ReactElement,
@@ -14,10 +18,11 @@ import {
useMemo, useMemo,
useState, useState,
} from 'react'; } from 'react';
import { Outlet, useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import { AffineErrorBoundary } from '../../components/affine/affine-error-boundary'; import { AffineErrorBoundary } from '../../components/affine/affine-error-boundary';
import { WorkspaceLayout } from '../../layouts/workspace-layout'; import { WorkspaceLayout } from '../../layouts/workspace-layout';
import { WorkbenchRoot } from '../../modules/workbench/workbench-root';
import { CurrentWorkspaceService } from '../../modules/workspace/current-workspace'; import { CurrentWorkspaceService } from '../../modules/workspace/current-workspace';
import { performanceRenderLogger } from '../../shared'; import { performanceRenderLogger } from '../../shared';
import { PageNotFound } from '../404'; import { PageNotFound } from '../404';
@@ -105,12 +110,14 @@ export const Component = (): ReactElement => {
} }
return ( return (
<ServiceProviderContext.Provider value={currentWorkspace.services}>
<Suspense fallback={<WorkspaceFallback key="workspaceFallback" />}> <Suspense fallback={<WorkspaceFallback key="workspaceFallback" />}>
<AffineErrorBoundary height="100vh"> <AffineErrorBoundary height="100vh">
<WorkspaceLayout> <WorkspaceLayout>
<Outlet /> <WorkbenchRoot />
</WorkspaceLayout> </WorkspaceLayout>
</AffineErrorBoundary> </AffineErrorBoundary>
</Suspense> </Suspense>
</ServiceProviderContext.Provider>
); );
}; };

View File

@@ -2,44 +2,14 @@ import * as Sentry from '@sentry/react';
import type { RouteObject } from 'react-router-dom'; import type { RouteObject } from 'react-router-dom';
import { createBrowserRouter as reactRouterCreateBrowserRouter } from 'react-router-dom'; import { createBrowserRouter as reactRouterCreateBrowserRouter } from 'react-router-dom';
export const routes = [ export const workbenchRoutes = [
{ {
path: '/', path: '/',
lazy: () => import('./pages/index'), lazy: () => import('./pages/index'),
}, },
{ {
path: '/workspace/:workspaceId', path: '/workspace/:workspaceId/*',
lazy: () => import('./pages/workspace/index'), lazy: () => import('./pages/workspace/index'),
children: [
{
path: 'all',
lazy: () => import('./pages/workspace/all-page/all-page'),
},
{
path: 'collection',
lazy: () => import('./pages/workspace/all-collection'),
},
{
path: 'collection/:collectionId',
lazy: () => import('./pages/workspace/collection/index'),
},
{
path: 'tag',
lazy: () => import('./pages/workspace/all-tag'),
},
{
path: 'tag/:tagId',
lazy: () => import('./pages/workspace/tag'),
},
{
path: 'trash',
lazy: () => import('./pages/workspace/trash-page'),
},
{
path: ':pageId',
lazy: () => import('./pages/workspace/detail-page/detail-page'),
},
],
}, },
{ {
path: '/share/:workspaceId/:pageId', path: '/share/:workspaceId/:pageId',
@@ -87,10 +57,45 @@ export const routes = [
}, },
] satisfies [RouteObject, ...RouteObject[]]; ] satisfies [RouteObject, ...RouteObject[]];
export const viewRoutes = [
{
path: '/all',
lazy: () => import('./pages/workspace/all-page/all-page'),
},
{
path: '/collection',
lazy: () => import('./pages/workspace/all-collection'),
},
{
path: '/collection/:collectionId',
lazy: () => import('./pages/workspace/collection/index'),
},
{
path: '/tag',
lazy: () => import('./pages/workspace/all-tag'),
},
{
path: '/tag/:tagId',
lazy: () => import('./pages/workspace/tag'),
},
{
path: '/trash',
lazy: () => import('./pages/workspace/trash-page'),
},
{
path: '/:pageId',
lazy: () => import('./pages/workspace/detail-page/detail-page'),
},
{
path: '*',
lazy: () => import('./pages/404'),
},
] satisfies [RouteObject, ...RouteObject[]];
const createBrowserRouter = Sentry.wrapCreateBrowserRouter( const createBrowserRouter = Sentry.wrapCreateBrowserRouter(
reactRouterCreateBrowserRouter reactRouterCreateBrowserRouter
); );
export const router = createBrowserRouter(routes, { export const router = createBrowserRouter(workbenchRoutes, {
future: { future: {
v7_normalizeFormMethod: true, v7_normalizeFormMethod: true,
}, },

View File

@@ -7,7 +7,6 @@ import {
WorkspaceManager, WorkspaceManager,
} from '@toeverything/infra'; } from '@toeverything/infra';
import { CurrentPageService } from './modules/page';
import { CurrentWorkspaceService } from './modules/workspace'; import { CurrentWorkspaceService } from './modules/workspace';
import { configureWebServices } from './web'; import { configureWebServices } from './web';
@@ -40,7 +39,6 @@ export async function configureTestingEnvironment() {
const { page } = workspace.services.get(PageManager).open('page0'); const { page } = workspace.services.get(PageManager).open('page0');
rootServices.get(CurrentWorkspaceService).openWorkspace(workspace); rootServices.get(CurrentWorkspaceService).openWorkspace(workspace);
workspace.services.get(CurrentPageService).openPage(page);
return { services: rootServices, workspace, page }; return { services: rootServices, workspace, page };
} }

View File

@@ -1,4 +1,4 @@
import { routes } from '@affine/core/router'; import { workbenchRoutes } from '@affine/core/router';
import { assertExists } from '@blocksuite/global/utils'; import { assertExists } from '@blocksuite/global/utils';
import type { StoryFn } from '@storybook/react'; import type { StoryFn } from '@storybook/react';
import { screen, userEvent, waitFor, within } from '@storybook/testing-library'; import { screen, userEvent, waitFor, within } from '@storybook/testing-library';
@@ -28,7 +28,7 @@ export const Index: StoryFn = () => {
Index.decorators = [withRouter]; Index.decorators = [withRouter];
Index.parameters = { Index.parameters = {
reactRouter: reactRouterParameters({ reactRouter: reactRouterParameters({
routing: reactRouterOutlets(routes), routing: reactRouterOutlets(workbenchRoutes),
}), }),
}; };
@@ -74,7 +74,7 @@ SettingPage.play = async ({ canvasElement, step }) => {
SettingPage.decorators = [withRouter]; SettingPage.decorators = [withRouter];
SettingPage.parameters = { SettingPage.parameters = {
reactRouter: reactRouterParameters({ reactRouter: reactRouterParameters({
routing: reactRouterOutlets(routes), routing: reactRouterOutlets(workbenchRoutes),
}), }),
}; };
@@ -84,7 +84,7 @@ export const NotFoundPage: StoryFn = () => {
NotFoundPage.decorators = [withRouter]; NotFoundPage.decorators = [withRouter];
NotFoundPage.parameters = { NotFoundPage.parameters = {
reactRouter: reactRouterParameters({ reactRouter: reactRouterParameters({
routing: reactRouterOutlets(routes), routing: reactRouterOutlets(workbenchRoutes),
location: { location: {
path: '/404', path: '/404',
}, },
@@ -114,7 +114,7 @@ WorkspaceList.play = async ({ canvasElement }) => {
WorkspaceList.decorators = [withRouter]; WorkspaceList.decorators = [withRouter];
WorkspaceList.parameters = { WorkspaceList.parameters = {
reactRouter: reactRouterParameters({ reactRouter: reactRouterParameters({
routing: reactRouterOutlets(routes), routing: reactRouterOutlets(workbenchRoutes),
location: { location: {
path: '/', path: '/',
}, },
@@ -151,7 +151,7 @@ SearchPage.play = async ({ canvasElement }) => {
SearchPage.decorators = [withRouter]; SearchPage.decorators = [withRouter];
SearchPage.parameters = { SearchPage.parameters = {
reactRouter: reactRouterParameters({ reactRouter: reactRouterParameters({
routing: reactRouterOutlets(routes), routing: reactRouterOutlets(workbenchRoutes),
location: { location: {
path: '/', path: '/',
}, },
@@ -193,7 +193,7 @@ ImportPage.play = async ({ canvasElement }) => {
ImportPage.decorators = [withRouter]; ImportPage.decorators = [withRouter];
ImportPage.parameters = { ImportPage.parameters = {
reactRouter: reactRouterParameters({ reactRouter: reactRouterParameters({
routing: reactRouterOutlets(routes), routing: reactRouterOutlets(workbenchRoutes),
location: { location: {
path: '/', path: '/',
}, },
@@ -206,7 +206,7 @@ export const OpenAppPage: StoryFn = () => {
OpenAppPage.decorators = [withRouter]; OpenAppPage.decorators = [withRouter];
OpenAppPage.parameters = { OpenAppPage.parameters = {
reactRouter: reactRouterParameters({ reactRouter: reactRouterParameters({
routing: reactRouterOutlets(routes), routing: reactRouterOutlets(workbenchRoutes),
location: { location: {
path: '/open-app/url', path: '/open-app/url',
searchParams: { searchParams: {

View File

@@ -1,9 +1,13 @@
import { BlockSuiteEditor } from '@affine/core/components/blocksuite/block-suite-editor'; import { BlockSuiteEditor } from '@affine/core/components/blocksuite/block-suite-editor';
import { ImagePreviewModal } from '@affine/core/components/image-preview'; import { ImagePreviewModal } from '@affine/core/components/image-preview';
import { CurrentPageService } from '@affine/core/modules/page';
import type { Page } from '@blocksuite/store';
import type { Meta } from '@storybook/react'; import type { Meta } from '@storybook/react';
import { PageManager, useService, Workspace } from '@toeverything/infra'; import type { Page } from '@toeverything/infra';
import {
PageManager,
ServiceProviderContext,
useService,
Workspace,
} from '@toeverything/infra';
import { initEmptyPage } from '@toeverything/infra'; import { initEmptyPage } from '@toeverything/infra';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { createPortal } from 'react-dom'; import { createPortal } from 'react-dom';
@@ -16,7 +20,6 @@ export default {
export const Default = () => { export const Default = () => {
const workspace = useService(Workspace); const workspace = useService(Workspace);
const pageManager = useService(PageManager); const pageManager = useService(PageManager);
const currentPageService = useService(CurrentPageService);
const [page, setPage] = useState<Page | null>(null); const [page, setPage] = useState<Page | null>(null);
@@ -25,7 +28,6 @@ export const Default = () => {
initEmptyPage(bsPage); initEmptyPage(bsPage);
const { page, release } = pageManager.open(bsPage.meta.id); const { page, release } = pageManager.open(bsPage.meta.id);
currentPageService.openPage(page);
fetch(new URL('@affine-test/fixtures/large-image.png', import.meta.url)) fetch(new URL('@affine-test/fixtures/large-image.png', import.meta.url))
.then(res => res.arrayBuffer()) .then(res => res.arrayBuffer())
@@ -54,19 +56,19 @@ export const Default = () => {
.catch(err => { .catch(err => {
console.error('Failed to load large-image.png', err); console.error('Failed to load large-image.png', err);
}); });
setPage(bsPage); setPage(page);
return () => { return () => {
release(); release();
currentPageService.closePage();
}; };
}, [currentPageService, pageManager, workspace]); }, [pageManager, workspace]);
if (!page) { if (!page) {
return null; return null;
} }
return ( return (
<ServiceProviderContext.Provider value={page.services}>
<div <div
style={{ style={{
height: '100vh', height: '100vh',
@@ -74,11 +76,15 @@ export const Default = () => {
overflow: 'auto', overflow: 'auto',
}} }}
> >
<BlockSuiteEditor mode="page" page={page} /> <BlockSuiteEditor mode="page" page={page.blockSuitePage} />
{createPortal( {createPortal(
<ImagePreviewModal pageId={page.id} workspace={page.workspace} />, <ImagePreviewModal
pageId={page.id}
workspace={page.blockSuitePage.workspace}
/>,
document.body document.body
)} )}
</div> </div>
</ServiceProviderContext.Provider>
); );
}; };

View File

@@ -356,6 +356,7 @@ __metadata:
foxact: "npm:^0.2.31" foxact: "npm:^0.2.31"
fractional-indexing: "npm:^3.2.0" fractional-indexing: "npm:^3.2.0"
graphql: "npm:^16.8.1" graphql: "npm:^16.8.1"
history: "npm:^5.3.0"
html-webpack-plugin: "npm:^5.6.0" html-webpack-plugin: "npm:^5.6.0"
idb: "npm:^8.0.0" idb: "npm:^8.0.0"
image-blob-reduce: "npm:^4.1.0" image-blob-reduce: "npm:^4.1.0"
@@ -3356,7 +3357,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@babel/runtime@npm:^7.0.0, @babel/runtime@npm:^7.10.2, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.13.10, @babel/runtime@npm:^7.16.7, @babel/runtime@npm:^7.17.8, @babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.20.13, @babel/runtime@npm:^7.20.6, @babel/runtime@npm:^7.21.0, @babel/runtime@npm:^7.22.6, @babel/runtime@npm:^7.23.2, @babel/runtime@npm:^7.23.9, @babel/runtime@npm:^7.7.2, @babel/runtime@npm:^7.8.4, @babel/runtime@npm:^7.9.2": "@babel/runtime@npm:^7.0.0, @babel/runtime@npm:^7.10.2, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.13.10, @babel/runtime@npm:^7.16.7, @babel/runtime@npm:^7.17.8, @babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.20.13, @babel/runtime@npm:^7.20.6, @babel/runtime@npm:^7.21.0, @babel/runtime@npm:^7.22.6, @babel/runtime@npm:^7.23.2, @babel/runtime@npm:^7.23.9, @babel/runtime@npm:^7.7.2, @babel/runtime@npm:^7.7.6, @babel/runtime@npm:^7.8.4, @babel/runtime@npm:^7.9.2":
version: 7.23.9 version: 7.23.9
resolution: "@babel/runtime@npm:7.23.9" resolution: "@babel/runtime@npm:7.23.9"
dependencies: dependencies:
@@ -22603,6 +22604,15 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"history@npm:^5.3.0":
version: 5.3.0
resolution: "history@npm:5.3.0"
dependencies:
"@babel/runtime": "npm:^7.7.6"
checksum: 10/52ba685b842ca6438ff11ef459951eb13d413ae715866a8dc5f7c3b1ea0cdeb8db6aabf7254551b85f56abc205e6e2d7e1d5afb36b711b401cdaff4f2cf187e9
languageName: node
linkType: hard
"hoist-non-react-statics@npm:^3.3.1, hoist-non-react-statics@npm:^3.3.2": "hoist-non-react-statics@npm:^3.3.1, hoist-non-react-statics@npm:^3.3.2":
version: 3.3.2 version: 3.3.2
resolution: "hoist-non-react-statics@npm:3.3.2" resolution: "hoist-non-react-statics@npm:3.3.2"