feat: allow peek view to be closed by the caller && bump bs (#8542)

depends on https://github.com/toeverything/blocksuite/pull/8558
fix BS-1570
This commit is contained in:
pengx17
2024-10-28 09:16:35 +00:00
parent 3f7752e261
commit 81029db6ce
34 changed files with 530 additions and 385 deletions
@@ -95,6 +95,14 @@ const DatabaseBacklinkRow = ({
}, [row?.cells]);
const t = useI18n();
const pageRefParams = useMemo(() => {
const params = new URLSearchParams();
if (row?.id) {
params.set('blockIds', row.databaseId);
}
return params;
}, [row]);
if (!row || !sortedCells) {
return null;
}
@@ -105,7 +113,11 @@ const DatabaseBacklinkRow = ({
defaultCollapsed={!defaultOpen}
icon={<DatabaseTableViewIcon />}
suffix={
<AffinePageReference className={styles.docRefLink} pageId={row.docId} />
<AffinePageReference
className={styles.docRefLink}
pageId={row.docId}
params={pageRefParams}
/>
}
>
<PropertyCollapsibleContent
@@ -131,8 +143,8 @@ export const DocDatabaseBacklinkInfo = ({
defaultOpen = [],
}: {
defaultOpen?: {
docId: string;
blockId: string;
databaseId: string;
rowId: string;
}[];
}) => {
const doc = useService(DocService).doc;
@@ -151,11 +163,13 @@ export const DocDatabaseBacklinkInfo = ({
return (
<div className={styles.root}>
{rows.map(({ docId, rowId, row$ }) => (
{rows.map(({ docId, databaseBlockId, rowId, row$ }) => (
<Fragment key={`${docId}-${rowId}`}>
<DatabaseBacklinkRow
defaultOpen={defaultOpen?.some(
backlink => backlink.docId === docId && backlink.blockId === rowId
backlink =>
backlink.databaseId === databaseBlockId &&
backlink.rowId === rowId
)}
row$={row$}
/>
@@ -1,3 +1,4 @@
import type { DefaultOpenProperty } from '@affine/core/components/doc-properties';
import type {
DocMode,
EdgelessRootService,
@@ -8,6 +9,7 @@ import type {
DocTitle,
} from '@blocksuite/affine/presets';
import type { InlineEditor } from '@blocksuite/inline';
import { effect } from '@preact/signals-core';
import type { DocService, WorkspaceService } from '@toeverything/infra';
import { Entity, LiveData } from '@toeverything/infra';
import { defaults, isEqual, omit } from 'lodash-es';
@@ -29,6 +31,9 @@ export class Editor extends Entity {
this.workspaceService.workspace.openOptions.isSharedMode;
readonly editorContainer$ = new LiveData<AffineEditorContainer | null>(null);
readonly defaultOpenProperty$ = new LiveData<DefaultOpenProperty | undefined>(
undefined
);
isPresenting$ = new LiveData<boolean>(false);
@@ -39,7 +44,7 @@ export class Editor extends Entity {
) as EdgelessRootService;
if (!edgelessRootService) return;
edgelessRootService.tool.setEdgelessTool({
edgelessRootService.gfx.tool.setTool({
type: !this.isPresenting$.value ? 'frameNavigator' : 'default',
});
}
@@ -60,6 +65,10 @@ export class Editor extends Entity {
this.editorContainer$.next(editorContainer);
}
setDefaultOpenProperty(defaultOpenProperty: DefaultOpenProperty | undefined) {
this.defaultOpenProperty$.next(defaultOpenProperty);
}
/**
* sync editor params with view query string
*/
@@ -102,6 +111,17 @@ export class Editor extends Entity {
if (!isEqual(selector, omit(editorParams, ['mode']))) {
this.setSelector(selector);
}
if (params.databaseId && params.databaseRowId) {
const defaultOpenProperty: DefaultOpenProperty = {
type: 'database',
databaseId: params.databaseId,
databaseRowId: params.databaseRowId,
};
if (!isEqual(defaultOpenProperty, this.defaultOpenProperty$.value)) {
this.setDefaultOpenProperty(defaultOpenProperty);
}
}
} finally {
updating = false;
}
@@ -194,15 +214,15 @@ export class Editor extends Entity {
this.isPresenting$.next(false);
} else {
this.isPresenting$.next(
edgelessPage.edgelessTool.type === 'frameNavigator'
edgelessPage.gfx.tool.currentToolName$.peek() === 'frameNavigator'
);
const disposable = edgelessPage.slots.edgelessToolUpdated.on(() => {
const disposable = effect(() => {
this.isPresenting$.next(
edgelessPage.edgelessTool.type === 'frameNavigator'
edgelessPage.gfx.tool.currentToolName$.value === 'frameNavigator'
);
});
unsubs.push(disposable.dispose.bind(disposable));
unsubs.push(disposable);
}
return () => {
@@ -129,7 +129,14 @@ export const preprocessParams = (
result.elementIds = result.elementIds.filter(v => v.length);
}
return pick(result, ['mode', 'blockIds', 'elementIds', 'refreshKey']);
return pick(result, [
'mode',
'blockIds',
'elementIds',
'databaseId',
'databaseRowId',
'refreshKey',
]);
};
export const paramsParseOptions: ParseOptions = {
@@ -142,6 +149,8 @@ export const paramsParseOptions: ParseOptions = {
value.length ? value.split(',').filter(v => v.length) : [],
elementIds: value =>
value.length ? value.split(',').filter(v => v.length) : [],
databaseId: 'string',
databaseRowId: 'string',
refreshKey: 'string',
},
};
@@ -17,31 +17,43 @@ import { firstValueFrom, map, race } from 'rxjs';
import { resolveLinkToDoc } from '../../navigation';
import type { WorkbenchService } from '../../workbench';
export type PeekViewTarget =
| HTMLElement
| BlockComponent
| AffineReference
| HTMLAnchorElement
| { docId: string; blockIds?: string[] };
export interface DocPeekViewInfo {
type: 'doc';
export type DocReferenceInfo = {
docId: string;
mode?: DocMode;
blockIds?: string[];
elementIds?: string[];
databaseId?: string;
databaseRowId?: string;
/**
* viewport in edgeless mode
*/
xywh?: `[${number},${number},${number},${number}]`;
};
export type PeekViewElement =
| HTMLElement
| BlockComponent
| AffineReference
| HTMLAnchorElement;
export interface PeekViewTarget {
element?: PeekViewElement;
docRef?: DocReferenceInfo;
}
export interface DocPeekViewInfo {
type: 'doc';
docRef: DocReferenceInfo;
}
export type ImagePeekViewInfo = {
type: 'image';
docId: string;
blockIds: [string];
docRef: DocReferenceInfo;
};
export type AIChatBlockPeekViewInfo = {
type: 'ai-chat-block';
docId: string;
docRef: DocReferenceInfo;
host: EditorHost;
model: AIChatBlockModel;
};
@@ -101,83 +113,90 @@ function resolvePeekInfoFromPeekTarget(
};
}
if (peekTarget instanceof AffineReference) {
const referenceInfo = peekTarget.referenceInfo;
if (referenceInfo) {
const { pageId: docId } = referenceInfo;
const info: DocPeekViewInfo = {
type: 'doc',
docId,
};
Object.assign(info, referenceInfo.params);
return info;
}
} else if ('model' in peekTarget) {
const blockModel = peekTarget.model;
if (isEmbedLinkedDocModel(blockModel)) {
const info: DocPeekViewInfo = {
type: 'doc',
docId: blockModel.pageId,
};
Object.assign(info, blockModel.params);
return info;
} else if (isEmbedSyncedDocModel(blockModel)) {
return {
type: 'doc',
docId: blockModel.pageId,
};
} else if (isSurfaceRefModel(blockModel)) {
const refModel = (peekTarget as SurfaceRefBlockComponent).referenceModel;
// refModel can be null if the reference is invalid
if (refModel) {
const docId =
'doc' in refModel ? refModel.doc.id : refModel.surface.doc.id;
const element = peekTarget.element;
if (element) {
if (element instanceof AffineReference) {
const referenceInfo = element.referenceInfo;
if (referenceInfo) {
const { pageId: docId } = referenceInfo;
const info: DocPeekViewInfo = {
type: 'doc',
docRef: {
docId,
},
};
Object.assign(info, referenceInfo.params);
return info;
}
} else if ('model' in element) {
const blockModel = element.model;
if (isEmbedLinkedDocModel(blockModel)) {
const info: DocPeekViewInfo = {
type: 'doc',
docRef: {
docId: blockModel.pageId,
},
};
Object.assign(info, blockModel.params);
return info;
} else if (isEmbedSyncedDocModel(blockModel)) {
return {
type: 'doc',
docId,
mode: 'edgeless',
xywh: refModel.xywh,
docRef: {
docId: blockModel.pageId,
},
};
} else if (isSurfaceRefModel(blockModel)) {
const refModel = (element as SurfaceRefBlockComponent).referenceModel;
// refModel can be null if the reference is invalid
if (refModel) {
const docId =
'doc' in refModel ? refModel.doc.id : refModel.surface.doc.id;
return {
type: 'doc',
docRef: {
docId,
mode: 'edgeless',
xywh: refModel.xywh,
},
};
}
} else if (isImageBlockModel(blockModel)) {
return {
type: 'image',
docRef: {
docId: blockModel.doc.id,
blockIds: [blockModel.id],
},
};
} else if (isAIChatBlockModel(blockModel)) {
return {
type: 'ai-chat-block',
docRef: {
docId: blockModel.doc.id,
blockIds: [blockModel.id],
},
model: blockModel,
host: element.host,
};
}
} else if (isImageBlockModel(blockModel)) {
return {
type: 'image',
docId: blockModel.doc.id,
blockIds: [blockModel.id],
};
} else if (isAIChatBlockModel(blockModel)) {
return {
type: 'ai-chat-block',
docId: blockModel.doc.id,
model: blockModel,
host: peekTarget.host,
};
} else if (element instanceof HTMLAnchorElement) {
const maybeDoc = resolveLinkToDoc(element.href);
if (maybeDoc) {
const info: DocPeekViewInfo = {
type: 'doc',
docRef: maybeDoc,
};
return info;
}
}
} else if (peekTarget instanceof HTMLAnchorElement) {
const maybeDoc = resolveLinkToDoc(peekTarget.href);
if (maybeDoc) {
const info: DocPeekViewInfo = {
type: 'doc',
docId: maybeDoc.docId,
};
}
if (maybeDoc.mode) {
info.mode = maybeDoc.mode;
}
if (maybeDoc.blockIds?.length) {
info.blockIds = maybeDoc.blockIds;
}
if (maybeDoc.elementIds?.length) {
info.elementIds = maybeDoc.elementIds;
}
return info;
}
} else if ('docId' in peekTarget) {
if ('docRef' in peekTarget && peekTarget.docRef) {
return {
type: 'doc',
docId: peekTarget.docId,
blockIds: peekTarget.blockIds,
docRef: peekTarget.docRef,
};
}
return;
@@ -208,7 +227,8 @@ export class PeekViewEntity extends Entity {
// return true if the peek view will be handled
open = async (
target: ActivePeekView['target'],
template?: TemplateResult
template?: TemplateResult,
abortSignal?: AbortSignal
) => {
const resolvedInfo = resolvePeekInfoFromPeekTarget(target, template);
if (!resolvedInfo) {
@@ -220,7 +240,7 @@ export class PeekViewEntity extends Entity {
// if there is an active peek view and it is a doc peek view, we will navigate it first
if (active?.info.type === 'doc' && this.show$.value?.value) {
// TODO(@pengx17): scroll to the viewing position?
this.workbenchService.workbench.openDoc(active.info.docId);
this.workbenchService.workbench.openDoc(active.info.docRef);
}
this._active$.next({ target, info: resolvedInfo });
@@ -231,6 +251,24 @@ export class PeekViewEntity extends Entity {
? 'zoom'
: 'fade',
});
if (abortSignal) {
const abortListener = () => {
if (this.active$.value?.target === target) {
this.close();
}
};
abortSignal.addEventListener('abort', abortListener);
const showSubscription = this.show$.subscribe(v => {
if (!v && !abortSignal.aborted) {
abortSignal.removeEventListener('abort', abortListener);
showSubscription.unsubscribe();
}
});
}
return firstValueFrom(race(this._active$, this.show$).pipe(map(() => {})));
};
@@ -8,7 +8,6 @@ import { PageNotFound } from '@affine/core/desktop/pages/404';
import { EditorService } from '@affine/core/modules/editor';
import { DebugLogger } from '@affine/debug';
import {
type DocMode,
type EdgelessRootService,
RefNodeSlotsProvider,
} from '@blocksuite/affine/blocks';
@@ -24,6 +23,7 @@ import clsx from 'clsx';
import { useCallback, useEffect } from 'react';
import { WorkbenchService } from '../../../workbench';
import type { DocReferenceInfo } from '../../entities/peek-view';
import { PeekViewService } from '../../services/peek-view';
import { useEditor } from '../utils';
import * as styles from './doc-peek-view.css';
@@ -81,6 +81,7 @@ function DocPeekPreviewEditor({
const doc = editor.doc;
const workspace = editor.doc.workspace;
const mode = useLiveData(editor.mode$);
const defaultOpenProperty = useLiveData(editor.defaultOpenProperty$);
const workbench = useService(WorkbenchService).workbench;
const peekView = useService(PeekViewService).peekView;
const editorElement = useLiveData(editor.editorContainer$);
@@ -96,11 +97,11 @@ function DocPeekPreviewEditor({
if (!refNodeSlots) return;
// doc change event inside peek view should be handled by peek view
disposableGroup.add(
// todo(@pengx17): seems not working
refNodeSlots.docLinkClicked.on(options => {
peekView
.open({
type: 'doc',
docId: options.pageId,
docRef: { docId: options.pageId },
...options.params,
})
.catch(console.error);
@@ -158,6 +159,7 @@ function DocPeekPreviewEditor({
mode={mode}
page={doc.blockSuiteDoc}
onEditorReady={handleOnEditorReady}
defaultOpenProperty={defaultOpenProperty}
/>
</Scrollable.Viewport>
<Scrollable.Scrollbar />
@@ -171,23 +173,24 @@ function DocPeekPreviewEditor({
);
}
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,
});
export function DocPeekPreview({ docRef }: { docRef: DocReferenceInfo }) {
const { docId, blockIds, elementIds, mode, xywh, databaseId, databaseRowId } =
docRef;
const { doc, editor, loading } = useEditor(
docId,
mode,
{
blockIds,
elementIds,
},
databaseId && databaseRowId
? {
databaseId,
databaseRowId,
type: 'database',
}
: undefined
);
// if sync engine has been synced and the page is null, show 404 page.
if (!doc || !editor) {
@@ -20,6 +20,7 @@ import {
import { DocInfoService } from '../../doc-info';
import { WorkbenchService } from '../../workbench';
import type { DocReferenceInfo } from '../entities/peek-view';
import { PeekViewService } from '../services/peek-view';
import * as styles from './peek-view-controls.css';
@@ -60,10 +61,8 @@ export const ControlButton = ({
};
type DocPeekViewControlsProps = HTMLAttributes<HTMLDivElement> & {
docId: string;
mode?: DocMode;
blockIds?: string[];
elementIds?: string[];
docRef: DocReferenceInfo;
};
export const DefaultPeekViewControls = ({
@@ -92,10 +91,7 @@ export const DefaultPeekViewControls = ({
};
export const DocPeekViewControls = ({
docId,
mode,
blockIds,
elementIds,
docRef,
className,
...rest
}: DocPeekViewControlsProps) => {
@@ -116,7 +112,7 @@ export const DocPeekViewControls = ({
name: t['com.affine.peek-view-controls.open-doc'](),
nameKey: 'open',
onClick: () => {
workbench.openDoc({ docId, mode, blockIds, elementIds });
workbench.openDoc(docRef);
peekView.close('none');
},
},
@@ -125,10 +121,7 @@ export const DocPeekViewControls = ({
nameKey: 'new-tab',
name: t['com.affine.peek-view-controls.open-doc-in-new-tab'](),
onClick: () => {
workbench.openDoc(
{ docId, mode, blockIds, elementIds },
{ at: 'new-tab' }
);
workbench.openDoc(docRef, { at: 'new-tab' });
peekView.close('none');
},
},
@@ -137,7 +130,7 @@ export const DocPeekViewControls = ({
nameKey: 'split-view',
name: t['com.affine.peek-view-controls.open-doc-in-split-view'](),
onClick: () => {
workbench.openDoc({ docId, mode }, { at: 'beside' });
workbench.openDoc(docRef, { at: 'beside' });
peekView.close('none');
},
},
@@ -146,20 +139,13 @@ export const DocPeekViewControls = ({
nameKey: 'info',
name: t['com.affine.peek-view-controls.open-info'](),
onClick: () => {
docInfoService.modal.open(docId);
docInfoService.modal.open(
typeof docRef === 'string' ? docRef : docRef.docId
);
},
},
].filter((opt): opt is ControlButtonProps => Boolean(opt));
}, [
t,
peekView,
workbench,
docId,
mode,
blockIds,
elementIds,
docInfoService.modal,
]);
}, [t, peekView, workbench, docRef, docInfoService.modal]);
return (
<div {...rest} className={clsx(styles.root, className)}>
{controls.map(option => (
@@ -22,20 +22,15 @@ function renderPeekView({ info }: ActivePeekView) {
return toReactNode(info.template);
}
if (info.type === 'doc') {
return (
<DocPeekPreview
mode={info.mode}
xywh={info.xywh}
docId={info.docId}
blockIds={info.blockIds}
elementIds={info.elementIds}
/>
);
return <DocPeekPreview docRef={info.docRef} />;
}
if (info.type === 'image') {
if (info.type === 'image' && info.docRef.blockIds?.[0]) {
return (
<ImagePreviewPeekView docId={info.docId} blockId={info.blockIds[0]} />
<ImagePreviewPeekView
docId={info.docRef.docId}
blockId={info.docRef.blockIds?.[0]}
/>
);
}
@@ -49,14 +44,7 @@ function renderPeekView({ info }: ActivePeekView) {
const renderControls = ({ info }: ActivePeekView) => {
if (info.type === 'doc') {
return (
<DocPeekViewControls
mode={info.mode}
docId={info.docId}
blockIds={info.blockIds}
elementIds={info.elementIds}
/>
);
return <DocPeekViewControls docRef={info.docRef} />;
}
if (info.type === 'image') {
@@ -86,8 +74,8 @@ const getRendererProps = (
children: preview,
controls,
target:
activePeekView?.target instanceof HTMLElement
? activePeekView.target
activePeekView?.target.element instanceof HTMLElement
? activePeekView.target.element
: undefined,
mode: getMode(activePeekView.info),
dialogFrame: activePeekView.info.type !== 'image',
@@ -108,8 +96,8 @@ export const PeekViewManagerModal = () => {
useEffect(() => {
const subscription = peekViewEntity.show$.subscribe(() => {
if (activePeekView?.target instanceof BlockComponent) {
activePeekView.target.requestUpdate();
if (activePeekView?.target.element instanceof BlockComponent) {
activePeekView.target.element.requestUpdate();
}
});
@@ -1,3 +1,4 @@
import type { DefaultOpenProperty } from '@affine/core/components/doc-properties';
import type { DocMode } from '@blocksuite/affine/blocks';
import type { Doc } from '@toeverything/infra';
import {
@@ -13,7 +14,8 @@ import { type Editor, type EditorSelector, EditorsService } from '../../editor';
export const useEditor = (
pageId: string,
preferMode?: DocMode,
preferSelector?: EditorSelector
preferSelector?: EditorSelector,
defaultOpenProperty?: DefaultOpenProperty
) => {
const currentWorkspace = useService(WorkspaceService).workspace;
const docsService = useService(DocsService);
@@ -22,7 +24,7 @@ export const useEditor = (
const docRecord = docRecordList.doc$(pageId).value;
const preferModeRef = useRef(preferMode);
const preferSelectorRef = useRef(preferSelector);
const defaultOpenPropertyRef = useRef(defaultOpenProperty);
const [doc, setDoc] = useState<Doc | null>(null);
const [editor, setEditor] = useState<Editor | null>(null);
@@ -44,6 +46,7 @@ export const useEditor = (
const editor = doc.scope.get(EditorsService).createEditor();
editor.setMode(preferModeRef.current || doc.primaryMode$.value);
editor.setSelector(preferSelectorRef.current);
editor.setDefaultOpenProperty(defaultOpenPropertyRef.current);
setEditor(editor);
return () => {
editor.dispose();