mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-09-16 15:45:13 +00:00
Allow quickly viewing which pages have changed in a CR/revision (#4348)
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Show changed pages in preview toolbars for change requests and revisions.
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { GitBookSiteContext } from '@/lib/context';
|
||||
import { AdminToolbarClient } from './AdminToolbarClient';
|
||||
import { getToolbarChangedPages } from './changedPages';
|
||||
import type { AdminToolbarContext } from './types';
|
||||
|
||||
export interface AdminToolbarProps {
|
||||
@@ -11,6 +12,7 @@ export interface AdminToolbarProps {
|
||||
*/
|
||||
export async function AdminToolbar(props: AdminToolbarProps) {
|
||||
const { context } = props;
|
||||
const changedPages = await getToolbarChangedPages(context);
|
||||
|
||||
// Create a minimal context to avoid serializing and passing too many data to the client
|
||||
const minimalContext: AdminToolbarContext = {
|
||||
@@ -57,6 +59,7 @@ export async function AdminToolbar(props: AdminToolbarProps) {
|
||||
published: context.site.urls.published,
|
||||
},
|
||||
},
|
||||
changedPages,
|
||||
};
|
||||
|
||||
return <AdminToolbarClient context={minimalContext} />;
|
||||
|
||||
@@ -4,6 +4,7 @@ import { MotionConfig, motion } from 'motion/react';
|
||||
import { useCheckForContentUpdate } from '../AutoRefreshContent';
|
||||
import { useVisitor } from '../Insights';
|
||||
import { useCurrentPagePath } from '../hooks';
|
||||
import { ChangedPagesButton } from './ChangedPagesButton';
|
||||
import { HideToolbarButton } from './HideToolbarButton';
|
||||
import { IframeWrapper } from './IframeWrapper';
|
||||
import { RefreshContentButton } from './RefreshContentButton';
|
||||
@@ -150,6 +151,8 @@ function ChangeRequestToolbar(props: ToolbarViewProps) {
|
||||
<ToolbarActions>
|
||||
{/* Refresh to retrieve latest changes */}
|
||||
{updated ? <RefreshContentButton refreshForUpdates={refreshForUpdates} /> : null}
|
||||
{/* View a popover with quick links to the changed pages */}
|
||||
<ChangedPagesButton changedPages={context.changedPages} />
|
||||
|
||||
{/* Edit in GitBook */}
|
||||
<EditPageButton href={changeRequest.urls.app} siteId={site.id} />
|
||||
@@ -211,6 +214,9 @@ function RevisionToolbar(props: ToolbarViewProps) {
|
||||
<ToolbarSubtitle subtitle={<ToolbarDate value={revision.createdAt} />} />
|
||||
</ToolbarBody>
|
||||
<ToolbarActions>
|
||||
{/* View a popover with quick links to the changed pages */}
|
||||
<ChangedPagesButton changedPages={context.changedPages} />
|
||||
|
||||
{/* Open commit in Git client */}
|
||||
<ToolbarButton
|
||||
title={
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
'use client';
|
||||
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
import { Icon, type IconName, IconStyle } from '@gitbook/icons';
|
||||
import { motion, useReducedMotion } from 'motion/react';
|
||||
import type { SVGProps } from 'react';
|
||||
import { DropdownMenu, DropdownMenuItem, DropdownMenuSeparator } from '../primitives';
|
||||
import type { ToolbarButtonProps } from './Toolbar';
|
||||
import type { MinimalChangedPage, MinimalChangedPages } from './types';
|
||||
|
||||
const STATUS_ICON_STYLES: Record<
|
||||
MinimalChangedPage['status'],
|
||||
{ chip: string; icon: IconName; iconClassName: string; iconStyle?: IconStyle }
|
||||
> = {
|
||||
created: {
|
||||
chip: 'border-green-200 bg-green-50',
|
||||
icon: 'plus',
|
||||
iconClassName: 'size-3.5 text-green-600',
|
||||
},
|
||||
edited: {
|
||||
chip: 'border-blue-200 bg-blue-50',
|
||||
icon: 'pencil',
|
||||
iconClassName: 'size-3 text-blue-600',
|
||||
iconStyle: IconStyle.Regular,
|
||||
},
|
||||
moved: {
|
||||
chip: 'border-blue-200 bg-blue-50',
|
||||
icon: 'arrow-right',
|
||||
iconClassName: 'size-3 text-blue-600',
|
||||
},
|
||||
deleted: {
|
||||
chip: 'border-red-200 bg-red-50',
|
||||
icon: 'minus',
|
||||
iconClassName: 'size-3 text-red-600',
|
||||
},
|
||||
};
|
||||
|
||||
const STATUS_ICON_CHIP_CLASS = 'flex size-5 shrink-0 items-center justify-center rounded-md border';
|
||||
|
||||
/**
|
||||
* A button that opens a popover with quick links to the _changed_ pages.
|
||||
*/
|
||||
export function ChangedPagesButton(props: {
|
||||
changedPages: MinimalChangedPages | null;
|
||||
motionValues?: ToolbarButtonProps['motionValues'];
|
||||
}) {
|
||||
const { changedPages, motionValues } = props;
|
||||
const reduceMotion = useReducedMotion();
|
||||
|
||||
if (!changedPages?.pages.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const changedPagesCount = changedPages.pages.length + changedPages.more;
|
||||
const changedPagesLabel = `${changedPagesCount} changed ${
|
||||
changedPagesCount === 1 ? 'page' : 'pages'
|
||||
}`;
|
||||
|
||||
const trigger = (
|
||||
<button
|
||||
type="button"
|
||||
aria-label="View changed pages"
|
||||
className="relative size-8 cursor-pointer border-0 bg-transparent p-0"
|
||||
>
|
||||
<motion.span
|
||||
aria-hidden="true"
|
||||
style={
|
||||
reduceMotion
|
||||
? undefined
|
||||
: {
|
||||
scale: motionValues?.scale,
|
||||
x: motionValues?.x,
|
||||
transformOrigin: 'bottom center',
|
||||
zIndex: motionValues?.scale ? 10 : 'auto',
|
||||
}
|
||||
}
|
||||
transition={{
|
||||
type: 'spring',
|
||||
stiffness: 400,
|
||||
damping: 30,
|
||||
}}
|
||||
className={tcls(
|
||||
'toolbar-button',
|
||||
'relative flex size-8 items-center justify-center gap-1 truncate rounded-full text-sm transition-colors',
|
||||
'text-tint-7 hover:text-tint-1',
|
||||
'dark:text-tint-12',
|
||||
'bg-[var(--toolbar-bg)]',
|
||||
'hover:bg-[color-mix(in_srgb,var(--toolbar-bg)_90%,white)]'
|
||||
)}
|
||||
>
|
||||
<DiffIcon className="size-4 shrink-0" />
|
||||
</motion.span>
|
||||
</button>
|
||||
);
|
||||
|
||||
return (
|
||||
<DropdownMenu
|
||||
button={trigger}
|
||||
buttonTooltip="View changed pages"
|
||||
side="top"
|
||||
align="end"
|
||||
sideOffset={12}
|
||||
className="w-80 max-w-[calc(100vw-2rem)] gap-0 overflow-hidden p-0"
|
||||
>
|
||||
<div className="px-3 py-2 font-medium text-sm text-tint">{changedPagesLabel}</div>
|
||||
<DropdownMenuSeparator className="m-0" />
|
||||
<div className="flex max-h-80 flex-col overflow-y-auto">
|
||||
{changedPages.pages.map((page) => (
|
||||
<ChangedPageMenuItem key={page.id} page={page} />
|
||||
))}
|
||||
</div>
|
||||
{changedPages.more > 0 ? (
|
||||
<>
|
||||
<DropdownMenuSeparator />
|
||||
<div className="px-3 py-1 text-tint-subtle text-xs">
|
||||
{changedPages.more} more changes not shown
|
||||
</div>
|
||||
</>
|
||||
) : null}
|
||||
</DropdownMenu>
|
||||
);
|
||||
}
|
||||
|
||||
function ChangedPageMenuItem(props: { page: MinimalChangedPage }) {
|
||||
const { page } = props;
|
||||
const actionLabel = page.action === 'editor' ? 'Open in editor' : 'Open';
|
||||
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
href={page.href}
|
||||
className="group/changed-page relative w-full min-w-0 items-center rounded-none px-3 py-1.5"
|
||||
>
|
||||
<span className="flex min-w-0 flex-1 items-center gap-2">
|
||||
<ChangedPageStatusIcon status={page.status} />
|
||||
<span className="min-w-0 flex-1 transition-[padding] group-hover/changed-page:pr-24 group-focus-visible/changed-page:pr-24 group-data-[highlighted]/changed-page:pr-24">
|
||||
<span className="block truncate font-medium text-sm leading-4">
|
||||
{page.title}
|
||||
</span>
|
||||
<span className="block truncate text-tint-subtle text-xs leading-4">
|
||||
/{page.path || ''}
|
||||
</span>
|
||||
</span>
|
||||
<span className="pointer-events-none absolute right-2 flex items-center gap-0.5 bg-tint-hover pl-3 text-tint-subtle text-xs opacity-0 transition-opacity group-hover/changed-page:opacity-100 group-focus-visible/changed-page:opacity-100 group-data-[highlighted]/changed-page:opacity-100">
|
||||
{actionLabel}
|
||||
<Icon icon="chevron-right" className="size-3 shrink-0" />
|
||||
</span>
|
||||
</span>
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
}
|
||||
|
||||
function ChangedPageStatusIcon(props: { status: MinimalChangedPage['status'] }) {
|
||||
const { status } = props;
|
||||
const style = STATUS_ICON_STYLES[status];
|
||||
|
||||
return (
|
||||
<span className={tcls(STATUS_ICON_CHIP_CLASS, style.chip)}>
|
||||
<Icon icon={style.icon} iconStyle={style.iconStyle} className={style.iconClassName} />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function DiffIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg fill="none" viewBox="0 0 16 16" aria-hidden="true" {...props}>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M8 2.4a.6.6 0 0 1 .6.6v2.9h2.9a.6.6 0 1 1 0 1.2H8.6V10a.6.6 0 1 1-1.2 0V7.1H4.5a.6.6 0 0 1 0-1.2h2.9V3a.6.6 0 0 1 .6-.6M4.5 12.4a.6.6 0 1 0 0 1.2h7a.6.6 0 1 0 0-1.2z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import type { GitBookSiteContext } from '@/lib/context';
|
||||
import { getDataOrNull, ignoreAllThrownError } from '@/lib/data';
|
||||
import { getToolbarChangedPagesFromChanges } from './changedPagesMapper';
|
||||
import type { MinimalChangedPages } from './types';
|
||||
|
||||
export const TOOLBAR_CHANGED_PAGES_LIMIT = 100;
|
||||
|
||||
/**
|
||||
* Fetch and reduce semantic changes to the minimal list needed by the toolbar client.
|
||||
*/
|
||||
export async function getToolbarChangedPages(
|
||||
context: GitBookSiteContext
|
||||
): Promise<MinimalChangedPages | null> {
|
||||
// The changed-pages list is a non-critical toolbar enhancement. Any failure to fetch or
|
||||
// reduce the changes should simply hide the button, never break rendering of the site page.
|
||||
return ignoreAllThrownError(fetchToolbarChangedPages(context));
|
||||
}
|
||||
|
||||
async function fetchToolbarChangedPages(
|
||||
context: GitBookSiteContext
|
||||
): Promise<MinimalChangedPages | null> {
|
||||
const changes = context.changeRequest
|
||||
? await getDataOrNull(
|
||||
context.dataFetcher.getChangeRequestChanges({
|
||||
spaceId: context.space.id,
|
||||
changeRequestId: context.changeRequest.id,
|
||||
limit: TOOLBAR_CHANGED_PAGES_LIMIT,
|
||||
})
|
||||
)
|
||||
: context.revisionId !== context.space.revision
|
||||
? await getDataOrNull(
|
||||
context.dataFetcher.getRevisionSemanticChanges({
|
||||
spaceId: context.space.id,
|
||||
revisionId: context.revisionId,
|
||||
limit: TOOLBAR_CHANGED_PAGES_LIMIT,
|
||||
})
|
||||
)
|
||||
: null;
|
||||
|
||||
if (!changes) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const pages = getToolbarChangedPagesFromChanges({
|
||||
changes: changes.changes,
|
||||
editorBaseURL: context.changeRequest?.urls.app ?? context.revision.urls.app,
|
||||
linker: context.linker,
|
||||
pages: context.revision.pages,
|
||||
});
|
||||
|
||||
return pages.length > 0
|
||||
? {
|
||||
pages,
|
||||
more: changes.more ?? 0,
|
||||
}
|
||||
: null;
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
import type { GitBookLinker } from '@/lib/links';
|
||||
import { getPagePath, resolvePageId } from '@/lib/pages';
|
||||
import { joinPathWithBaseURL } from '@/lib/paths';
|
||||
import {
|
||||
type ChangedRevisionPage,
|
||||
type Revision,
|
||||
RevisionPageType,
|
||||
type RevisionSemanticChange,
|
||||
} from '@gitbook/api';
|
||||
import type { MinimalChangedPage } from './types';
|
||||
|
||||
// When one page has multiple semantic changes, show the label that best summarizes its final state.
|
||||
const CHANGE_STATUS_SUMMARY_ORDER: Array<MinimalChangedPage['status']> = [
|
||||
'edited',
|
||||
'moved',
|
||||
'created',
|
||||
'deleted',
|
||||
];
|
||||
|
||||
export function getToolbarChangedPagesFromChanges(input: {
|
||||
changes: RevisionSemanticChange[];
|
||||
linker: Pick<GitBookLinker, 'toPathForPage'>;
|
||||
pages: Revision['pages'];
|
||||
editorBaseURL: string;
|
||||
}): MinimalChangedPage[] {
|
||||
const changedPages = new Map<string, MinimalChangedPage>();
|
||||
|
||||
for (const change of input.changes) {
|
||||
// Only real document page changes can become toolbar rows; computed/link/group pages are ignored.
|
||||
const pageChange = getChangedPageChange(change);
|
||||
if (!pageChange || pageChange.page.type !== RevisionPageType.Document) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Resolve against the current revision so preview links use the current title and path.
|
||||
const resolved = resolvePageId(input.pages, pageChange.page.id);
|
||||
const isDeleted = pageChange.status === 'deleted';
|
||||
const currentPage = resolved?.page;
|
||||
const path = currentPage ? getPagePath(input.pages, currentPage) : pageChange.page.path;
|
||||
|
||||
if (!path && !currentPage) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Missing non-deleted pages cannot be linked reliably from the preview.
|
||||
if (!currentPage && !isDeleted) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Deleted pages may no longer exist in the preview, so link to their editor change.
|
||||
if (!currentPage) {
|
||||
changedPages.set(pageChange.page.id, {
|
||||
id: pageChange.page.id,
|
||||
title: pageChange.page.title,
|
||||
path: path ?? '',
|
||||
href: joinPathWithBaseURL(input.editorBaseURL, path ?? ''),
|
||||
status: pageChange.status,
|
||||
action: 'editor',
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
const pageId = currentPage.id;
|
||||
const existing = changedPages.get(pageId);
|
||||
// The API can return multiple changes for one page; keep the clearest one-row summary.
|
||||
if (existing && shouldKeepExistingPageChange(existing.status, pageChange.status)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
changedPages.set(pageId, {
|
||||
id: pageId,
|
||||
title: currentPage.title,
|
||||
path: path ?? '',
|
||||
href: isDeleted
|
||||
? joinPathWithBaseURL(input.editorBaseURL, path ?? '')
|
||||
: input.linker.toPathForPage({
|
||||
pages: input.pages,
|
||||
page: currentPage,
|
||||
}),
|
||||
status: pageChange.status,
|
||||
action: isDeleted ? 'editor' : 'preview',
|
||||
});
|
||||
}
|
||||
|
||||
return Array.from(changedPages.values());
|
||||
}
|
||||
|
||||
function getChangedPageChange(
|
||||
change: RevisionSemanticChange
|
||||
): { page: ChangedRevisionPage; status: MinimalChangedPage['status'] } | null {
|
||||
switch (change.type) {
|
||||
case 'page_created':
|
||||
return { page: change.page, status: 'created' };
|
||||
case 'page_edited':
|
||||
return { page: change.page, status: 'edited' };
|
||||
case 'page_moved':
|
||||
return { page: change.page, status: 'moved' };
|
||||
case 'page_deleted':
|
||||
return { page: change.page, status: 'deleted' };
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function shouldKeepExistingPageChange(
|
||||
existing: MinimalChangedPage['status'],
|
||||
next: MinimalChangedPage['status']
|
||||
) {
|
||||
return getChangeStatusSummaryIndex(existing) >= getChangeStatusSummaryIndex(next);
|
||||
}
|
||||
|
||||
function getChangeStatusSummaryIndex(status: MinimalChangedPage['status']) {
|
||||
return CHANGE_STATUS_SUMMARY_ORDER.indexOf(status);
|
||||
}
|
||||
@@ -43,6 +43,20 @@ export type MinimalSite = {
|
||||
};
|
||||
};
|
||||
|
||||
export type MinimalChangedPage = {
|
||||
id: string;
|
||||
title: string;
|
||||
path: string;
|
||||
href: string;
|
||||
status: 'created' | 'edited' | 'moved' | 'deleted';
|
||||
action: 'preview' | 'editor';
|
||||
};
|
||||
|
||||
export type MinimalChangedPages = {
|
||||
pages: MinimalChangedPage[];
|
||||
more: number;
|
||||
};
|
||||
|
||||
export type AdminToolbarContext = {
|
||||
organizationId: string;
|
||||
revisionId: string;
|
||||
@@ -50,6 +64,7 @@ export type AdminToolbarContext = {
|
||||
changeRequest: MinimalChangeRequest | null;
|
||||
revision: MinimalRevision;
|
||||
site: MinimalSite;
|
||||
changedPages: MinimalChangedPages | null;
|
||||
};
|
||||
|
||||
export interface AdminToolbarClientProps {
|
||||
|
||||
@@ -11,6 +11,7 @@ import * as RadixDropdownMenu from '@radix-ui/react-dropdown-menu';
|
||||
import { assert } from 'ts-essentials';
|
||||
import { Link, type LinkInsightsProps } from '.';
|
||||
import { ToggleChevron } from './ToggleChevron';
|
||||
import { Tooltip } from './Tooltip';
|
||||
|
||||
export type DropdownButtonProps<E extends HTMLElement = HTMLElement> = Omit<
|
||||
Partial<DetailedHTMLProps<HTMLAttributes<E>, E>>,
|
||||
@@ -36,6 +37,8 @@ const DROPDOWN_CONTENT_INNER_CLASS =
|
||||
export function DropdownMenu(props: {
|
||||
/** Content of the button */
|
||||
button: React.ReactNode;
|
||||
/** Tooltip label for the button */
|
||||
buttonTooltip?: React.ReactNode;
|
||||
/** Content of the dropdown */
|
||||
children: React.ReactNode;
|
||||
/** Custom styles */
|
||||
@@ -52,32 +55,53 @@ export function DropdownMenu(props: {
|
||||
* @default "start"
|
||||
*/
|
||||
align?: RadixDropdownMenu.DropdownMenuContentProps['align'];
|
||||
/**
|
||||
* Distance between the trigger and the dropdown.
|
||||
* @default 0
|
||||
*/
|
||||
sideOffset?: RadixDropdownMenu.DropdownMenuContentProps['sideOffset'];
|
||||
}) {
|
||||
const {
|
||||
button,
|
||||
buttonTooltip,
|
||||
children,
|
||||
className,
|
||||
openOnHover = false,
|
||||
side = 'bottom',
|
||||
align = 'start',
|
||||
sideOffset = 0,
|
||||
} = props;
|
||||
const [hovered, setHovered] = useState(false);
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const isOpen = openOnHover ? open || hovered : open;
|
||||
|
||||
const trigger = (
|
||||
<RadixDropdownMenu.Trigger
|
||||
asChild
|
||||
onMouseEnter={() => setHovered(true)}
|
||||
onMouseLeave={() => setHovered(false)}
|
||||
onClick={() => (openOnHover ? setOpen(!open) : null)}
|
||||
className="group/dropdown"
|
||||
>
|
||||
{button}
|
||||
</RadixDropdownMenu.Trigger>
|
||||
);
|
||||
|
||||
return (
|
||||
<DropdownMenuContext.Provider value={{ open: isOpen, setOpen }}>
|
||||
<RadixDropdownMenu.Root modal={false} open={isOpen} onOpenChange={setOpen}>
|
||||
<RadixDropdownMenu.Trigger
|
||||
asChild
|
||||
onMouseEnter={() => setHovered(true)}
|
||||
onMouseLeave={() => setHovered(false)}
|
||||
onClick={() => (openOnHover ? setOpen(!open) : null)}
|
||||
className="group/dropdown"
|
||||
>
|
||||
{button}
|
||||
</RadixDropdownMenu.Trigger>
|
||||
{buttonTooltip ? (
|
||||
<Tooltip
|
||||
label={buttonTooltip}
|
||||
pinOnClick={false}
|
||||
rootProps={{ disableHoverableContent: true }}
|
||||
>
|
||||
{trigger}
|
||||
</Tooltip>
|
||||
) : (
|
||||
trigger
|
||||
)}
|
||||
|
||||
<RadixDropdownMenu.Portal>
|
||||
<RadixDropdownMenu.Content
|
||||
@@ -88,6 +112,7 @@ export function DropdownMenu(props: {
|
||||
onMouseLeave={() => setHovered(false)}
|
||||
align={align}
|
||||
side={side}
|
||||
sideOffset={sideOffset}
|
||||
className={DROPDOWN_CONTENT_OUTER_CLASS}
|
||||
>
|
||||
<div className={tcls(DROPDOWN_CONTENT_INNER_CLASS, className)}>
|
||||
|
||||
@@ -20,6 +20,7 @@ export function Tooltip(props: {
|
||||
arrowProps?: RadixTooltip.TooltipArrowProps;
|
||||
arrow?: boolean;
|
||||
className?: string;
|
||||
pinOnClick?: boolean;
|
||||
}) {
|
||||
const {
|
||||
children,
|
||||
@@ -31,6 +32,7 @@ export function Tooltip(props: {
|
||||
arrowProps,
|
||||
arrow = false,
|
||||
className,
|
||||
pinOnClick = true,
|
||||
} = props;
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
@@ -56,7 +58,11 @@ export function Tooltip(props: {
|
||||
|
||||
return (
|
||||
<RadixTooltip.Root open={open || clicked} onOpenChange={setOpen} {...rootProps}>
|
||||
<RadixTooltip.Trigger asChild onClick={() => setClicked(true)} {...triggerProps}>
|
||||
<RadixTooltip.Trigger
|
||||
asChild
|
||||
onClick={pinOnClick ? () => setClicked(true) : undefined}
|
||||
{...triggerProps}
|
||||
>
|
||||
{children}
|
||||
</RadixTooltip.Trigger>
|
||||
<RadixTooltip.Portal {...portalProps}>
|
||||
|
||||
@@ -139,6 +139,20 @@ export function createDataFetcher(
|
||||
changeRequestId: params.changeRequestId,
|
||||
});
|
||||
},
|
||||
getChangeRequestChanges(params) {
|
||||
return getChangeRequestChanges(input, {
|
||||
spaceId: params.spaceId,
|
||||
changeRequestId: params.changeRequestId,
|
||||
limit: params.limit,
|
||||
});
|
||||
},
|
||||
getRevisionSemanticChanges(params) {
|
||||
return getRevisionSemanticChanges(input, {
|
||||
spaceId: params.spaceId,
|
||||
revisionId: params.revisionId,
|
||||
limit: params.limit,
|
||||
});
|
||||
},
|
||||
getDocument(params) {
|
||||
return getDocument(input, {
|
||||
spaceId: params.spaceId,
|
||||
@@ -323,6 +337,76 @@ const getRevision = cache(
|
||||
}
|
||||
);
|
||||
|
||||
const getChangeRequestChanges = cache(
|
||||
async (
|
||||
input: DataFetcherInput,
|
||||
params: { spaceId: string; changeRequestId: string; limit?: number }
|
||||
) => {
|
||||
'use cache: remote';
|
||||
cacheTag(
|
||||
getCacheTag({
|
||||
tag: 'change-request',
|
||||
space: params.spaceId,
|
||||
changeRequest: params.changeRequestId,
|
||||
})
|
||||
);
|
||||
|
||||
return wrapDataFetcherError(async () => {
|
||||
return trace(
|
||||
`getChangeRequestChanges(${params.spaceId}, ${params.changeRequestId})`,
|
||||
async () => {
|
||||
const api = apiClient(input);
|
||||
const res = await api.spaces.getChangeRequestChanges(
|
||||
params.spaceId,
|
||||
params.changeRequestId,
|
||||
{
|
||||
limit: params.limit,
|
||||
},
|
||||
{
|
||||
...noCacheFetchOptions,
|
||||
}
|
||||
);
|
||||
cacheTag(...getCacheTagsFromResponse(res));
|
||||
cacheLife('minutes');
|
||||
return res.data;
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
const getRevisionSemanticChanges = cache(
|
||||
async (
|
||||
input: DataFetcherInput,
|
||||
params: { spaceId: string; revisionId: string; limit?: number }
|
||||
) => {
|
||||
'use cache: remote';
|
||||
return wrapDataFetcherError(async () => {
|
||||
return trace(
|
||||
`getRevisionSemanticChanges(${params.spaceId}, ${params.revisionId})`,
|
||||
async () => {
|
||||
const api = apiClient(input);
|
||||
const res = await api.spaces.getRevisionSemanticChanges(
|
||||
params.spaceId,
|
||||
params.revisionId,
|
||||
{
|
||||
computed: false,
|
||||
limit: params.limit,
|
||||
metadata: false,
|
||||
},
|
||||
{
|
||||
...noCacheFetchOptions,
|
||||
}
|
||||
);
|
||||
cacheTag(...getCacheTagsFromResponse(res));
|
||||
cacheLife('max');
|
||||
return res.data;
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
const getRevisionPageMarkdown = cache(
|
||||
async (
|
||||
input: DataFetcherInput,
|
||||
|
||||
@@ -65,6 +65,15 @@ export interface GitBookDataFetcher {
|
||||
changeRequestId: string;
|
||||
}): Promise<DataFetcherResponse<api.ChangeRequest>>;
|
||||
|
||||
/**
|
||||
* Get the semantic changes for a change request.
|
||||
*/
|
||||
getChangeRequestChanges(params: {
|
||||
spaceId: string;
|
||||
changeRequestId: string;
|
||||
limit?: number;
|
||||
}): Promise<DataFetcherResponse<api.RevisionSemanticChanges>>;
|
||||
|
||||
/**
|
||||
* Get the revision by its space ID and revision ID.
|
||||
*/
|
||||
@@ -73,6 +82,15 @@ export interface GitBookDataFetcher {
|
||||
revisionId: string;
|
||||
}): Promise<DataFetcherResponse<api.Revision>>;
|
||||
|
||||
/**
|
||||
* Get the semantic changes for a revision.
|
||||
*/
|
||||
getRevisionSemanticChanges(params: {
|
||||
spaceId: string;
|
||||
revisionId: string;
|
||||
limit?: number;
|
||||
}): Promise<DataFetcherResponse<api.RevisionSemanticChanges>>;
|
||||
|
||||
/**
|
||||
* Get a revision page by its path.
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user