Skip to content

Commit 16b3c22

Browse files
authored
feat(docs): implement merge accessed third party endpoints component (#6100)
1 parent 077e403 commit 16b3c22

File tree

5 files changed

+406
-4
lines changed

5 files changed

+406
-4
lines changed

packages/fern-docs/bundle/src/mdx/components/index.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ import { RunnableEndpoint } from "./runnable-endpoint";
4949
import {
5050
EndpointRequestSnippet,
5151
EndpointResponseSnippet,
52+
MergeAccessedThirdPartyEndpointsWidget,
5253
MergeSupportedFieldsByIntegrationWidget,
5354
Schema,
5455
SchemaSnippet
@@ -97,6 +98,7 @@ const FERN_COMPONENTS = {
9798
Icon,
9899
If,
99100
Json,
101+
MergeAccessedThirdPartyEndpointsWidget,
100102
MergeSupportedFieldsByIntegrationWidget,
101103
Mermaid,
102104
ParamField,
Lines changed: 264 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,264 @@
1+
"use client";
2+
3+
import type { HttpMethod } from "@fern-api/docs-utils";
4+
import type * as ApiDefinition from "@fern-api/fdr-sdk/api-definition";
5+
import { SectionContainer } from "@fern-docs/components/api-reference/endpoints/TypeDefinitionAnchor";
6+
import {
7+
TypeDefinitionAnchorPart,
8+
TypeDefinitionRoot
9+
} from "@fern-docs/components/api-reference/type-definitions/TypeDefinitionContext";
10+
import { useCurrentSlug } from "@fern-docs/components/hooks/use-current-pathname";
11+
import { ChevronRight } from "lucide-react";
12+
import { useRef, useState } from "react";
13+
14+
import { TypeDefinitionSlotsServer } from "@/components/api-reference/type-definitions/TypeDefinitionSlotsServer";
15+
import { TypeReferenceDefinitions } from "@/components/api-reference/type-definitions/TypeReferenceDefinitions";
16+
import type { TypeDefinitionWithSerializedDescriptions } from "@/mdx/plugins/serialize-type-definition-descriptions";
17+
18+
const METHOD_COLORS: Record<HttpMethod, string> = {
19+
GET: "text-[#3b82f6]",
20+
POST: "text-[#00b187]",
21+
PUT: "text-[#6366f1]",
22+
PATCH: "text-[#eab308]",
23+
DELETE: "text-[#ea0524]",
24+
HEAD: "text-black dark:text-white",
25+
OPTIONS: "text-black dark:text-white",
26+
CONNECT: "text-black dark:text-white",
27+
TRACE: "text-black dark:text-white"
28+
};
29+
30+
function MethodText({ method }: { method: HttpMethod }) {
31+
return (
32+
<span className={`shrink-0 font-semibold ${METHOD_COLORS[method] ?? "text-[var(--gray-a11)]"}`}>{method}</span>
33+
);
34+
}
35+
36+
interface EndpointModel {
37+
name: string;
38+
fields: string[];
39+
}
40+
41+
interface EndpointData {
42+
method: HttpMethod;
43+
path: string;
44+
apiName: string;
45+
models: EndpointModel[];
46+
}
47+
48+
interface MergeAccessedThirdPartyEndpointsData {
49+
endpoints: EndpointData[];
50+
apiName?: string;
51+
}
52+
53+
type MergeAccessedThirdPartyEndpointsWidgetProps = {
54+
/**
55+
* Base64 gzip-encoded JSON data containing endpoints and models.
56+
* The rehype-schema plugin will decode this to extract the model names
57+
* and inject the corresponding typeDefinitions, types, and decodedData.
58+
*/
59+
data: string;
60+
/**
61+
* @internal injected by rehype-schema plugin - the decoded data from the gzip payload
62+
*/
63+
decodedData?: MergeAccessedThirdPartyEndpointsData;
64+
/**
65+
* @internal injected by rehype-schema plugin based on the models in data
66+
* Maps model names to their type definitions
67+
*/
68+
typeDefinitions?: Record<string, ApiDefinition.TypeDefinition | TypeDefinitionWithSerializedDescriptions>;
69+
/**
70+
* @internal injected by rehype-schema plugin
71+
*/
72+
types?: Record<ApiDefinition.TypeId, ApiDefinition.TypeDefinition>;
73+
lang?: string;
74+
className?: string;
75+
};
76+
77+
function ModelAccordion({
78+
model,
79+
typeDefinition,
80+
types,
81+
lang,
82+
isExpanded,
83+
onToggle,
84+
accordionKey
85+
}: {
86+
model: EndpointModel;
87+
typeDefinition: ApiDefinition.TypeDefinition | TypeDefinitionWithSerializedDescriptions | undefined;
88+
types: Record<ApiDefinition.TypeId, ApiDefinition.TypeDefinition>;
89+
lang: string;
90+
isExpanded: boolean;
91+
onToggle: () => void;
92+
accordionKey: string;
93+
}) {
94+
const accordionRef = useRef<HTMLDivElement>(null);
95+
96+
const handleToggle = () => {
97+
onToggle();
98+
if (!isExpanded) {
99+
// Will be expanding - scroll into view after animation completes
100+
setTimeout(() => {
101+
accordionRef.current?.scrollIntoView({
102+
behavior: "smooth",
103+
block: "nearest"
104+
});
105+
}, 300);
106+
}
107+
};
108+
109+
if (typeDefinition == null) {
110+
return null;
111+
}
112+
113+
// Filter the shape's properties to only show supported fields
114+
let filteredShape = typeDefinition.shape;
115+
if (filteredShape.type === "object") {
116+
const filteredProperties = filteredShape.properties.filter((property) => model.fields.includes(property.key));
117+
filteredShape = {
118+
...filteredShape,
119+
properties: filteredProperties
120+
};
121+
}
122+
123+
const schemaName = typeDefinition.displayName || typeDefinition.name || "schema";
124+
125+
return (
126+
<div
127+
ref={accordionRef}
128+
className={`scroll-mt-4 border-b border-[#e0e0e0] transition-colors duration-200 last:border-b-0 dark:border-gray-700 ${isExpanded ? "bg-[#fbfcfd] dark:bg-gray-800/30" : ""}`}
129+
>
130+
<button
131+
type="button"
132+
onClick={handleToggle}
133+
className="flex w-full cursor-pointer items-center justify-between gap-2 px-3 py-2 text-left transition-colors hover:bg-gray-50 dark:hover:bg-gray-800/50"
134+
>
135+
<span
136+
className={`transition-all duration-200 ${isExpanded ? "text-[20px] font-semibold" : "font-medium"}`}
137+
>
138+
Access {model.name} Information
139+
</span>
140+
<ChevronRight
141+
className={`text-muted h-4 w-4 shrink-0 transition-transform duration-200 ${isExpanded ? "rotate-90" : ""}`}
142+
/>
143+
</button>
144+
<div
145+
className="grid transition-[grid-template-rows] duration-200 ease-in-out"
146+
style={{ gridTemplateRows: isExpanded ? "1fr" : "0fr" }}
147+
>
148+
<div className="overflow-hidden">
149+
<div className="max-h-[600px] overflow-y-auto px-3 py-2">
150+
<div className="text-sm font-bold text-[#8492a6]">{schemaName} fields</div>
151+
<TypeDefinitionAnchorPart part={schemaName}>
152+
<SectionContainer>
153+
<TypeReferenceDefinitions
154+
shape={filteredShape}
155+
types={types}
156+
lang={lang}
157+
exclude={[]}
158+
excludeDeprecated={false}
159+
/>
160+
</SectionContainer>
161+
</TypeDefinitionAnchorPart>
162+
</div>
163+
</div>
164+
</div>
165+
</div>
166+
);
167+
}
168+
169+
function EndpointRow({
170+
endpoint,
171+
typeDefinitions,
172+
types,
173+
lang,
174+
expandedModel,
175+
onToggleModel
176+
}: {
177+
endpoint: EndpointData;
178+
typeDefinitions: Record<string, ApiDefinition.TypeDefinition | TypeDefinitionWithSerializedDescriptions>;
179+
types: Record<ApiDefinition.TypeId, ApiDefinition.TypeDefinition>;
180+
lang: string;
181+
expandedModel: string | null;
182+
onToggleModel: (key: string) => void;
183+
}) {
184+
return (
185+
<div className="border-b border-[#e0e0e0] py-4 last:border-b-0 dark:border-gray-700">
186+
<div className="flex flex-col gap-4 min-[1100px]:flex-row min-[1100px]:items-start min-[1100px]:justify-between">
187+
{/* Left side: Method and path */}
188+
<div className="flex min-w-[150px] items-center gap-2">
189+
<MethodText method={endpoint.method} />
190+
<div className="break-all text-sm">{endpoint.path}</div>
191+
</div>
192+
193+
{/* Right side: Model accordions */}
194+
<div className="rounded-2 w-full shrink grow-0 overflow-hidden border border-[#eaeef3] min-[1100px]:shrink-0 min-[1100px]:basis-[540px] dark:border-gray-700">
195+
<div className="border-b border-[#e0e0e0] px-2 py-2 dark:border-gray-700">
196+
<span className="text-sm font-bold">Merge interacts with this API endpoint to...</span>
197+
</div>
198+
{endpoint.models.map((model) => {
199+
const key = `${endpoint.method}-${endpoint.path}-${model.name}`;
200+
return (
201+
<ModelAccordion
202+
key={key}
203+
accordionKey={key}
204+
model={model}
205+
typeDefinition={typeDefinitions[model.name]}
206+
types={types}
207+
lang={lang}
208+
isExpanded={expandedModel === key}
209+
onToggle={() => onToggleModel(key)}
210+
/>
211+
);
212+
})}
213+
</div>
214+
</div>
215+
</div>
216+
);
217+
}
218+
219+
export function MergeAccessedThirdPartyEndpointsWidget({
220+
decodedData,
221+
typeDefinitions,
222+
types,
223+
lang,
224+
className
225+
}: MergeAccessedThirdPartyEndpointsWidgetProps) {
226+
const currentSlug = useCurrentSlug();
227+
const [expandedModel, setExpandedModel] = useState<string | null>(null);
228+
229+
if (decodedData == null || typeDefinitions == null || types == null) {
230+
return null;
231+
}
232+
233+
const language = lang ?? "en";
234+
const endpoints = decodedData.endpoints;
235+
236+
const handleToggleModel = (key: string) => {
237+
setExpandedModel((prev) => (prev === key ? null : key));
238+
};
239+
240+
return (
241+
<TypeDefinitionRoot types={types} slug={currentSlug}>
242+
<TypeDefinitionSlotsServer types={types} lang={language}>
243+
<div className={className}>
244+
<div className="mb-4 border-b border-[#e0e0e0] pb-4 dark:border-gray-700">
245+
<h3 className="m-0! text-xl font-semibold">API Endpoints</h3>
246+
</div>
247+
<div>
248+
{endpoints.map((endpoint, index) => (
249+
<EndpointRow
250+
key={`${endpoint.method}-${endpoint.path}-${index}`}
251+
endpoint={endpoint}
252+
typeDefinitions={typeDefinitions}
253+
types={types}
254+
lang={language}
255+
expandedModel={expandedModel}
256+
onToggleModel={handleToggleModel}
257+
/>
258+
))}
259+
</div>
260+
</div>
261+
</TypeDefinitionSlotsServer>
262+
</TypeDefinitionRoot>
263+
);
264+
}

packages/fern-docs/bundle/src/mdx/components/snippets/MergeSupportedFieldsByIntegrationWidget.tsx

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { useCurrentSlug } from "@fern-docs/components/hooks/use-current-pathname
1010
import { ArrowUpRight, ChevronDown, ChevronRight } from "lucide-react";
1111
import Image from "next/image";
1212
import Link from "next/link";
13-
import { useState } from "react";
13+
import { useRef, useState } from "react";
1414

1515
import { TypeDefinitionSlotsServer } from "@/components/api-reference/type-definitions/TypeDefinitionSlotsServer";
1616
import { TypeReferenceDefinitions } from "@/components/api-reference/type-definitions/TypeReferenceDefinitions";
@@ -84,6 +84,24 @@ function IntegrationRow({
8484
passthroughRequestsHref?: string;
8585
deletedDataDetectionHref?: string;
8686
}) {
87+
const rowRef = useRef<HTMLDivElement>(null);
88+
const expandableRef = useRef<HTMLDivElement>(null);
89+
90+
const handleToggle = () => {
91+
const expandable = expandableRef.current;
92+
if (expandable) {
93+
const onTransitionEnd = () => {
94+
expandable.removeEventListener("transitionend", onTransitionEnd);
95+
rowRef.current?.scrollIntoView({
96+
behavior: "smooth",
97+
block: "start"
98+
});
99+
};
100+
expandable.addEventListener("transitionend", onTransitionEnd);
101+
}
102+
onToggle();
103+
};
104+
87105
// Filter the shape's properties to only show supported fields
88106
let filteredShape = typeDefinition.shape;
89107
if (filteredShape.type === "object") {
@@ -99,10 +117,10 @@ function IntegrationRow({
99117
const schemaName = typeDefinition.displayName || typeDefinition.name || "schema";
100118

101119
return (
102-
<div className="border-b border-[#e0e0e0] last:border-b-0">
120+
<div ref={rowRef} className="scroll-mt-4 border-b border-[#e0e0e0] last:border-b-0">
103121
<button
104122
type="button"
105-
onClick={onToggle}
123+
onClick={handleToggle}
106124
className="flex w-full cursor-pointer items-center gap-3 py-2 text-left transition-colors hover:bg-gray-50 dark:hover:bg-gray-800/50"
107125
>
108126
{integration.integrationImage && (
@@ -121,6 +139,7 @@ function IntegrationRow({
121139
/>
122140
</button>
123141
<div
142+
ref={expandableRef}
124143
className="grid transition-[grid-template-rows] duration-200 ease-in-out"
125144
style={{ gridTemplateRows: isExpanded ? "1fr" : "0fr" }}
126145
>
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
export * from "./EndpointRequestSnippet";
22
export * from "./EndpointResponseSnippet";
3+
export * from "./MergeAccessedThirdPartyEndpointsWidget";
34
export * from "./MergeSupportedFieldsByIntegrationWidget";
45
export * from "./Schema";
56
export * from "./SchemaSnippet";

0 commit comments

Comments
 (0)