Compare commits

...

3 Commits

Author SHA1 Message Date
Steven Hall 33c30ba7a1 WIP 2025-04-04 14:34:22 +01:00
Nolann B. 139a8050a5 Fix OpenAPI enum display (#3077) 2025-04-02 10:15:19 +02:00
Nolann B. da7b369ffd Fix missing headers in OpenAPIResponses (#3074) 2025-04-01 21:57:36 +02:00
18 changed files with 215 additions and 54 deletions
+6
View File
@@ -0,0 +1,6 @@
---
'@gitbook/react-openapi': patch
'gitbook': patch
---
Fix missing headers in OpenAPIResponses
+6
View File
@@ -0,0 +1,6 @@
---
'@gitbook/react-openapi': patch
'gitbook': patch
---
Fix OpenAPI enum display
+7 -7
View File
@@ -35,7 +35,7 @@
}, },
"packages/colors": { "packages/colors": {
"name": "@gitbook/colors", "name": "@gitbook/colors",
"version": "0.3.0", "version": "0.3.1",
"devDependencies": { "devDependencies": {
"typescript": "^5.5.3", "typescript": "^5.5.3",
}, },
@@ -49,7 +49,7 @@
}, },
"packages/gitbook": { "packages/gitbook": {
"name": "gitbook", "name": "gitbook",
"version": "0.8.2", "version": "0.9.1",
"dependencies": { "dependencies": {
"@gitbook/api": "*", "@gitbook/api": "*",
"@gitbook/cache-do": "workspace:*", "@gitbook/cache-do": "workspace:*",
@@ -136,7 +136,7 @@
}, },
"packages/gitbook-v2": { "packages/gitbook-v2": {
"name": "gitbook-v2", "name": "gitbook-v2",
"version": "0.2.2", "version": "0.2.3",
"dependencies": { "dependencies": {
"@gitbook/api": "*", "@gitbook/api": "*",
"@gitbook/cache-tags": "workspace:*", "@gitbook/cache-tags": "workspace:*",
@@ -162,7 +162,7 @@
"name": "@gitbook/icons", "name": "@gitbook/icons",
"version": "0.2.0", "version": "0.2.0",
"bin": { "bin": {
"gitbook-icons": "./bin/gitbook-icons.js", "gitbook-icons": "./bin/gitbook-icons.js"
}, },
"dependencies": { "dependencies": {
"@fortawesome/fontawesome-free": "^6.6.0", "@fortawesome/fontawesome-free": "^6.6.0",
@@ -180,7 +180,7 @@
}, },
"packages/openapi-parser": { "packages/openapi-parser": {
"name": "@gitbook/openapi-parser", "name": "@gitbook/openapi-parser",
"version": "2.1.1", "version": "2.1.2",
"dependencies": { "dependencies": {
"@scalar/openapi-parser": "^0.10.10", "@scalar/openapi-parser": "^0.10.10",
"@scalar/openapi-types": "^0.1.9", "@scalar/openapi-types": "^0.1.9",
@@ -219,7 +219,7 @@
"name": "@gitbook/react-math", "name": "@gitbook/react-math",
"version": "0.6.0", "version": "0.6.0",
"bin": { "bin": {
"gitbook-math": "./bin/gitbook-math.js", "gitbook-math": "./bin/gitbook-math.js"
}, },
"dependencies": { "dependencies": {
"object-hash": "^3.0.0", "object-hash": "^3.0.0",
@@ -234,7 +234,7 @@
}, },
"packages/react-openapi": { "packages/react-openapi": {
"name": "@gitbook/react-openapi", "name": "@gitbook/react-openapi",
"version": "1.1.6", "version": "1.1.8",
"dependencies": { "dependencies": {
"@gitbook/openapi-parser": "workspace:*", "@gitbook/openapi-parser": "workspace:*",
"@scalar/api-client-react": "^1.2.5", "@scalar/api-client-react": "^1.2.5",
@@ -179,4 +179,9 @@ export interface GitBookDataFetcher {
integrationName: string; integrationName: string;
request: api.RenderIntegrationUI; request: api.RenderIntegrationUI;
}): Promise<DataFetcherResponse<api.ContentKitRenderOutput>>; }): Promise<DataFetcherResponse<api.ContentKitRenderOutput>>;
getAction(params: {
url: string
claims: any
}): Promise<DataFetcherResponse<{ text: string; url: string; icon?: string }>>;
} }
@@ -25,10 +25,21 @@ export async function POST(req: NextRequest) {
); );
} }
const result = await revalidateTags(json.tags); try {
const result = await revalidateTags(json.tags);
return NextResponse.json({
success: true,
stats: result.stats,
});
} catch (err: unknown) {
return NextResponse.json(
{
error: 'Failed to revalidate tags',
message: err instanceof Error ? err.message : 'Internal Server Error',
stack: err instanceof Error ? err.stack : '',
},
{ status: 500 }
);
}
return NextResponse.json({
success: true,
stats: result.stats,
});
} }
@@ -13,6 +13,7 @@ import assertNever from 'assert-never';
import { Annotation } from './Annotation/Annotation'; import { Annotation } from './Annotation/Annotation';
import type { DocumentContextProps } from './DocumentView'; import type { DocumentContextProps } from './DocumentView';
import { Emoji } from './Emoji'; import { Emoji } from './Emoji';
import { InlineAction } from './InlineAction';
import { InlineImage } from './InlineImage'; import { InlineImage } from './InlineImage';
import { InlineLink } from './InlineLink'; import { InlineLink } from './InlineLink';
import { InlineMath } from './Math'; import { InlineMath } from './Math';
@@ -61,6 +62,8 @@ export function Inline<
return <Mention {...contextProps} inline={inline} />; return <Mention {...contextProps} inline={inline} />;
case 'inline-image': case 'inline-image':
return <InlineImage {...contextProps} inline={inline} />; return <InlineImage {...contextProps} inline={inline} />;
case 'action':
return <InlineAction {...contextProps} inline={inline} />;
default: default:
assertNever(inline); assertNever(inline);
} }
@@ -0,0 +1,33 @@
import { getVisitorAuthClaims } from '@/lib/adaptive';
import { getDataOrNull } from '@v2/lib/data';
import { getSiteURLDataFromMiddleware } from '@v2/lib/middleware';
import type { InlineProps } from './Inline';
import { TrackEventButton } from './TrackEventButton';
export async function InlineAction(props: InlineProps<any>) {
const { inline, context } = props;
if (!context.contentContext) {
throw new Error('inline action requires a contentContext');
}
const action = inline.data.action;
if (action.type === 'url') {
return <TrackEventButton action={inline.data.action} resolved={{ url: action.url, text: action.text }} />
}
// API action
const { dataFetcher } = context.contentContext;
const siteData = await getSiteURLDataFromMiddleware();
const claims = getVisitorAuthClaims(siteData)
const resolved = await getDataOrNull(dataFetcher.getAction({ url: inline.data.action.url, claims }));
if (!resolved) {
throw new Error('inline action not found');
}
return <TrackEventButton action={inline.data.action} resolved={resolved} />
}
@@ -265,15 +265,11 @@
/* Schema Enum */ /* Schema Enum */
.openapi-schema-enum { .openapi-schema-enum {
@apply flex flex-row text-sm leading-relaxed gap-2 flex-wrap text-tint; @apply text-sm leading-relaxed max-w-full text-tint;
}
.openapi-schema-enum-list {
@apply flex flex-row gap-1.5 items-center;
} }
.openapi-schema-enum-value { .openapi-schema-enum-value {
@apply text-sm; @apply text-sm mr-1.5;
} }
.openapi-schema-enum-value:first-child { .openapi-schema-enum-value:first-child {
@@ -0,0 +1,36 @@
'use client';
import { tcls } from '@/lib/tailwind';
import { useTrackEvent } from '../Insights';
import { LoadingButton } from '../primitives';
export function TrackEventButton(props: {
action: any;
resolved: { url: string; text: string };
}) {
const { action, resolved } = props;
const trackEvent = useTrackEvent();
return (
<LoadingButton
onClick={async () => {
trackEvent({ type: 'action_click', action: action });
window.open(resolved.url, '_blank', 'noopener noreferrer');
}}
href={resolved.url}
variant="primary"
size="medium"
className={tcls(
'theme-bold:bg-header-link theme-bold:text-header-background theme-bold:shadow-none theme-bold:hover:bg-header-link theme-bold:hover:text-header-background theme-bold:hover:shadow-none'
)}
// insights={{
// type: 'link_click',
// link: {
// target: linkTarget,
// position: SiteInsightsLinkPosition.Header,
// },
// }}
>
{resolved.text}
</LoadingButton>
);
}
@@ -93,6 +93,7 @@ export function InsightsProvider(props: InsightsProviderProps) {
const flushEventsSync = useEventCallback(() => { const flushEventsSync = useEventCallback(() => {
const session = getSession(); const session = getSession();
const visitorId = visitorIdRef.current; const visitorId = visitorIdRef.current;
console.log('flushEventsSync');
if (!visitorId) { if (!visitorId) {
throw new Error('Visitor ID should be set before flushing events'); throw new Error('Visitor ID should be set before flushing events');
} }
@@ -126,6 +127,8 @@ export function InsightsProvider(props: InsightsProviderProps) {
}; };
} }
console.log('allEvents', allEvents.length)
if (allEvents.length > 0) { if (allEvents.length > 0) {
if (enabled) { if (enabled) {
sendEvents({ sendEvents({
@@ -143,6 +146,7 @@ export function InsightsProvider(props: InsightsProviderProps) {
const visitorId = visitorIdRef.current ?? (await getVisitorId(appURL)); const visitorId = visitorIdRef.current ?? (await getVisitorId(appURL));
visitorIdRef.current = visitorId; visitorIdRef.current = visitorId;
console.log('flushBatchedEvents');
flushEventsSync(); flushEventsSync();
}, 1500); }, 1500);
@@ -167,6 +171,8 @@ export function InsightsProvider(props: InsightsProviderProps) {
context, context,
}; };
console.log('trackEvent', event, eventsRef.current[pathname].pageContext);
if (eventsRef.current[pathname].pageContext !== undefined) { if (eventsRef.current[pathname].pageContext !== undefined) {
// If the pageId is set, we know that the page_view event has been tracked // If the pageId is set, we know that the page_view event has been tracked
// and we can flush the events // and we can flush the events
@@ -6,7 +6,7 @@ import { type ClassValue, tcls } from '@/lib/tailwind';
import { Link, type LinkInsightsProps } from './Link'; import { Link, type LinkInsightsProps } from './Link';
type ButtonProps = { export type ButtonProps = {
href?: string; href?: string;
variant?: 'primary' | 'secondary'; variant?: 'primary' | 'secondary';
size?: 'default' | 'medium' | 'small'; size?: 'default' | 'medium' | 'small';
@@ -0,0 +1,16 @@
'use client';
import { tcls } from '@/lib/tailwind';
import { useTransition } from 'react';
import { Button, type ButtonProps } from './Button';
export function LoadingButton({ href, onClick, className, ...rest }: ButtonProps & { onClick: () => Promise<void> }) {
const [isPending, startTransition] = useTransition();
const handleClick = (e) => {
e.preventDefault();
startTransition(onClick);
return false;
}
return <Button className={tcls(className, isPending ? 'animate-pulse' : '')} {...rest} onClick={handleClick} />
}
@@ -8,3 +8,4 @@ export * from './StyledLink';
export * from './DateRelative'; export * from './DateRelative';
export * from './Emoji'; export * from './Emoji';
export * from './LoadingPane'; export * from './LoadingPane';
export * from './LoadingButton';
+1
View File
@@ -6,6 +6,7 @@ import { headers } from 'next/headers';
export async function shouldTrackEvents(): Promise<boolean> { export async function shouldTrackEvents(): Promise<boolean> {
const headersList = await headers(); const headersList = await headers();
return true;
if ( if (
process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'development' ||
(process.env.GITBOOK_BLOCK_PAGE_VIEWS_TRACKING && (process.env.GITBOOK_BLOCK_PAGE_VIEWS_TRACKING &&
+16
View File
@@ -270,6 +270,22 @@ async function getDataFetcherV1(): Promise<GitBookDataFetcher> {
return result; return result;
}); });
}, },
getAction(params) {
return wrapDataFetcherError(async () => {
console.log('fetching', params.url);
const url = new URL(params.url);
Object.keys(params.claims).forEach((key) => {
url.searchParams.append(key, params.claims[key]);
});
const result = await fetch(url.toString());
console.log('fetched', result.statusText)
if (!result) {
throw new DataFetcherError('Action not found', 404);
}
return await result.json();
});
}
}; };
return dataFetcher; return dataFetcher;
@@ -36,13 +36,15 @@ export function OpenAPIResponse(props: {
/> />
</OpenAPIDisclosure> </OpenAPIDisclosure>
) : null} ) : null}
<div className="openapi-responsebody"> {mediaType.schema && (
<OpenAPISchemaProperties <div className="openapi-responsebody">
id={`response-${context.blockKey}`} <OpenAPISchemaProperties
properties={mediaType.schema ? [{ schema: mediaType.schema }] : []} id={`response-${context.blockKey}`}
context={context} properties={[{ schema: mediaType.schema }]}
/> context={context}
</div> />
</div>
)}
</div> </div>
); );
} }
+37 -12
View File
@@ -21,7 +21,42 @@ export function OpenAPIResponses(props: {
icon={context.icons.chevronRight} icon={context.icons.chevronRight}
groups={Object.entries(responses).map( groups={Object.entries(responses).map(
([statusCode, response]: [string, OpenAPIV3.ResponseObject]) => { ([statusCode, response]: [string, OpenAPIV3.ResponseObject]) => {
const content = Object.entries(response.content ?? {}); const tabs = (() => {
// If there is no content, but there are headers, we need to show the headers
if (
(!response.content || !Object.keys(response.content).length) &&
response.headers &&
Object.keys(response.headers).length
) {
return [
{
id: 'default',
body: (
<OpenAPIResponse
response={response}
mediaType={{}}
context={context}
/>
),
},
];
}
return Object.entries(response.content ?? {}).map(
([contentType, mediaType]) => ({
id: contentType,
label: contentType,
body: (
<OpenAPIResponse
response={response}
mediaType={mediaType}
context={context}
/>
),
})
);
})();
const description = response.description; const description = response.description;
return { return {
@@ -39,17 +74,7 @@ export function OpenAPIResponses(props: {
) : null} ) : null}
</div> </div>
), ),
tabs: content.map(([contentType, mediaType]) => ({ tabs,
id: contentType,
label: contentType,
body: (
<OpenAPIResponse
response={response}
mediaType={mediaType}
context={context}
/>
),
})),
}; };
} }
)} )}
+14 -16
View File
@@ -275,22 +275,20 @@ function OpenAPISchemaEnum(props: {
} }
return ( return (
<div className="openapi-schema-enum"> <span className="openapi-schema-enum">
<span>Available options:</span> Available options:{' '}
<div className="openapi-schema-enum-list"> {enumValues.map((item, index) => (
{enumValues.map((item, index) => ( <span key={index} className="openapi-schema-enum-value">
<span key={index} className="openapi-schema-enum-value"> <OpenAPICopyButton
<OpenAPICopyButton value={item.value}
value={item.value} label={item.description}
label={item.description} withTooltip={!!item.description}
withTooltip={!!item.description} >
> <code>{`${item.value}`}</code>
<code>{`${item.value}`}</code> </OpenAPICopyButton>
</OpenAPICopyButton> </span>
</span> ))}
))} </span>
</div>
</div>
); );
} }