mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-09-24 19:32:07 +00:00
Add support for rendering Tags (#4006)
This commit is contained in:
@@ -1,7 +1,9 @@
|
||||
import { formatDateFull, formatDateShort, formatNumericDate } from '@/components/utils/dates';
|
||||
import { getRevisionTags, resolveBlockTags } from '@/lib/tags';
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
import type { DocumentBlockUpdate, DocumentBlockUpdates } from '@gitbook/api';
|
||||
import { assert } from 'ts-essentials';
|
||||
import { Tag } from '../Tag';
|
||||
import type { BlockProps } from './Block';
|
||||
import { Blocks } from './Blocks';
|
||||
|
||||
@@ -35,13 +37,12 @@ export function Update(props: BlockProps<DocumentBlockUpdate>) {
|
||||
short: formatDateShort(parsedDate),
|
||||
}[dateFormat];
|
||||
|
||||
// Resolve tags from the block data using revision-level tag definitions
|
||||
const revisionTags = getRevisionTags(contextProps.context.contentContext?.revision);
|
||||
const resolvedTags = resolveBlockTags(block.data.tags, revisionTags);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={tcls(
|
||||
'relative flex flex-col gap-2 md:flex-row md:gap-4 lg:gap-12 xl:gap-20',
|
||||
style
|
||||
)}
|
||||
>
|
||||
<div className={tcls('relative flex flex-col gap-2 md:flex-row md:gap-4 lg:gap-8', style)}>
|
||||
<div
|
||||
className={tcls(
|
||||
// Date is only sticky on larger screens when we use flex-row layout, with 0px fallback to prevent flicker and flaky tests before JS sets the variable
|
||||
@@ -55,6 +56,13 @@ export function Update(props: BlockProps<DocumentBlockUpdate>) {
|
||||
>
|
||||
{displayDate}
|
||||
</time>
|
||||
{resolvedTags.length > 0 ? (
|
||||
<div className="mt-2 flex flex-wrap gap-1.5">
|
||||
{resolvedTags.map((tag) => (
|
||||
<Tag key={tag.slug} label={tag.label} />
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<Blocks
|
||||
{...contextProps}
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
} from '../PageActions/PageActionsDropdown';
|
||||
import { PageIcon } from '../PageIcon';
|
||||
import { StyledLink } from '../primitives';
|
||||
import { PageTags } from './PageTags';
|
||||
|
||||
export async function PageHeader(props: {
|
||||
context: GitBookSiteContext;
|
||||
@@ -119,6 +120,7 @@ export async function PageHeader(props: {
|
||||
</ol>
|
||||
</nav>
|
||||
)}
|
||||
<PageTags page={page} revision={revision} />
|
||||
{page.layout.title ? (
|
||||
<h1
|
||||
className={tcls(
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import { getRevisionTags, resolveTag } from '@/lib/tags';
|
||||
import type { Revision, RevisionPageDocument } from '@gitbook/api';
|
||||
import { Tag } from '../Tag';
|
||||
|
||||
/**
|
||||
* Render the tags that were added to a page.
|
||||
*/
|
||||
export function PageTags(props: { page: RevisionPageDocument; revision: Revision }) {
|
||||
const { page, revision } = props;
|
||||
|
||||
const pageTags = page.tags; // TODO: simplify once new API lands as `tags` will always be defined.
|
||||
if (!pageTags || pageTags.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const revisionTags = getRevisionTags(revision);
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{pageTags.map((tagRef) => {
|
||||
const resolved = resolveTag(tagRef.tag.tag, revisionTags);
|
||||
if (!resolved) {
|
||||
return null;
|
||||
}
|
||||
return <Tag key={resolved.slug} label={resolved.label} />;
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,12 +1,12 @@
|
||||
'use client';
|
||||
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
import type { ClientTOCPageDocument } from './encodeClientTableOfContents';
|
||||
|
||||
import { SiteInsightsLinkPosition } from '@gitbook/api';
|
||||
import { Tag } from '../Tag';
|
||||
import { PagesList } from './PagesList';
|
||||
import { TOCPageIcon } from './TOCPageIcon';
|
||||
import { ToggleableLinkItem } from './ToggleableLinkItem';
|
||||
import type { ClientTOCPageDocument } from './encodeClientTableOfContents';
|
||||
|
||||
export function PageDocumentItem(props: { page: ClientTOCPageDocument }) {
|
||||
const { page } = props;
|
||||
@@ -40,7 +40,10 @@ export function PageDocumentItem(props: { page: ClientTOCPageDocument }) {
|
||||
}
|
||||
>
|
||||
<TOCPageIcon page={page} />
|
||||
{page.title}
|
||||
<span className="flex items-center gap-2">
|
||||
{page.title}
|
||||
{page.primaryTag ? <Tag label={page.primaryTag.label} /> : null}
|
||||
</span>
|
||||
</ToggleableLinkItem>
|
||||
</li>
|
||||
);
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import type { GitBookSiteContext } from '@/lib/context';
|
||||
import { getPagePaths, hasPageVisibleDescendant } from '@/lib/pages';
|
||||
import { resolveContentRef } from '@/lib/references';
|
||||
import { getRevisionTags, resolveTag } from '@/lib/tags';
|
||||
import { removeUndefined } from '@/lib/typescript';
|
||||
import type { ContentRef, RevisionPage } from '@gitbook/api';
|
||||
import type { ContentRef, RevisionPage, RevisionPageTag, RevisionTag } from '@gitbook/api';
|
||||
import assertNever from 'assert-never';
|
||||
|
||||
export type ClientTOCPageLink = {
|
||||
@@ -24,6 +25,7 @@ export type ClientTOCPageDocument = {
|
||||
icon?: string;
|
||||
pathnames: string[];
|
||||
descendants?: ClientTOCPage[];
|
||||
primaryTag?: RevisionTag;
|
||||
};
|
||||
|
||||
export type ClientTOCPageGroup = {
|
||||
@@ -69,6 +71,8 @@ export async function encodeClientTableOfContents(
|
||||
? await encodeClientTableOfContents(context, rootPages, page.pages)
|
||||
: undefined;
|
||||
|
||||
const primaryTag = resolvePrimaryTag(page.tags, context);
|
||||
|
||||
result.push(
|
||||
removeUndefined({
|
||||
id: page.id,
|
||||
@@ -78,6 +82,7 @@ export async function encodeClientTableOfContents(
|
||||
icon: page.icon,
|
||||
pathnames: getPagePaths(rootPages, page),
|
||||
descendants,
|
||||
primaryTag,
|
||||
type: 'document',
|
||||
})
|
||||
);
|
||||
@@ -122,3 +127,25 @@ export async function encodeClientTableOfContents(
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tags attached to a page can also get a `primary` prop which, when set, is the tag that gets shown in the TOC.
|
||||
* - only 1 tag can be primary for available-space reasons.
|
||||
* */
|
||||
function resolvePrimaryTag(
|
||||
pageTags: RevisionPageTag[] | undefined,
|
||||
context: GitBookSiteContext
|
||||
): RevisionTag | undefined {
|
||||
// TODO: simplify once new API lands as `tags` will always be defined.
|
||||
if (!pageTags || pageTags.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const primary = pageTags.find((t) => t.primary);
|
||||
if (!primary) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const revisionTags = getRevisionTags(context.revision);
|
||||
return resolveTag(primary.tag.tag, revisionTags);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
import type { RevisionTag } from '@gitbook/api';
|
||||
|
||||
/**
|
||||
* Renders a tag, used to tag content like pages and blocks.
|
||||
*/
|
||||
export function Tag(props: { label: RevisionTag['label']; className?: string }) {
|
||||
const { label, className } = props;
|
||||
|
||||
return (
|
||||
<span
|
||||
data-tag=""
|
||||
title={label}
|
||||
className={tcls(
|
||||
'inline-flex items-center rounded-full bg-tint-5 px-2 py-1 font-medium text-tint-strong text-xs leading-normal contrast-more:ring-1 contrast-more:ring-tint',
|
||||
className
|
||||
)}
|
||||
>
|
||||
<span className="truncate">{label}</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import type { ContentRefTag, Revision, RevisionTag } from '@gitbook/api';
|
||||
|
||||
/**
|
||||
* Get the tags defined on a revision.
|
||||
*/
|
||||
export function getRevisionTags(revision: Revision | undefined): RevisionTag[] {
|
||||
// TODO: simplify once new API lands as `tags` will always be defined.
|
||||
return revision?.tags ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a tag slug to its full RevisionTag.
|
||||
*/
|
||||
export function resolveTag(slug: string, revisionTags: RevisionTag[]): RevisionTag | undefined {
|
||||
return revisionTags.find((t) => t.slug === slug);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve an array of block-level tag references to their full RevisionTags.
|
||||
*/
|
||||
export function resolveBlockTags(
|
||||
tags: ContentRefTag[] | undefined,
|
||||
revisionTags: RevisionTag[]
|
||||
): RevisionTag[] {
|
||||
// TODO: simplify once new API lands as `tags` will always be defined.
|
||||
if (!tags || tags.length === 0) {
|
||||
return [];
|
||||
}
|
||||
return tags
|
||||
.map((tag) => resolveTag(tag.tag, revisionTags))
|
||||
.filter((tag): tag is RevisionTag => tag !== undefined);
|
||||
}
|
||||
Reference in New Issue
Block a user