feat(core): workbench system (#5837)
This commit is contained in:
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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>;
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
});
|
||||||
});
|
}, []);
|
||||||
},
|
const jumpToExpired = useCallback((logic: RouteLogic = RouteLogic.PUSH) => {
|
||||||
[navigate]
|
return navigate('/expired', {
|
||||||
);
|
replace: logic === RouteLogic.REPLACE,
|
||||||
const jumpToExpired = useCallback(
|
});
|
||||||
(logic: RouteLogic = RouteLogic.PUSH) => {
|
}, []);
|
||||||
return navigate('/expired', {
|
|
||||||
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(
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export * from './current-page';
|
|
||||||
@@ -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]);
|
||||||
|
|||||||
105
packages/frontend/core/src/modules/workbench/browser-adapter.ts
Normal file
105
packages/frontend/core/src/modules/workbench/browser-adapter.ts
Normal 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
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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),
|
||||||
|
};
|
||||||
|
}
|
||||||
2
packages/frontend/core/src/modules/workbench/index.ts
Normal file
2
packages/frontend/core/src/modules/workbench/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export * from './view';
|
||||||
|
export * from './workbench';
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from './view';
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
33
packages/frontend/core/src/modules/workbench/view/view.ts
Normal file
33
packages/frontend/core/src/modules/workbench/view/view.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { style } from '@vanilla-extract/css';
|
||||||
|
|
||||||
|
export const workbenchRootContainer = style({
|
||||||
|
display: 'flex',
|
||||||
|
height: '100%',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const workbenchViewContainer = style({
|
||||||
|
flex: 1,
|
||||||
|
});
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
101
packages/frontend/core/src/modules/workbench/workbench.ts
Normal file
101
packages/frontend/core/src/modules/workbench/workbench.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,45 +192,47 @@ export const Component = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppContainer>
|
<ServiceProviderContext.Provider value={page.services}>
|
||||||
<MainContainer>
|
<AppContainer>
|
||||||
<div className={styles.root}>
|
<MainContainer>
|
||||||
<div className={styles.mainContainer}>
|
<div className={styles.root}>
|
||||||
<ShareHeader
|
<div className={styles.mainContainer}>
|
||||||
pageId={page.id}
|
<ShareHeader
|
||||||
publishMode={publishMode}
|
pageId={page.id}
|
||||||
blockSuiteWorkspace={page.blockSuitePage.workspace}
|
publishMode={publishMode}
|
||||||
/>
|
blockSuiteWorkspace={page.blockSuitePage.workspace}
|
||||||
<Scrollable.Root>
|
/>
|
||||||
<Scrollable.Viewport className={styles.editorContainer}>
|
<Scrollable.Root>
|
||||||
<PageDetailEditor
|
<Scrollable.Viewport className={styles.editorContainer}>
|
||||||
isPublic
|
<PageDetailEditor
|
||||||
publishMode={publishMode}
|
isPublic
|
||||||
workspace={page.blockSuitePage.workspace}
|
publishMode={publishMode}
|
||||||
pageId={page.id}
|
workspace={page.blockSuitePage.workspace}
|
||||||
onLoad={() => noop}
|
pageId={page.id}
|
||||||
/>
|
onLoad={() => noop}
|
||||||
{publishMode === 'page' ? <ShareFooter /> : null}
|
/>
|
||||||
</Scrollable.Viewport>
|
{publishMode === 'page' ? <ShareFooter /> : null}
|
||||||
<Scrollable.Scrollbar />
|
</Scrollable.Viewport>
|
||||||
</Scrollable.Root>
|
<Scrollable.Scrollbar />
|
||||||
{loginStatus !== 'authenticated' ? (
|
</Scrollable.Root>
|
||||||
<a
|
{loginStatus !== 'authenticated' ? (
|
||||||
href="https://affine.pro"
|
<a
|
||||||
target="_blank"
|
href="https://affine.pro"
|
||||||
className={styles.link}
|
target="_blank"
|
||||||
rel="noreferrer"
|
className={styles.link}
|
||||||
>
|
rel="noreferrer"
|
||||||
<span className={styles.linkText}>
|
>
|
||||||
{t['com.affine.share-page.footer.built-with']()}
|
<span className={styles.linkText}>
|
||||||
</span>
|
{t['com.affine.share-page.footer.built-with']()}
|
||||||
<Logo1Icon fontSize={20} />
|
</span>
|
||||||
</a>
|
<Logo1Icon fontSize={20} />
|
||||||
) : null}
|
</a>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</MainContainer>
|
||||||
</MainContainer>
|
</AppContainer>
|
||||||
</AppContainer>
|
</ServiceProviderContext.Provider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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 = () => {
|
||||||
|
|||||||
@@ -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 (
|
||||||
<Suspense fallback={<WorkspaceFallback key="workspaceFallback" />}>
|
<ServiceProviderContext.Provider value={currentWorkspace.services}>
|
||||||
<AffineErrorBoundary height="100vh">
|
<Suspense fallback={<WorkspaceFallback key="workspaceFallback" />}>
|
||||||
<WorkspaceLayout>
|
<AffineErrorBoundary height="100vh">
|
||||||
<Outlet />
|
<WorkspaceLayout>
|
||||||
</WorkspaceLayout>
|
<WorkbenchRoot />
|
||||||
</AffineErrorBoundary>
|
</WorkspaceLayout>
|
||||||
</Suspense>
|
</AffineErrorBoundary>
|
||||||
|
</Suspense>
|
||||||
|
</ServiceProviderContext.Provider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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,
|
||||||
},
|
},
|
||||||
@@ -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 };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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: {
|
||||||
|
|||||||
@@ -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,31 +56,35 @@ 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 (
|
||||||
<div
|
<ServiceProviderContext.Provider value={page.services}>
|
||||||
style={{
|
<div
|
||||||
height: '100vh',
|
style={{
|
||||||
width: '100vw',
|
height: '100vh',
|
||||||
overflow: 'auto',
|
width: '100vw',
|
||||||
}}
|
overflow: 'auto',
|
||||||
>
|
}}
|
||||||
<BlockSuiteEditor mode="page" page={page} />
|
>
|
||||||
{createPortal(
|
<BlockSuiteEditor mode="page" page={page.blockSuitePage} />
|
||||||
<ImagePreviewModal pageId={page.id} workspace={page.workspace} />,
|
{createPortal(
|
||||||
document.body
|
<ImagePreviewModal
|
||||||
)}
|
pageId={page.id}
|
||||||
</div>
|
workspace={page.blockSuitePage.workspace}
|
||||||
|
/>,
|
||||||
|
document.body
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</ServiceProviderContext.Provider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
12
yarn.lock
12
yarn.lock
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user