fix(core): block link recognition in self-hosted (#8340)
Part of [BS-1445](https://linear.app/affine-design/issue/BS-1445/点击-affine-link-时识别应用内链接,以触发应用内跳转) <div class='graphite__hidden'> <div>🎥 Video uploaded on Graphite:</div> <a href="https://app.graphite.dev/media/video/8ypiIKZXudF5a0tIgIzf/fae580bc-7d30-4711-a70e-7a5cf26c76f1.mov"> <img src="https://app.graphite.dev/api/v1/graphite/video/thumbnail/8ypiIKZXudF5a0tIgIzf/fae580bc-7d30-4711-a70e-7a5cf26c76f1.mov"> </a> </div> <video src="https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/8ypiIKZXudF5a0tIgIzf/fae580bc-7d30-4711-a70e-7a5cf26c76f1.mov">Screen Recording 2024-09-21 at 10.50.12.mov</video>
This commit is contained in:
@@ -27,7 +27,7 @@ export const generateUrl = ({
|
|||||||
pageId,
|
pageId,
|
||||||
blockIds,
|
blockIds,
|
||||||
elementIds,
|
elementIds,
|
||||||
shareMode,
|
shareMode: mode,
|
||||||
xywh, // not needed currently
|
xywh, // not needed currently
|
||||||
}: UseSharingUrl) => {
|
}: UseSharingUrl) => {
|
||||||
// Base URL construction
|
// Base URL construction
|
||||||
@@ -36,13 +36,8 @@ export const generateUrl = ({
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const url = new URL(`/workspace/${workspaceId}/${pageId}`, baseUrl);
|
const url = new URL(`/workspace/${workspaceId}/${pageId}`, baseUrl);
|
||||||
const search = toURLSearchParams({
|
const search = toURLSearchParams({ mode, blockIds, elementIds, xywh });
|
||||||
mode: shareMode,
|
if (search?.size) url.search = search.toString();
|
||||||
blockIds,
|
|
||||||
elementIds,
|
|
||||||
xywh,
|
|
||||||
});
|
|
||||||
if (search) url.search = search.toString();
|
|
||||||
return url.toString();
|
return url.toString();
|
||||||
} catch {
|
} catch {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -175,16 +175,21 @@ const DetailPageImpl = memo(function DetailPageImpl() {
|
|||||||
refNodeSlots.docLinkClicked.on(({ pageId, params }) => {
|
refNodeSlots.docLinkClicked.on(({ pageId, params }) => {
|
||||||
if (params) {
|
if (params) {
|
||||||
const { mode, blockIds, elementIds } = params;
|
const { mode, blockIds, elementIds } = params;
|
||||||
return jumpToPageBlock(
|
jumpToPageBlock(
|
||||||
docCollection.id,
|
docCollection.id,
|
||||||
pageId,
|
pageId,
|
||||||
mode,
|
mode,
|
||||||
blockIds,
|
blockIds,
|
||||||
elementIds
|
elementIds
|
||||||
);
|
);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
return openPage(docCollection.id, pageId);
|
if (editor.doc.id === pageId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
openPage(docCollection.id, pageId);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -247,13 +247,12 @@ const SharePageInner = ({
|
|||||||
refNodeSlots.docLinkClicked.on(({ pageId, params }) => {
|
refNodeSlots.docLinkClicked.on(({ pageId, params }) => {
|
||||||
if (params) {
|
if (params) {
|
||||||
const { mode, blockIds, elementIds } = params;
|
const { mode, blockIds, elementIds } = params;
|
||||||
return jumpToPageBlock(
|
jumpToPageBlock(workspaceId, pageId, mode, blockIds, elementIds);
|
||||||
workspaceId,
|
return;
|
||||||
pageId,
|
}
|
||||||
mode,
|
|
||||||
blockIds,
|
if (editor.doc.id === pageId) {
|
||||||
elementIds
|
return;
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return openPage(workspaceId, pageId);
|
return openPage(workspaceId, pageId);
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { toURLSearchParams } from '@affine/core/modules/navigation';
|
import { toURLSearchParams } from '@affine/core/modules/navigation';
|
||||||
|
import type { ReferenceParams } from '@blocksuite/blocks';
|
||||||
import type { WorkspaceService } from '@toeverything/infra';
|
import type { WorkspaceService } from '@toeverything/infra';
|
||||||
import {
|
import {
|
||||||
fromPromise,
|
fromPromise,
|
||||||
@@ -278,17 +279,14 @@ export class DocsSearchService extends Service {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const refs: {
|
const refs: ({ docId: string } & ReferenceParams)[] = nodes.flatMap(
|
||||||
docId: string;
|
node => {
|
||||||
mode?: string;
|
const { ref } = node.fields;
|
||||||
blockIds?: string[];
|
return typeof ref === 'string'
|
||||||
elementIds?: string[];
|
? [JSON.parse(ref)]
|
||||||
}[] = nodes.flatMap(node => {
|
: ref.map(item => JSON.parse(item));
|
||||||
const { ref } = node.fields;
|
}
|
||||||
return typeof ref === 'string'
|
);
|
||||||
? [JSON.parse(ref)]
|
|
||||||
: ref.map(item => JSON.parse(item));
|
|
||||||
});
|
|
||||||
|
|
||||||
const docData = await this.indexer.docIndex.getAll(
|
const docData = await this.indexer.docIndex.getAll(
|
||||||
Array.from(new Set(refs.map(ref => ref.docId)))
|
Array.from(new Set(refs.map(ref => ref.docId)))
|
||||||
@@ -352,17 +350,14 @@ export class DocsSearchService extends Service {
|
|||||||
.pipe(
|
.pipe(
|
||||||
switchMap(({ nodes }) => {
|
switchMap(({ nodes }) => {
|
||||||
return fromPromise(async () => {
|
return fromPromise(async () => {
|
||||||
const refs: {
|
const refs: ({ docId: string } & ReferenceParams)[] = nodes.flatMap(
|
||||||
docId: string;
|
node => {
|
||||||
mode?: string;
|
const { ref } = node.fields;
|
||||||
blockIds?: string[];
|
return typeof ref === 'string'
|
||||||
elementIds?: string[];
|
? [JSON.parse(ref)]
|
||||||
}[] = nodes.flatMap(node => {
|
: ref.map(item => JSON.parse(item));
|
||||||
const { ref } = node.fields;
|
}
|
||||||
return typeof ref === 'string'
|
);
|
||||||
? [JSON.parse(ref)]
|
|
||||||
: ref.map(item => JSON.parse(item));
|
|
||||||
});
|
|
||||||
|
|
||||||
const docData = await this.indexer.docIndex.getAll(
|
const docData = await this.indexer.docIndex.getAll(
|
||||||
Array.from(new Set(refs.map(ref => ref.docId)))
|
Array.from(new Set(refs.map(ref => ref.docId)))
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { afterEach } from 'node:test';
|
import { afterEach } from 'node:test';
|
||||||
|
|
||||||
import { beforeEach, expect, test, vi } from 'vitest';
|
import { beforeEach, describe, expect, test, vi } from 'vitest';
|
||||||
|
|
||||||
import { resolveLinkToDoc, toURLSearchParams } from '../utils';
|
import { resolveLinkToDoc, toURLSearchParams } from '../utils';
|
||||||
|
|
||||||
@@ -84,12 +84,64 @@ const testCases: [string, ReturnType<typeof resolveLinkToDoc>][] = [
|
|||||||
docId: '-Uge-K6SYcAbcNYfQ5U-j',
|
docId: '-Uge-K6SYcAbcNYfQ5U-j',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
[
|
||||||
|
'http//localhost:8000/workspace/48__RTCSwASvWZxyAk3Jw/-Uge-K6SYcAbcNYfQ5U-j?mode=edgeless&elementIds=,yyyy,',
|
||||||
|
{
|
||||||
|
workspaceId: '48__RTCSwASvWZxyAk3Jw',
|
||||||
|
docId: '-Uge-K6SYcAbcNYfQ5U-j',
|
||||||
|
mode: 'edgeless',
|
||||||
|
elementIds: ['yyyy'],
|
||||||
|
},
|
||||||
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const [input, expected] of testCases) {
|
for (const [input, expected] of testCases) {
|
||||||
defineTest(input, expected);
|
defineTest(input, expected);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// self-hosted
|
||||||
|
describe('resolveLinkToDoc in self-hosted', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.unstubAllGlobals();
|
||||||
|
vi.stubGlobal('location', { origin: 'https://local.first' });
|
||||||
|
});
|
||||||
|
|
||||||
|
const testCases: [string, ReturnType<typeof resolveLinkToDoc>][] = [
|
||||||
|
['http://example.com/', null],
|
||||||
|
[
|
||||||
|
'/workspace/48__RTCSwASvWZxyAk3Jw/-Uge-K6SYcAbcNYfQ5U-j?blockIds=xxxx',
|
||||||
|
{
|
||||||
|
workspaceId: '48__RTCSwASvWZxyAk3Jw',
|
||||||
|
docId: '-Uge-K6SYcAbcNYfQ5U-j',
|
||||||
|
blockIds: ['xxxx'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'http://affine.pro/workspace/48__RTCSwASvWZxyAk3Jw/-Uge-K6SYcAbcNYfQ5U-j?blockIds=xxxx',
|
||||||
|
{
|
||||||
|
workspaceId: '48__RTCSwASvWZxyAk3Jw',
|
||||||
|
docId: '-Uge-K6SYcAbcNYfQ5U-j',
|
||||||
|
blockIds: ['xxxx'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'https://local.first/workspace/48__RTCSwASvWZxyAk3Jw/-Uge-K6SYcAbcNYfQ5U-j?blockIds=xxxx',
|
||||||
|
{
|
||||||
|
workspaceId: '48__RTCSwASvWZxyAk3Jw',
|
||||||
|
docId: '-Uge-K6SYcAbcNYfQ5U-j',
|
||||||
|
blockIds: ['xxxx'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const [input, expected] of testCases) {
|
||||||
|
test(`resolveLinkToDoc(${input})InSelfHosted`, () => {
|
||||||
|
const result = resolveLinkToDoc(input);
|
||||||
|
expect(result).toEqual(expected);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
function defineTestWithToURLSearchParams(
|
function defineTestWithToURLSearchParams(
|
||||||
input?: Partial<Record<string, string | string[]>>,
|
input?: Partial<Record<string, string | string[]>>,
|
||||||
expected?: ReturnType<typeof toURLSearchParams>
|
expected?: ReturnType<typeof toURLSearchParams>
|
||||||
|
|||||||
@@ -3,23 +3,27 @@ import { isNil, pick, pickBy } from 'lodash-es';
|
|||||||
import type { ParsedQuery, ParseOptions } from 'query-string';
|
import type { ParsedQuery, ParseOptions } from 'query-string';
|
||||||
import queryString from 'query-string';
|
import queryString from 'query-string';
|
||||||
|
|
||||||
function maybeAffineOrigin(origin: string) {
|
function maybeAffineOrigin(origin: string, baseUrl: string) {
|
||||||
return (
|
return (
|
||||||
origin.startsWith('file://') ||
|
origin.startsWith('file://') ||
|
||||||
origin.startsWith('affine://') ||
|
origin.startsWith('affine://') ||
|
||||||
origin.endsWith('affine.pro') || // stable/beta
|
origin.endsWith('affine.pro') || // stable/beta
|
||||||
origin.endsWith('affine.fail') || // canary
|
origin.endsWith('affine.fail') || // canary
|
||||||
origin.includes('localhost') // dev
|
origin === baseUrl // localhost or self-hosted
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const resolveRouteLinkMeta = (href: string) => {
|
export const resolveRouteLinkMeta = (
|
||||||
|
href: string,
|
||||||
|
baseUrl = location.origin
|
||||||
|
) => {
|
||||||
try {
|
try {
|
||||||
const url = new URL(href, location.origin);
|
const url = new URL(href, baseUrl);
|
||||||
|
|
||||||
// check if origin is one of affine's origins
|
// check if origin is one of affine's origins
|
||||||
|
// check if origin is localhost or self-hosted
|
||||||
|
|
||||||
if (!maybeAffineOrigin(url.origin)) {
|
if (!maybeAffineOrigin(url.origin, baseUrl)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user