mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-09-29 13:39:11 +00:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 078eacce45 | |||
| 9f168ff93b |
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Keep links working when their target page is deleted and re-created at the same path, by falling back to the path recorded on the link.
|
||||
@@ -738,3 +738,131 @@ describe('resolveContentRef for direct space links', () => {
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolveContentRef for a page ref whose page was re-created', () => {
|
||||
function buildPage(id: string, title: string, path: string): RevisionPageDocument {
|
||||
return {
|
||||
object: 'page',
|
||||
id,
|
||||
type: 'document',
|
||||
kind: 'sheet',
|
||||
title,
|
||||
path,
|
||||
slug: path.split('/').pop(),
|
||||
pages: [],
|
||||
tags: [],
|
||||
layout: {},
|
||||
urls: { app: 'https://app.gitbook.com/page' },
|
||||
} as unknown as RevisionPageDocument;
|
||||
}
|
||||
|
||||
function buildRevision(id: string, pages: RevisionPageDocument[]): Revision {
|
||||
return {
|
||||
object: 'revision',
|
||||
id,
|
||||
type: 'edits',
|
||||
pages,
|
||||
files: [],
|
||||
reusableContents: [],
|
||||
tags: [],
|
||||
parents: [],
|
||||
createdAt: '',
|
||||
urls: { app: '' },
|
||||
} as unknown as Revision;
|
||||
}
|
||||
|
||||
function buildSpace(id: string): Space {
|
||||
return {
|
||||
object: 'space',
|
||||
id,
|
||||
title: id,
|
||||
organization: 'org',
|
||||
revision: `rev-${id}`,
|
||||
urls: {
|
||||
location: `https://api.gitbook.com/spaces/${id}`,
|
||||
app: `https://app.gitbook.com/o/org/s/${id}/`,
|
||||
published: `https://${id}.gitbook.io/`,
|
||||
},
|
||||
} as unknown as Space;
|
||||
}
|
||||
|
||||
const recreated = buildPage('page-new', 'Detector', 'alerts-by-name/detector');
|
||||
const other = buildPage('page-other', 'Other', 'alerts-by-name/other');
|
||||
const alertsRevision = buildRevision('rev-alerts', [recreated, other]);
|
||||
const alerts = buildSpace('alerts');
|
||||
const notes = buildSpace('notes');
|
||||
|
||||
const dataFetcher = {
|
||||
getSpace: async ({ spaceId }: { spaceId: string }) =>
|
||||
spaceId === 'alerts'
|
||||
? { data: alerts }
|
||||
: { error: { code: 404, message: 'Not found' } },
|
||||
getRevision: async ({ spaceId }: { spaceId: string }) =>
|
||||
spaceId === 'alerts'
|
||||
? { data: alertsRevision }
|
||||
: { error: { code: 404, message: 'Not found' } },
|
||||
getChangeRequest: async () => ({ error: { code: 404, message: 'Not found' } }),
|
||||
withToken: function () {
|
||||
return this;
|
||||
},
|
||||
} as unknown as GitBookDataFetcher;
|
||||
|
||||
function buildContext(space: Space, revision: Revision): GitBookAnyContext {
|
||||
return {
|
||||
dataFetcher,
|
||||
linker: createLinker({
|
||||
host: 'docs.example.com',
|
||||
spaceBasePath: '/',
|
||||
siteBasePath: '/',
|
||||
}),
|
||||
organizationId: 'org',
|
||||
space,
|
||||
revision,
|
||||
revisionId: revision.id,
|
||||
changeRequest: null,
|
||||
shareKey: undefined,
|
||||
} as unknown as GitBookAnyContext;
|
||||
}
|
||||
|
||||
// Refs only get a typed `path` once @gitbook/api ships it, so the tests add it through a cast.
|
||||
const withPath = (ref: object, path: string) => ({ ...ref, path }) as never;
|
||||
|
||||
it('finds the page at the recorded path when its ID no longer exists', async () => {
|
||||
const result = await resolveContentRef(
|
||||
withPath({ kind: 'page', page: 'page-deleted' }, 'alerts-by-name/detector'),
|
||||
buildContext(alerts, alertsRevision)
|
||||
);
|
||||
|
||||
expect(result?.text).toBe('Detector');
|
||||
});
|
||||
|
||||
it('finds the page at the recorded path in another space', async () => {
|
||||
const result = await resolveContentRef(
|
||||
withPath(
|
||||
{ kind: 'page', space: 'alerts', page: 'page-deleted' },
|
||||
'alerts-by-name/detector'
|
||||
),
|
||||
buildContext(notes, buildRevision('rev-notes', []))
|
||||
);
|
||||
|
||||
expect(result?.text).toBe('Detector');
|
||||
});
|
||||
|
||||
it('keeps resolving by ID while the page still exists', async () => {
|
||||
const result = await resolveContentRef(
|
||||
withPath({ kind: 'page', page: 'page-other' }, 'alerts-by-name/detector'),
|
||||
buildContext(alerts, alertsRevision)
|
||||
);
|
||||
|
||||
expect(result?.text).toBe('Other');
|
||||
});
|
||||
|
||||
it('does not resolve when neither the ID nor the recorded path exists', async () => {
|
||||
const result = await resolveContentRef(
|
||||
withPath({ kind: 'page', page: 'page-deleted' }, 'alerts-by-name/removed'),
|
||||
buildContext(alerts, alertsRevision)
|
||||
);
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,6 +4,7 @@ import type React from 'react';
|
||||
import type {
|
||||
ContentRef,
|
||||
JSONDocument,
|
||||
Revision,
|
||||
RevisionFile,
|
||||
RevisionPageDocument,
|
||||
RevisionReusableContent,
|
||||
@@ -16,7 +17,7 @@ import type { Filesystem } from '@gitbook/openapi-parser';
|
||||
|
||||
import { getGitBookAppHref } from './app';
|
||||
import { getBlockById, getBlockTitle } from './document';
|
||||
import { resolvePageId } from './pages';
|
||||
import { resolvePageId, resolvePagePath } from './pages';
|
||||
import {
|
||||
findSiteSpaceBy,
|
||||
getFallbackSiteSpacePath,
|
||||
@@ -192,7 +193,8 @@ export async function resolveContentRef(
|
||||
? activePage
|
||||
? { page: activePage, ancestors: [] }
|
||||
: undefined
|
||||
: resolvePageId(revision.pages, contentRef.page);
|
||||
: (resolvePageId(revision.pages, contentRef.page) ??
|
||||
resolvePageAtRecordedPath(revision, contentRef));
|
||||
|
||||
const page = resolvePageResult?.page;
|
||||
const ancestors =
|
||||
@@ -430,6 +432,17 @@ export function resolveContentRefFallback(contentRef: ContentRef): ResolvedConte
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the page at the path recorded on a page or anchor ref, for when its page ID no longer
|
||||
* exists (the page was deleted and re-created at the same path).
|
||||
*/
|
||||
function resolvePageAtRecordedPath(revision: Revision, contentRef: ContentRef) {
|
||||
// TODO: read `contentRef.path` directly once @gitbook/api ships it on ContentRefPage and
|
||||
// ContentRefAnchor; until then the field is untyped.
|
||||
const { path } = contentRef as { path?: unknown };
|
||||
return typeof path === 'string' && path ? resolvePagePath(revision.pages, path) : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* This function is used to get the best possible target space while resolving a content ref.
|
||||
* It will try to return the space in the site context if it exists to avoid cross-site links.
|
||||
|
||||
Reference in New Issue
Block a user