mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-17 01:56:27 +08:00
refactor(core): better block link (#8082)
This commit is contained in:
@@ -6,7 +6,6 @@ import {
|
||||
} from '@affine/core/modules/peek-view';
|
||||
import { WorkbenchLink } from '@affine/core/modules/workbench';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import type { BlockStdScope } from '@blocksuite/block-std';
|
||||
import type { DocMode } from '@blocksuite/blocks';
|
||||
import {
|
||||
BlockLinkIcon,
|
||||
@@ -17,16 +16,16 @@ import {
|
||||
} from '@blocksuite/icons/rc';
|
||||
import type { DocCollection } from '@blocksuite/store';
|
||||
import { useService } from '@toeverything/infra';
|
||||
import { nanoid } from 'nanoid';
|
||||
import {
|
||||
type PropsWithChildren,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
|
||||
import * as styles from './styles.css';
|
||||
import { scrollAnchoring } from './utils';
|
||||
|
||||
export interface PageReferenceRendererOptions {
|
||||
pageId: string;
|
||||
@@ -90,8 +89,6 @@ export function AffinePageReference({
|
||||
wrapper: Wrapper,
|
||||
mode = 'page',
|
||||
params = {},
|
||||
isSameDoc = false,
|
||||
std,
|
||||
}: {
|
||||
pageId: string;
|
||||
docCollection: DocCollection;
|
||||
@@ -102,16 +99,10 @@ export function AffinePageReference({
|
||||
blockIds?: string[];
|
||||
elementIds?: string[];
|
||||
};
|
||||
isSameDoc?: boolean;
|
||||
std?: BlockStdScope;
|
||||
}) {
|
||||
const pageMetaHelper = useDocMetaHelper(docCollection);
|
||||
const journalHelper = useJournalHelper(docCollection);
|
||||
const t = useI18n();
|
||||
const [anchor, setAnchor] = useState<{
|
||||
mode: DocMode;
|
||||
id: string;
|
||||
} | null>(null);
|
||||
|
||||
const { mode: linkedWithMode, blockIds, elementIds } = params;
|
||||
|
||||
@@ -128,21 +119,11 @@ export function AffinePageReference({
|
||||
|
||||
const ref = useRef<HTMLAnchorElement>(null);
|
||||
|
||||
const [refreshKey, setRefreshKey] = useState<string>(() => nanoid());
|
||||
|
||||
const peekView = useService(PeekViewService).peekView;
|
||||
const isInPeekView = useInsidePeekView();
|
||||
|
||||
useEffect(() => {
|
||||
if (isSameDoc) {
|
||||
if (mode === 'edgeless' && elementIds?.length) {
|
||||
setAnchor({ mode, id: elementIds[0] });
|
||||
} else if (blockIds?.length) {
|
||||
setAnchor({ mode, id: blockIds[0] });
|
||||
}
|
||||
} else {
|
||||
setAnchor(null);
|
||||
}
|
||||
}, [std, isSameDoc, mode, blockIds, elementIds]);
|
||||
|
||||
const onClick = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
if (e.shiftKey && ref.current) {
|
||||
@@ -151,33 +132,34 @@ export function AffinePageReference({
|
||||
peekView.open(ref.current).catch(console.error);
|
||||
}
|
||||
|
||||
if (std && anchor) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const { mode, id } = anchor;
|
||||
scrollAnchoring(std, mode, id);
|
||||
} else if (isInPeekView) {
|
||||
if (isInPeekView) {
|
||||
peekView.close();
|
||||
}
|
||||
|
||||
// update refresh key
|
||||
setRefreshKey(nanoid());
|
||||
|
||||
return;
|
||||
},
|
||||
[isInPeekView, peekView, anchor, std]
|
||||
[isInPeekView, peekView]
|
||||
);
|
||||
|
||||
// A block/element reference link
|
||||
const search = new URLSearchParams();
|
||||
if (linkedWithMode) {
|
||||
search.set('mode', linkedWithMode);
|
||||
}
|
||||
if (blockIds?.length) {
|
||||
search.set('blockIds', blockIds.join(','));
|
||||
}
|
||||
if (elementIds?.length) {
|
||||
search.set('elementIds', elementIds.join(','));
|
||||
}
|
||||
const query = useMemo(() => {
|
||||
// A block/element reference link
|
||||
const search = new URLSearchParams();
|
||||
if (linkedWithMode) {
|
||||
search.set('mode', linkedWithMode);
|
||||
}
|
||||
if (blockIds?.length) {
|
||||
search.set('blockIds', blockIds.join(','));
|
||||
}
|
||||
if (elementIds?.length) {
|
||||
search.set('elementIds', elementIds.join(','));
|
||||
}
|
||||
search.set('refreshKey', refreshKey);
|
||||
|
||||
const query = search.size > 0 ? `?${search.toString()}` : '';
|
||||
return search.size > 0 ? `?${search.toString()}` : '';
|
||||
}, [blockIds, elementIds, linkedWithMode, refreshKey]);
|
||||
|
||||
return (
|
||||
<WorkbenchLink
|
||||
|
||||
+21
-47
@@ -1,7 +1,6 @@
|
||||
import type { EditorSelector } from '@affine/core/modules/editor';
|
||||
import { EditorService } from '@affine/core/modules/editor';
|
||||
import type { ReferenceInfo } from '@blocksuite/affine-model';
|
||||
import type { DocMode } from '@blocksuite/blocks';
|
||||
import type { InlineEditor } from '@blocksuite/inline/inline-editor';
|
||||
import type {
|
||||
AffineEditorContainer,
|
||||
DocTitle,
|
||||
@@ -9,6 +8,7 @@ import type {
|
||||
PageEditor,
|
||||
} from '@blocksuite/presets';
|
||||
import { type Doc, Slot } from '@blocksuite/store';
|
||||
import { useService } from '@toeverything/infra';
|
||||
import clsx from 'clsx';
|
||||
import type React from 'react';
|
||||
import {
|
||||
@@ -18,10 +18,8 @@ import {
|
||||
useLayoutEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
|
||||
import { scrollAnchoring } from '../../affine/reference-link/utils';
|
||||
import { BlocksuiteDocEditor, BlocksuiteEdgelessEditor } from './lit-adaper';
|
||||
import * as styles from './styles.css';
|
||||
|
||||
@@ -46,7 +44,6 @@ interface BlocksuiteEditorContainerProps {
|
||||
shared?: boolean;
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
defaultEditorSelector?: EditorSelector;
|
||||
}
|
||||
|
||||
// mimic the interface of the webcomponent and expose slots & host
|
||||
@@ -60,21 +57,14 @@ export const BlocksuiteEditorContainer = forwardRef<
|
||||
AffineEditorContainer,
|
||||
BlocksuiteEditorContainerProps
|
||||
>(function AffineEditorContainer(
|
||||
{ page, mode, className, style, shared, defaultEditorSelector },
|
||||
{ page, mode, className, style, shared },
|
||||
ref
|
||||
) {
|
||||
const editorService = useService(EditorService);
|
||||
const rootRef = useRef<HTMLDivElement>(null);
|
||||
const docRef = useRef<PageEditor>(null);
|
||||
const docTitleRef = useRef<DocTitle>(null);
|
||||
const edgelessRef = useRef<EdgelessEditor>(null);
|
||||
const [anchor] = useState<string | null>(() => {
|
||||
if (mode === 'edgeless' && defaultEditorSelector?.elementIds?.length) {
|
||||
return defaultEditorSelector.elementIds[0];
|
||||
} else if (defaultEditorSelector?.blockIds?.length) {
|
||||
return defaultEditorSelector.blockIds[0];
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
const slots: BlocksuiteEditorContainerRef['slots'] = useMemo(() => {
|
||||
return {
|
||||
@@ -180,40 +170,24 @@ export const BlocksuiteEditorContainer = forwardRef<
|
||||
}, [affineEditorContainerProxy, ref]);
|
||||
|
||||
useEffect(() => {
|
||||
if (anchor) {
|
||||
let canceled = false;
|
||||
affineEditorContainerProxy.updateComplete
|
||||
.then(() => {
|
||||
if (!canceled) {
|
||||
const std = affineEditorContainerProxy.host?.std;
|
||||
if (std) {
|
||||
scrollAnchoring(std, mode, anchor);
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(console.error);
|
||||
return () => {
|
||||
canceled = true;
|
||||
};
|
||||
} else {
|
||||
// if no anchor, focus the title
|
||||
let canceled = false;
|
||||
let canceled = false;
|
||||
let unsubscribe: () => void = () => {};
|
||||
|
||||
affineEditorContainerProxy.updateComplete
|
||||
.then(() => {
|
||||
if (!canceled) {
|
||||
const title = docTitleRef.current?.querySelector<
|
||||
HTMLElement & { inlineEditor: InlineEditor }
|
||||
>('rich-text');
|
||||
title?.inlineEditor.focusEnd();
|
||||
}
|
||||
})
|
||||
.catch(console.error);
|
||||
return () => {
|
||||
canceled = true;
|
||||
};
|
||||
}
|
||||
}, [anchor, affineEditorContainerProxy, mode]);
|
||||
affineEditorContainerProxy.updateComplete
|
||||
.then(() => {
|
||||
if (!canceled) {
|
||||
unsubscribe = editorService.editor.bindEditorContainer(
|
||||
affineEditorContainerProxy,
|
||||
docTitleRef.current as DocTitle
|
||||
);
|
||||
}
|
||||
})
|
||||
.catch(console.error);
|
||||
return () => {
|
||||
canceled = true;
|
||||
unsubscribe();
|
||||
};
|
||||
}, [affineEditorContainerProxy, mode, editorService]);
|
||||
|
||||
const handleClickPageModeBlank = useCallback(() => {
|
||||
affineEditorContainerProxy.host?.std.command.exec(
|
||||
|
||||
+1
-12
@@ -1,5 +1,4 @@
|
||||
import { EditorLoading } from '@affine/component/page-detail-skeleton';
|
||||
import type { EditorSelector } from '@affine/core/modules/editor';
|
||||
import type { DocMode } from '@blocksuite/blocks';
|
||||
import { assertExists } from '@blocksuite/global/utils';
|
||||
import type { AffineEditorContainer } from '@blocksuite/presets';
|
||||
@@ -30,7 +29,6 @@ export type EditorProps = {
|
||||
onLoadEditor?: (editor: AffineEditorContainer) => () => void;
|
||||
style?: CSSProperties;
|
||||
className?: string;
|
||||
defaultEditorSelector?: EditorSelector;
|
||||
};
|
||||
|
||||
function usePageRoot(page: Doc) {
|
||||
@@ -57,15 +55,7 @@ function usePageRoot(page: Doc) {
|
||||
|
||||
const BlockSuiteEditorImpl = forwardRef<AffineEditorContainer, EditorProps>(
|
||||
function BlockSuiteEditorImpl(
|
||||
{
|
||||
mode,
|
||||
page,
|
||||
className,
|
||||
onLoadEditor,
|
||||
shared,
|
||||
style,
|
||||
defaultEditorSelector,
|
||||
},
|
||||
{ mode, page, className, onLoadEditor, shared, style },
|
||||
ref
|
||||
) {
|
||||
usePageRoot(page);
|
||||
@@ -115,7 +105,6 @@ const BlockSuiteEditorImpl = forwardRef<AffineEditorContainer, EditorProps>(
|
||||
ref={onRefChange}
|
||||
className={className}
|
||||
style={style}
|
||||
defaultEditorSelector={defaultEditorSelector}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -77,20 +77,16 @@ const usePatchSpecs = (page: Doc, shared: boolean, mode: DocMode) => {
|
||||
const pageId = data.pageId;
|
||||
if (!pageId) return <span />;
|
||||
|
||||
const isSameDoc = pageId === page.id;
|
||||
|
||||
return (
|
||||
<AffinePageReference
|
||||
docCollection={page.collection}
|
||||
pageId={pageId}
|
||||
mode={mode}
|
||||
params={data.params}
|
||||
std={reference.std}
|
||||
isSameDoc={isSameDoc}
|
||||
/>
|
||||
);
|
||||
};
|
||||
}, [mode, page.collection, page.id]);
|
||||
}, [mode, page.collection]);
|
||||
|
||||
const specs = useMemo(() => {
|
||||
return mode === 'edgeless'
|
||||
|
||||
@@ -40,6 +40,7 @@ export const EditorModeSwitch = () => {
|
||||
const togglePage = useCallback(() => {
|
||||
if (currentMode === 'page' || isSharedMode || trash) return;
|
||||
editor.setMode('page');
|
||||
editor.setSelector(undefined);
|
||||
editor.doc.setPrimaryMode('page');
|
||||
toast(t['com.affine.toastMessage.pageMode']());
|
||||
track.$.header.actions.switchPageMode({ mode: 'page' });
|
||||
@@ -48,6 +49,7 @@ export const EditorModeSwitch = () => {
|
||||
const toggleEdgeless = useCallback(() => {
|
||||
if (currentMode === 'edgeless' || isSharedMode || trash) return;
|
||||
editor.setMode('edgeless');
|
||||
editor.setSelector(undefined);
|
||||
editor.doc.setPrimaryMode('edgeless');
|
||||
toast(t['com.affine.toastMessage.edgelessMode']());
|
||||
track.$.header.actions.switchPageMode({ mode: 'edgeless' });
|
||||
|
||||
@@ -11,7 +11,7 @@ import clsx from 'clsx';
|
||||
import type { CSSProperties } from 'react';
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
|
||||
import { type EditorSelector, EditorService } from '../modules/editor';
|
||||
import { EditorService } from '../modules/editor';
|
||||
import {
|
||||
EditorSettingService,
|
||||
fontStyleOptions,
|
||||
@@ -35,13 +35,11 @@ export interface PageDetailEditorProps {
|
||||
docCollection: DocCollection;
|
||||
pageId: string;
|
||||
onLoad?: OnLoadEditor;
|
||||
defaultEditorSelector?: EditorSelector;
|
||||
}
|
||||
|
||||
const PageDetailEditorMain = memo(function PageDetailEditorMain({
|
||||
page,
|
||||
onLoad,
|
||||
defaultEditorSelector,
|
||||
}: PageDetailEditorProps & { page: BlockSuiteDoc }) {
|
||||
const editor = useService(EditorService).editor;
|
||||
const mode = useLiveData(editor.mode$);
|
||||
@@ -108,7 +106,6 @@ const PageDetailEditorMain = memo(function PageDetailEditorMain({
|
||||
mode={mode}
|
||||
page={page}
|
||||
shared={isSharedMode}
|
||||
defaultEditorSelector={defaultEditorSelector}
|
||||
onLoadEditor={onLoadEditor}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -1,29 +1,32 @@
|
||||
import type { DocMode } from '@blocksuite/blocks';
|
||||
import type { AffineEditorContainer } from '@blocksuite/presets';
|
||||
import type { InlineEditor } from '@blocksuite/inline/inline-editor';
|
||||
import type { AffineEditorContainer, DocTitle } from '@blocksuite/presets';
|
||||
import type { DocService, WorkspaceService } from '@toeverything/infra';
|
||||
import { Entity, LiveData } from '@toeverything/infra';
|
||||
import { isEqual } from 'lodash-es';
|
||||
|
||||
import type { WorkbenchView } from '../../workbench';
|
||||
import { EditorScope } from '../scopes/editor';
|
||||
import type { EditorSelector } from '../types';
|
||||
import { scrollAnchoring } from '../utils/scroll-anchoring';
|
||||
|
||||
export class Editor extends Entity<{
|
||||
defaultMode: DocMode;
|
||||
defaultEditorSelector?: EditorSelector;
|
||||
}> {
|
||||
export class Editor extends Entity {
|
||||
readonly scope = this.framework.createScope(EditorScope, {
|
||||
editor: this as Editor,
|
||||
});
|
||||
|
||||
readonly mode$ = new LiveData(this.props.defaultMode);
|
||||
readonly selector$ = new LiveData<EditorSelector | undefined>(
|
||||
this.props.defaultEditorSelector
|
||||
);
|
||||
readonly mode$ = new LiveData<DocMode>('page');
|
||||
readonly selector$ = new LiveData<EditorSelector | undefined>(undefined);
|
||||
readonly doc = this.docService.doc;
|
||||
readonly isSharedMode =
|
||||
this.workspaceService.workspace.openOptions.isSharedMode;
|
||||
|
||||
readonly editorContainer$ = new LiveData<AffineEditorContainer | null>(null);
|
||||
|
||||
setSelector(selector: EditorSelector | undefined) {
|
||||
this.selector$.next(selector);
|
||||
}
|
||||
|
||||
toggleMode() {
|
||||
this.mode$.next(this.mode$.value === 'edgeless' ? 'page' : 'edgeless');
|
||||
}
|
||||
@@ -36,6 +39,149 @@ export class Editor extends Entity<{
|
||||
this.editorContainer$.next(editorContainer);
|
||||
}
|
||||
|
||||
/**
|
||||
* sync editor params with view query string
|
||||
*/
|
||||
bindWorkbenchView(view: WorkbenchView) {
|
||||
// eslint-disable-next-line rxjs/finnish
|
||||
const viewParams$ = view.queryString$<{
|
||||
mode?: DocMode;
|
||||
blockIds?: string[];
|
||||
elementIds?: string[];
|
||||
refreshKey?: string;
|
||||
}>({
|
||||
// Cannot handle single id situation correctly: `blockIds=xxx`
|
||||
arrayFormat: 'none',
|
||||
types: {
|
||||
mode: value =>
|
||||
value === 'page' || value === 'edgeless' ? value : undefined,
|
||||
blockIds: value => (value.length ? value.split(',') : []),
|
||||
elementIds: value => (value.length ? value.split(',') : []),
|
||||
refreshKey: 'string',
|
||||
},
|
||||
});
|
||||
|
||||
const stablePrimaryMode = this.doc.getPrimaryMode();
|
||||
|
||||
const editorParams$ = LiveData.computed(get => {
|
||||
const selector = get(this.selector$);
|
||||
return {
|
||||
mode: get(this.mode$),
|
||||
blockIds: selector?.blockIds,
|
||||
elementIds: selector?.elementIds,
|
||||
refreshKey: selector?.refreshKey,
|
||||
};
|
||||
});
|
||||
|
||||
// prevent infinite loop
|
||||
let updating = false;
|
||||
|
||||
const unsubscribeViewParams = viewParams$.subscribe(params => {
|
||||
if (updating) return;
|
||||
updating = true;
|
||||
// when view params changed, sync to editor
|
||||
try {
|
||||
const mode =
|
||||
viewParams$.value.mode || stablePrimaryMode || ('page' as DocMode);
|
||||
if (mode !== editorParams$.value.mode) {
|
||||
this.setMode(mode);
|
||||
}
|
||||
const newSelector = {
|
||||
blockIds: params.blockIds,
|
||||
elementIds: params.elementIds,
|
||||
refreshKey: params.refreshKey,
|
||||
};
|
||||
if (!isEqual(newSelector, editorParams$.value)) {
|
||||
this.setSelector({
|
||||
blockIds: params.blockIds,
|
||||
elementIds: params.elementIds,
|
||||
refreshKey: params.refreshKey,
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
updating = false;
|
||||
}
|
||||
});
|
||||
|
||||
const unsubscribeEditorParams = editorParams$.subscribe(params => {
|
||||
if (updating) return;
|
||||
updating = true;
|
||||
try {
|
||||
// when editor params changed, sync to view
|
||||
const newQueryString: any = {};
|
||||
let updated = false;
|
||||
if (params.mode !== viewParams$.value.mode) {
|
||||
newQueryString.mode = params.mode;
|
||||
updated = true;
|
||||
}
|
||||
const stringBlockIds = params.blockIds?.join(',');
|
||||
const stringElementIds = params.elementIds?.join(',');
|
||||
const stringViewBlockIds = viewParams$.value.blockIds?.join(',');
|
||||
const stringViewElementIds = viewParams$.value.elementIds?.join(',');
|
||||
if (
|
||||
stringBlockIds !== stringViewBlockIds ||
|
||||
stringElementIds !== stringViewElementIds ||
|
||||
params.refreshKey !== viewParams$.value.refreshKey
|
||||
) {
|
||||
newQueryString.blockIds = stringBlockIds;
|
||||
newQueryString.elementIds = stringElementIds;
|
||||
newQueryString.refreshKey = params.refreshKey;
|
||||
updated = true;
|
||||
}
|
||||
|
||||
if (updated) {
|
||||
view.updateQueryString(newQueryString, { replace: true });
|
||||
}
|
||||
} finally {
|
||||
updating = false;
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
unsubscribeEditorParams.unsubscribe();
|
||||
unsubscribeViewParams.unsubscribe();
|
||||
};
|
||||
}
|
||||
|
||||
bindEditorContainer(
|
||||
editorContainer: AffineEditorContainer,
|
||||
docTitle: DocTitle
|
||||
) {
|
||||
const focusAt$ = LiveData.computed(get => {
|
||||
const selector = get(this.selector$);
|
||||
const id =
|
||||
get(this.mode$) === 'edgeless' && selector?.elementIds?.length
|
||||
? selector?.elementIds?.[0]
|
||||
: selector?.blockIds?.[0];
|
||||
if (id) {
|
||||
return { id, refreshKey: selector?.refreshKey };
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
if (focusAt$.value === null) {
|
||||
const title = docTitle.querySelector<
|
||||
HTMLElement & { inlineEditor: InlineEditor }
|
||||
>('rich-text');
|
||||
title?.inlineEditor.focusEnd();
|
||||
}
|
||||
const unsubscribe = focusAt$
|
||||
.distinctUntilChanged(
|
||||
(a, b) => a?.id === b?.id && a?.refreshKey === b?.refreshKey
|
||||
)
|
||||
.subscribe(params => {
|
||||
if (params?.id) {
|
||||
const std = editorContainer.host?.std;
|
||||
if (std) {
|
||||
scrollAnchoring(std, this.mode$.value, params.id);
|
||||
}
|
||||
}
|
||||
});
|
||||
return () => {
|
||||
unsubscribe.unsubscribe();
|
||||
};
|
||||
}
|
||||
|
||||
constructor(
|
||||
private readonly docService: DocService,
|
||||
private readonly workspaceService: WorkspaceService
|
||||
|
||||
@@ -1,14 +1,9 @@
|
||||
import type { DocMode } from '@blocksuite/blocks';
|
||||
import { Service } from '@toeverything/infra';
|
||||
|
||||
import { Editor } from '../entities/editor';
|
||||
import type { EditorSelector } from '../types';
|
||||
|
||||
export class EditorsService extends Service {
|
||||
createEditor(defaultMode: DocMode, defaultEditorSelector?: EditorSelector) {
|
||||
return this.framework.createEntity(Editor, {
|
||||
defaultMode,
|
||||
defaultEditorSelector,
|
||||
});
|
||||
createEditor() {
|
||||
return this.framework.createEntity(Editor);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export type EditorSelector = {
|
||||
blockIds?: string[];
|
||||
elementIds?: string[];
|
||||
refreshKey?: string;
|
||||
};
|
||||
|
||||
+1
@@ -39,6 +39,7 @@ function scrollAnchoringInEdgelessMode(
|
||||
h / service.viewport.zoom
|
||||
);
|
||||
} else {
|
||||
if (!service.getElementById) return;
|
||||
const model = service.getElementById(id);
|
||||
if (!model) return;
|
||||
|
||||
@@ -5,16 +5,17 @@ import { AffineErrorBoundary } from '@affine/core/components/affine/affine-error
|
||||
import { BlockSuiteEditor } from '@affine/core/components/blocksuite/block-suite-editor';
|
||||
import { EditorOutlineViewer } from '@affine/core/components/blocksuite/outline-viewer';
|
||||
import { useNavigateHelper } from '@affine/core/hooks/use-navigate-helper';
|
||||
import { EditorService } from '@affine/core/modules/editor';
|
||||
import { PageNotFound } from '@affine/core/pages/404';
|
||||
import { DebugLogger } from '@affine/debug';
|
||||
import type { DocMode, EdgelessRootService } from '@blocksuite/blocks';
|
||||
import { Bound, DisposableGroup } from '@blocksuite/global/utils';
|
||||
import type { AffineEditorContainer } from '@blocksuite/presets';
|
||||
import {
|
||||
DocsService,
|
||||
FrameworkScope,
|
||||
useLiveData,
|
||||
useService,
|
||||
useServices,
|
||||
} from '@toeverything/infra';
|
||||
import clsx from 'clsx';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
@@ -62,27 +63,21 @@ function fitViewport(
|
||||
}
|
||||
}
|
||||
|
||||
export function DocPeekPreview({
|
||||
docId,
|
||||
blockIds,
|
||||
elementIds,
|
||||
mode,
|
||||
function DocPeekPreviewEditor({
|
||||
xywh,
|
||||
}: {
|
||||
docId: string;
|
||||
blockIds?: string[];
|
||||
elementIds?: string[];
|
||||
mode?: DocMode;
|
||||
xywh?: `[${number},${number},${number},${number}]`;
|
||||
}) {
|
||||
const { doc, workspace, editor, loading } = useEditor(docId, mode, {
|
||||
blockIds,
|
||||
elementIds,
|
||||
const { editorService } = useServices({
|
||||
EditorService,
|
||||
});
|
||||
const editor = editorService.editor;
|
||||
const doc = editor.doc;
|
||||
const workspace = editor.doc.workspace;
|
||||
const mode = useLiveData(editor.mode$);
|
||||
const { jumpToTag } = useNavigateHelper();
|
||||
const workbench = useService(WorkbenchService).workbench;
|
||||
const peekView = useService(PeekViewService).peekView;
|
||||
const defaultEditorSelector = useLiveData(editor?.selector$);
|
||||
const [editorElement, setEditorElement] =
|
||||
useState<AffineEditorContainer | null>(null);
|
||||
|
||||
@@ -90,27 +85,16 @@ export function DocPeekPreview({
|
||||
setEditorElement(editor);
|
||||
};
|
||||
|
||||
const docs = useService(DocsService);
|
||||
const [resolvedMode, setResolvedMode] = useState<DocMode | undefined>(mode);
|
||||
|
||||
useEffect(() => {
|
||||
editorElement?.updateComplete
|
||||
.then(() => {
|
||||
if (resolvedMode === 'edgeless') {
|
||||
if (mode === 'edgeless') {
|
||||
fitViewport(editorElement, xywh);
|
||||
}
|
||||
})
|
||||
.catch(console.error);
|
||||
return;
|
||||
}, [editorElement, resolvedMode, xywh]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!mode || !resolvedMode) {
|
||||
setResolvedMode(
|
||||
docs.list.doc$(docId).value?.primaryMode$.value || 'page'
|
||||
);
|
||||
}
|
||||
}, [docId, docs.list, resolvedMode, mode]);
|
||||
}, [editorElement, mode, xywh]);
|
||||
|
||||
useEffect(() => {
|
||||
const disposable = AIProvider.slots.requestOpenWithChat.on(() => {
|
||||
@@ -164,20 +148,11 @@ export function DocPeekPreview({
|
||||
}, [editorElement, jumpToTag, peekView, workspace.id]);
|
||||
|
||||
const openOutlinePanel = useCallback(() => {
|
||||
workbench.openDoc(docId);
|
||||
workbench.openDoc(doc.id);
|
||||
workbench.openSidebar();
|
||||
workbench.activeView$.value.activeSidebarTab('outline');
|
||||
peekView.close();
|
||||
}, [docId, peekView, workbench]);
|
||||
|
||||
// if sync engine has been synced and the page is null, show 404 page.
|
||||
if (!doc || !resolvedMode || !editor) {
|
||||
return loading || !resolvedMode ? (
|
||||
<PageDetailSkeleton key="current-page-is-null" />
|
||||
) : (
|
||||
<PageNotFound noPermission />
|
||||
);
|
||||
}
|
||||
}, [doc, peekView, workbench]);
|
||||
|
||||
return (
|
||||
<AffineErrorBoundary>
|
||||
@@ -185,20 +160,15 @@ export function DocPeekPreview({
|
||||
<Scrollable.Viewport
|
||||
className={clsx('affine-page-viewport', styles.affineDocViewport)}
|
||||
>
|
||||
<FrameworkScope scope={doc.scope}>
|
||||
<FrameworkScope scope={editor.scope}>
|
||||
<BlockSuiteEditor
|
||||
ref={onRef}
|
||||
className={styles.editor}
|
||||
mode={resolvedMode}
|
||||
defaultEditorSelector={defaultEditorSelector}
|
||||
page={doc.blockSuiteDoc}
|
||||
/>
|
||||
</FrameworkScope>
|
||||
</FrameworkScope>
|
||||
<BlockSuiteEditor
|
||||
ref={onRef}
|
||||
className={styles.editor}
|
||||
mode={mode}
|
||||
page={doc.blockSuiteDoc}
|
||||
/>
|
||||
<EditorOutlineViewer
|
||||
editor={editorElement}
|
||||
show={resolvedMode === 'page'}
|
||||
show={mode === 'page'}
|
||||
openOutlinePanel={openOutlinePanel}
|
||||
/>
|
||||
</Scrollable.Viewport>
|
||||
@@ -208,3 +178,39 @@ export function DocPeekPreview({
|
||||
</AffineErrorBoundary>
|
||||
);
|
||||
}
|
||||
|
||||
export function DocPeekPreview({
|
||||
docId,
|
||||
blockIds,
|
||||
elementIds,
|
||||
mode,
|
||||
xywh,
|
||||
}: {
|
||||
docId: string;
|
||||
blockIds?: string[];
|
||||
elementIds?: string[];
|
||||
mode?: DocMode;
|
||||
xywh?: `[${number},${number},${number},${number}]`;
|
||||
}) {
|
||||
const { doc, editor, loading } = useEditor(docId, mode, {
|
||||
blockIds,
|
||||
elementIds,
|
||||
});
|
||||
|
||||
// if sync engine has been synced and the page is null, show 404 page.
|
||||
if (!doc || !editor) {
|
||||
return loading ? (
|
||||
<PageDetailSkeleton key="current-page-is-null" />
|
||||
) : (
|
||||
<PageNotFound noPermission />
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<FrameworkScope scope={doc.scope}>
|
||||
<FrameworkScope scope={editor.scope}>
|
||||
<DocPeekPreviewEditor xywh={xywh} />
|
||||
</FrameworkScope>
|
||||
</FrameworkScope>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -41,12 +41,9 @@ export const useEditor = (
|
||||
if (!doc) {
|
||||
return;
|
||||
}
|
||||
const editor = doc.scope
|
||||
.get(EditorsService)
|
||||
.createEditor(
|
||||
preferModeRef.current || doc.primaryMode$.value,
|
||||
preferSelectorRef.current
|
||||
);
|
||||
const editor = doc.scope.get(EditorsService).createEditor();
|
||||
editor.setMode(preferModeRef.current || doc.primaryMode$.value);
|
||||
editor.setSelector(preferSelectorRef.current);
|
||||
setEditor(editor);
|
||||
return () => {
|
||||
editor.dispose();
|
||||
|
||||
@@ -86,9 +86,9 @@ export class View extends Entity<{
|
||||
parseBooleans: true,
|
||||
}
|
||||
) {
|
||||
return this.location$.map(
|
||||
location => queryString.parse(location.search, options) as Partial<T>
|
||||
);
|
||||
return this.location$
|
||||
.selector(v => v.search)
|
||||
.map(search => queryString.parse(search, options) as Partial<T>);
|
||||
}
|
||||
|
||||
updateQueryString<T extends Record<string, unknown>>(
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export { View as WorkbenchView } from './entities/view';
|
||||
export { Workbench } from './entities/workbench';
|
||||
export { ViewScope } from './scopes/view';
|
||||
export { WorkbenchService } from './services/workbench';
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { Editor } from '@affine/core/modules/editor';
|
||||
import { EditorsService } from '@affine/core/modules/editor';
|
||||
import { ViewService } from '@affine/core/modules/workbench/services/view';
|
||||
import { type DocMode, DocModes } from '@blocksuite/blocks';
|
||||
import type { Doc } from '@toeverything/infra';
|
||||
import {
|
||||
DocsService,
|
||||
@@ -26,80 +25,24 @@ const useLoadDoc = (pageId: string) => {
|
||||
const docRecord = useLiveData(docRecordList.doc$(pageId));
|
||||
const viewService = useService(ViewService);
|
||||
|
||||
const queryString$ = viewService.view.queryString$<{
|
||||
mode?: string;
|
||||
blockIds?: string[];
|
||||
elementIds?: string[];
|
||||
}>({
|
||||
// Cannot handle single id situation correctly: `blockIds=xxx`
|
||||
arrayFormat: 'none',
|
||||
types: {
|
||||
mode: 'string',
|
||||
blockIds: value => (value.length ? value.split(',') : []),
|
||||
elementIds: value => (value.length ? value.split(',') : []),
|
||||
},
|
||||
});
|
||||
|
||||
const modeInQuery = useLiveData(
|
||||
queryString$.map(q => {
|
||||
if (q.mode && DocModes.includes(q.mode as DocMode)) {
|
||||
return q.mode as DocMode;
|
||||
}
|
||||
return null;
|
||||
})
|
||||
);
|
||||
|
||||
// We only read the querystring mode when entering, so use useState here.
|
||||
const [initialQueryStringSelector] = useState(() => {
|
||||
const { blockIds, elementIds } = queryString$.value;
|
||||
return { blockIds, elementIds };
|
||||
});
|
||||
|
||||
const [doc, setDoc] = useState<Doc | null>(null);
|
||||
const [editor, setEditor] = useState<Editor | null>(null);
|
||||
const editorMode = useLiveData(editor?.mode$);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (!docRecord) {
|
||||
return;
|
||||
}
|
||||
const { doc: opened, release } = docsService.open(pageId);
|
||||
setDoc(opened);
|
||||
return () => {
|
||||
release();
|
||||
};
|
||||
}, [docRecord, docsService, pageId]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (!doc) {
|
||||
return;
|
||||
}
|
||||
|
||||
const editor = doc.scope
|
||||
.get(EditorsService)
|
||||
.createEditor(
|
||||
modeInQuery || doc.getPrimaryMode() || ('page' as DocMode),
|
||||
initialQueryStringSelector
|
||||
);
|
||||
const { doc, release } = docsService.open(pageId);
|
||||
setDoc(doc);
|
||||
const editor = doc.scope.get(EditorsService).createEditor();
|
||||
const unbind = editor.bindWorkbenchView(viewService.view);
|
||||
setEditor(editor);
|
||||
return () => {
|
||||
unbind();
|
||||
editor.dispose();
|
||||
release();
|
||||
};
|
||||
}, [doc, modeInQuery, initialQueryStringSelector]);
|
||||
|
||||
// update editor mode to queryString
|
||||
useEffect(() => {
|
||||
if (editorMode) {
|
||||
viewService.view.updateQueryString(
|
||||
{
|
||||
mode: editorMode,
|
||||
},
|
||||
{
|
||||
replace: true,
|
||||
}
|
||||
);
|
||||
}
|
||||
}, [editorMode, viewService.view]);
|
||||
}, [docRecord, docsService, pageId, viewService.view]);
|
||||
|
||||
// set sync engine priority target
|
||||
useEffect(() => {
|
||||
|
||||
@@ -33,7 +33,7 @@ import {
|
||||
WorkspaceService,
|
||||
} from '@toeverything/infra';
|
||||
import clsx from 'clsx';
|
||||
import { memo, useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { memo, useCallback, useEffect, useRef } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import type { Map as YMap } from 'yjs';
|
||||
|
||||
@@ -93,7 +93,6 @@ const DetailPageImpl = memo(function DetailPageImpl() {
|
||||
const isInTrash = useLiveData(doc.meta$.map(meta => meta.trash));
|
||||
const { openPage, jumpToPageBlock, jumpToTag } = useNavigateHelper();
|
||||
const editorContainer = useLiveData(editor.editorContainer$);
|
||||
const [defaultSelector] = useState(() => editor.selector$.value);
|
||||
|
||||
const isSideBarOpen = useLiveData(workbench.sidebarOpen$);
|
||||
const { appSettings } = useAppSettingHelper();
|
||||
@@ -290,7 +289,6 @@ const DetailPageImpl = memo(function DetailPageImpl() {
|
||||
pageId={doc.id}
|
||||
onLoad={onLoad}
|
||||
docCollection={docCollection}
|
||||
defaultEditorSelector={defaultSelector}
|
||||
/>
|
||||
</Scrollable.Viewport>
|
||||
<Scrollable.Scrollbar
|
||||
|
||||
@@ -159,7 +159,8 @@ const SharePageInner = ({
|
||||
|
||||
setPage(doc);
|
||||
|
||||
const editor = doc.scope.get(EditorsService).createEditor(publishMode);
|
||||
const editor = doc.scope.get(EditorsService).createEditor();
|
||||
editor.setMode(publishMode);
|
||||
|
||||
setEditor(editor);
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user