mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-14 21:27:20 +00:00
feat: linked doc supports aliases (#9009)
Closes: [AF-1882](https://linear.app/affine-design/issue/AF-1882/实现-generatedocurlservice) Upstreams: https://github.com/toeverything/blocksuite/pull/8806
This commit is contained in:
@@ -30,6 +30,7 @@ import * as styles from './styles.css';
|
||||
interface AffinePageReferenceProps {
|
||||
pageId: string;
|
||||
params?: URLSearchParams;
|
||||
title?: string | null; // title alias
|
||||
className?: string;
|
||||
Icon?: ComponentType;
|
||||
onClick?: (e: MouseEvent) => void;
|
||||
@@ -38,20 +39,21 @@ interface AffinePageReferenceProps {
|
||||
function AffinePageReferenceInner({
|
||||
pageId,
|
||||
params,
|
||||
title,
|
||||
Icon: UserIcon,
|
||||
}: AffinePageReferenceProps) {
|
||||
const docDisplayMetaService = useService(DocDisplayMetaService);
|
||||
const docsService = useService(DocsService);
|
||||
const i18n = useI18n();
|
||||
|
||||
let linkWithMode: DocMode | null = null;
|
||||
let linkToNode = false;
|
||||
let referenceWithMode: DocMode | null = null;
|
||||
let referenceToNode = false;
|
||||
if (params) {
|
||||
const m = params.get('mode');
|
||||
if (m && (m === 'page' || m === 'edgeless')) {
|
||||
linkWithMode = m as DocMode;
|
||||
referenceWithMode = m as DocMode;
|
||||
}
|
||||
linkToNode = params.has('blockIds') || params.has('elementIds');
|
||||
referenceToNode = params.has('blockIds') || params.has('elementIds');
|
||||
}
|
||||
|
||||
const Icon = useLiveData(
|
||||
@@ -61,25 +63,33 @@ function AffinePageReferenceInner({
|
||||
}
|
||||
return get(
|
||||
docDisplayMetaService.icon$(pageId, {
|
||||
mode: linkWithMode ?? undefined,
|
||||
mode: referenceWithMode ?? undefined,
|
||||
reference: true,
|
||||
referenceToNode: linkToNode,
|
||||
referenceToNode,
|
||||
hasTitleAlias: Boolean(title),
|
||||
})
|
||||
);
|
||||
})
|
||||
);
|
||||
|
||||
const notFound = !useLiveData(docsService.list.doc$(pageId));
|
||||
|
||||
let title = useLiveData(
|
||||
const docTitle = useLiveData(
|
||||
docDisplayMetaService.title$(pageId, { reference: true })
|
||||
);
|
||||
|
||||
title = notFound ? i18n.t('com.affine.notFoundPage.title') : title;
|
||||
if (notFound) {
|
||||
title = i18n.t('com.affine.notFoundPage.title');
|
||||
}
|
||||
|
||||
if (!title) {
|
||||
title = i18n.t(docTitle);
|
||||
}
|
||||
|
||||
return (
|
||||
<span className={notFound ? styles.notFound : ''}>
|
||||
<Icon className={styles.pageReferenceIcon} />
|
||||
<span className="affine-reference-title">{i18n.t(title)}</span>
|
||||
<span className="affine-reference-title">{title}</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -87,6 +97,7 @@ function AffinePageReferenceInner({
|
||||
export function AffinePageReference({
|
||||
pageId,
|
||||
params,
|
||||
title,
|
||||
className,
|
||||
Icon,
|
||||
onClick: userOnClick,
|
||||
@@ -152,7 +163,12 @@ export function AffinePageReference({
|
||||
onClick={onClick}
|
||||
className={clsx(styles.pageReferenceLink, className)}
|
||||
>
|
||||
<AffinePageReferenceInner pageId={pageId} params={params} Icon={Icon} />
|
||||
<AffinePageReferenceInner
|
||||
pageId={pageId}
|
||||
params={params}
|
||||
title={title}
|
||||
Icon={Icon}
|
||||
/>
|
||||
</WorkbenchLink>
|
||||
);
|
||||
}
|
||||
@@ -161,6 +177,7 @@ export function AffineSharedPageReference({
|
||||
pageId,
|
||||
docCollection,
|
||||
params,
|
||||
title,
|
||||
Icon,
|
||||
onClick: userOnClick,
|
||||
}: AffinePageReferenceProps & {
|
||||
@@ -213,7 +230,12 @@ export function AffineSharedPageReference({
|
||||
onClick={onClick}
|
||||
className={styles.pageReferenceLink}
|
||||
>
|
||||
<AffinePageReferenceInner pageId={pageId} params={params} Icon={Icon} />
|
||||
<AffinePageReferenceInner
|
||||
pageId={pageId}
|
||||
params={params}
|
||||
title={title}
|
||||
Icon={Icon}
|
||||
/>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -56,6 +56,7 @@ import {
|
||||
patchEmbedLinkedDocBlockConfig,
|
||||
patchForMobile,
|
||||
patchForSharedPage,
|
||||
patchGenerateDocUrlExtension,
|
||||
patchNotificationService,
|
||||
patchParseDocUrlExtension,
|
||||
patchPeekViewService,
|
||||
@@ -112,6 +113,8 @@ const usePatchSpecs = (shared: boolean, mode: DocMode) => {
|
||||
const pageId = data.pageId;
|
||||
if (!pageId) return <span />;
|
||||
|
||||
// title alias
|
||||
const title = data.title;
|
||||
const params = toURLSearchParams(data.params);
|
||||
|
||||
if (workspaceService.workspace.openOptions.isSharedMode) {
|
||||
@@ -120,11 +123,14 @@ const usePatchSpecs = (shared: boolean, mode: DocMode) => {
|
||||
docCollection={workspaceService.workspace.docCollection}
|
||||
pageId={pageId}
|
||||
params={params}
|
||||
title={title}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <AffinePageReference pageId={pageId} params={params} />;
|
||||
return (
|
||||
<AffinePageReference pageId={pageId} params={params} title={title} />
|
||||
);
|
||||
};
|
||||
}, [workspaceService]);
|
||||
|
||||
@@ -147,6 +153,7 @@ const usePatchSpecs = (shared: boolean, mode: DocMode) => {
|
||||
patched = patched.concat(patchPeekViewService(peekViewService));
|
||||
patched = patched.concat(patchEdgelessClipboard());
|
||||
patched = patched.concat(patchParseDocUrlExtension(framework));
|
||||
patched = patched.concat(patchGenerateDocUrlExtension(framework));
|
||||
patched = patched.concat(patchQuickSearchService(framework));
|
||||
patched = patched.concat(patchEmbedLinkedDocBlockConfig(framework));
|
||||
if (shared) {
|
||||
|
||||
@@ -52,7 +52,7 @@ function createCopyLinkToBlockMenuItem(
|
||||
const options: UseSharingUrl = {
|
||||
workspaceId,
|
||||
pageId,
|
||||
shareMode: mode,
|
||||
mode,
|
||||
blockIds: [model.id],
|
||||
};
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
toReactNode,
|
||||
type useConfirmModal,
|
||||
} from '@affine/component';
|
||||
import { WorkspaceServerService } from '@affine/core/modules/cloud';
|
||||
import type { EditorService } from '@affine/core/modules/editor';
|
||||
import { EditorSettingService } from '@affine/core/modules/editor-setting';
|
||||
import { resolveLinkToDoc } from '@affine/core/modules/navigation';
|
||||
@@ -47,6 +48,7 @@ import {
|
||||
EdgelessRootBlockComponent,
|
||||
EmbedLinkedDocBlockComponent,
|
||||
EmbedLinkedDocBlockConfigExtension,
|
||||
GenerateDocUrlExtension,
|
||||
MobileSpecsPatches,
|
||||
NotificationExtension,
|
||||
ParseDocUrlExtension,
|
||||
@@ -55,6 +57,7 @@ import {
|
||||
ReferenceNodeConfigExtension,
|
||||
} from '@blocksuite/affine/blocks';
|
||||
import { type BlockSnapshot, Text } from '@blocksuite/affine/store';
|
||||
import type { ReferenceParams } from '@blocksuite/affine-model';
|
||||
import {
|
||||
AIChatBlockSchema,
|
||||
type DocProps,
|
||||
@@ -68,6 +71,7 @@ import { customElement } from 'lit/decorators.js';
|
||||
import { literal } from 'lit/static-html.js';
|
||||
import { pick } from 'lodash-es';
|
||||
|
||||
import { generateUrl } from '../../../../hooks/affine/use-share-url';
|
||||
import { createKeyboardToolbarConfig } from './widgets/keyboard-toolbar';
|
||||
|
||||
export type ReferenceReactRenderer = (
|
||||
@@ -110,13 +114,13 @@ export function patchReferenceRenderer(
|
||||
reactToLit: (element: ElementOrFactory) => TemplateResult,
|
||||
reactRenderer: ReferenceReactRenderer
|
||||
): ExtensionType {
|
||||
const litRenderer = (reference: AffineReference) => {
|
||||
const customContent = (reference: AffineReference) => {
|
||||
const node = reactRenderer(reference);
|
||||
return reactToLit(node);
|
||||
};
|
||||
|
||||
return ReferenceNodeConfigExtension({
|
||||
customContent: litRenderer,
|
||||
customContent,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -463,18 +467,32 @@ export function patchParseDocUrlExtension(framework: FrameworkProvider) {
|
||||
const info = resolveLinkToDoc(url);
|
||||
if (!info || info.workspaceId !== workspaceService.workspace.id) return;
|
||||
|
||||
return {
|
||||
docId: info.docId,
|
||||
blockIds: info.blockIds,
|
||||
elementIds: info.elementIds,
|
||||
mode: info.mode,
|
||||
};
|
||||
delete info.refreshKey;
|
||||
|
||||
return info;
|
||||
},
|
||||
});
|
||||
|
||||
return [ParseDocUrl];
|
||||
}
|
||||
|
||||
export function patchGenerateDocUrlExtension(framework: FrameworkProvider) {
|
||||
const workspaceService = framework.get(WorkspaceService);
|
||||
const workspaceServerService = framework.get(WorkspaceServerService);
|
||||
const GenerateDocUrl = GenerateDocUrlExtension({
|
||||
generateDocUrl(pageId: string, params?: ReferenceParams) {
|
||||
return generateUrl({
|
||||
...params,
|
||||
pageId,
|
||||
workspaceId: workspaceService.workspace.id,
|
||||
baseUrl: workspaceServerService.server?.baseUrl ?? location.origin,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
return [GenerateDocUrl];
|
||||
}
|
||||
|
||||
export function patchEdgelessClipboard() {
|
||||
class EdgelessClipboardWatcher extends BlockServiceWatcher {
|
||||
static override readonly flavour = 'affine:page';
|
||||
|
||||
@@ -93,7 +93,7 @@ function createCopyLinkToBlockMenuItem(
|
||||
const mode = editor.mode$.value;
|
||||
const pageId = editor.doc.id;
|
||||
const workspaceId = editor.doc.workspace.id;
|
||||
const options: UseSharingUrl = { workspaceId, pageId, shareMode: mode };
|
||||
const options: UseSharingUrl = { workspaceId, pageId, mode };
|
||||
let type = '';
|
||||
|
||||
if (mode === 'page') {
|
||||
|
||||
@@ -13,7 +13,7 @@ import { useCallback } from 'react';
|
||||
export type UseSharingUrl = {
|
||||
workspaceId: string;
|
||||
pageId: string;
|
||||
shareMode?: DocMode;
|
||||
mode?: DocMode;
|
||||
blockIds?: string[];
|
||||
elementIds?: string[];
|
||||
xywh?: string; // not needed currently
|
||||
@@ -30,7 +30,7 @@ export const generateUrl = ({
|
||||
pageId,
|
||||
blockIds,
|
||||
elementIds,
|
||||
shareMode: mode,
|
||||
mode,
|
||||
xywh, // not needed currently
|
||||
}: UseSharingUrl & { baseUrl: string }) => {
|
||||
try {
|
||||
@@ -38,23 +38,24 @@ export const generateUrl = ({
|
||||
const search = toURLSearchParams({ mode, blockIds, elementIds, xywh });
|
||||
if (search?.size) url.search = search.toString();
|
||||
return url.toString();
|
||||
} catch {
|
||||
return null;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
const getShareLinkType = ({
|
||||
shareMode,
|
||||
mode,
|
||||
blockIds,
|
||||
elementIds,
|
||||
}: {
|
||||
shareMode?: DocMode;
|
||||
mode?: DocMode;
|
||||
blockIds?: string[];
|
||||
elementIds?: string[];
|
||||
}) => {
|
||||
if (shareMode === 'page') {
|
||||
if (mode === 'page') {
|
||||
return 'doc';
|
||||
} else if (shareMode === 'edgeless') {
|
||||
} else if (mode === 'edgeless') {
|
||||
return 'whiteboard';
|
||||
} else if (blockIds && blockIds.length > 0) {
|
||||
return 'block';
|
||||
@@ -131,17 +132,17 @@ export const useSharingUrl = ({ workspaceId, pageId }: UseSharingUrl) => {
|
||||
const serverService = useService(ServerService);
|
||||
|
||||
const onClickCopyLink = useCallback(
|
||||
(shareMode?: DocMode, blockIds?: string[], elementIds?: string[]) => {
|
||||
(mode?: DocMode, blockIds?: string[], elementIds?: string[]) => {
|
||||
const sharingUrl = generateUrl({
|
||||
baseUrl: serverService.server.baseUrl,
|
||||
workspaceId,
|
||||
pageId,
|
||||
blockIds,
|
||||
elementIds,
|
||||
shareMode, // if view is not provided, use the current view
|
||||
mode, // if view is not provided, use the current view
|
||||
});
|
||||
const type = getShareLinkType({
|
||||
shareMode,
|
||||
mode,
|
||||
blockIds,
|
||||
elementIds,
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { extractEmojiIcon } from '@affine/core/utils';
|
||||
import { i18nTime } from '@affine/i18n';
|
||||
import {
|
||||
AliasIcon as LitAliasIcon,
|
||||
BlockLinkIcon as LitBlockLinkIcon,
|
||||
EdgelessIcon as LitEdgelessIcon,
|
||||
LinkedEdgelessIcon as LitLinkedEdgelessIcon,
|
||||
@@ -11,6 +12,7 @@ import {
|
||||
YesterdayIcon as LitYesterdayIcon,
|
||||
} from '@blocksuite/icons/lit';
|
||||
import {
|
||||
AliasIcon,
|
||||
BlockLinkIcon,
|
||||
EdgelessIcon,
|
||||
LinkedEdgelessIcon,
|
||||
@@ -42,6 +44,7 @@ interface DocDisplayIconOptions<T extends IconType> {
|
||||
mode?: 'edgeless' | 'page';
|
||||
reference?: boolean;
|
||||
referenceToNode?: boolean;
|
||||
hasTitleAlias?: boolean;
|
||||
/**
|
||||
* @default true
|
||||
*/
|
||||
@@ -57,6 +60,7 @@ interface DocDisplayTitleOptions {
|
||||
}
|
||||
|
||||
const rcIcons = {
|
||||
AliasIcon,
|
||||
BlockLinkIcon,
|
||||
EdgelessIcon,
|
||||
LinkedEdgelessIcon,
|
||||
@@ -67,6 +71,7 @@ const rcIcons = {
|
||||
YesterdayIcon,
|
||||
};
|
||||
const litIcons = {
|
||||
AliasIcon: LitAliasIcon,
|
||||
BlockLinkIcon: LitBlockLinkIcon,
|
||||
EdgelessIcon: LitEdgelessIcon,
|
||||
LinkedEdgelessIcon: LitLinkedEdgelessIcon,
|
||||
@@ -130,6 +135,12 @@ export class DocDisplayMetaService extends Service {
|
||||
const mode = doc ? get(doc.primaryMode$) : undefined;
|
||||
const finalMode = options?.mode ?? mode ?? 'page';
|
||||
const referenceToNode = !!(options?.reference && options.referenceToNode);
|
||||
const hasTitleAlias = !!(options?.reference && options?.hasTitleAlias);
|
||||
|
||||
// increases block link priority with title alias
|
||||
if (hasTitleAlias) {
|
||||
return iconSet.AliasIcon;
|
||||
}
|
||||
|
||||
// increases block link priority
|
||||
if (referenceToNode) {
|
||||
|
||||
Reference in New Issue
Block a user