Skip to content

Commit 2970885

Browse files
committed
OpenAPI: support transformAuthInputs
1 parent 40d0fa3 commit 2970885

File tree

7 files changed

+107
-44
lines changed

7 files changed

+107
-44
lines changed

.changeset/grumpy-grapes-punch.md

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,4 +54,50 @@ export const APIPage = createAPIPage(openapi, {
5454
},
5555
},
5656
});
57-
```
57+
```
58+
59+
2. Remove `disablePlayground` from `createAPIPage()`, use `playground.enabled` instead:
60+
61+
```ts
62+
// components/api-page.tsx
63+
import { openapi } from '@/lib/openapi';
64+
import { createAPIPage } from 'fumadocs-openapi/ui';
65+
66+
export const APIPage = createAPIPage(openapi, {
67+
playground: {
68+
enabled: false,
69+
}
70+
});
71+
```
72+
73+
3. Support client config:
74+
75+
```tsx
76+
// components/api-page.tsx
77+
import { openapi } from '@/lib/openapi';
78+
import { createAPIPage } from 'fumadocs-openapi/ui';
79+
import client from "./api-page.client"
80+
81+
export const APIPage = createAPIPage(openapi, {
82+
client,
83+
});
84+
```
85+
86+
```tsx
87+
// components/api-page.client.tsx
88+
'use client';
89+
import { defineClientConfig } from 'fumadocs-openapi/ui/client';
90+
91+
export default defineClientConfig({
92+
playground: {
93+
transformAuthInputs: (inputs) => [
94+
...inputs,
95+
{
96+
fieldName: 'auth.tests',
97+
children: <div>Tests</div>,
98+
defaultValue: '',
99+
},
100+
],
101+
},
102+
});
103+
```

packages/openapi/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@
2222
"types": "./dist/ui/index.d.ts"
2323
},
2424
"./ui/client": {
25-
"import": "./dist/ui/client.js",
26-
"types": "./dist/ui/client.d.ts"
25+
"import": "./dist/ui/client/index.js",
26+
"types": "./dist/ui/client/index.d.ts"
2727
},
2828
"./playground": {
2929
"import": "./dist/playground/index.js",

packages/openapi/src/playground/client.tsx

Lines changed: 35 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,9 @@ export interface CustomField<TName extends FieldPath<FormValues>, Info> {
100100
}) => ReactElement;
101101
}
102102

103-
export interface ClientProps extends HTMLAttributes<HTMLFormElement> {
103+
export interface ClientProps
104+
extends HTMLAttributes<HTMLFormElement>,
105+
PlaygroundClientOptions {
104106
route: string;
105107
method: string;
106108
parameters?: ParameterField[];
@@ -115,26 +117,32 @@ export interface ClientProps extends HTMLAttributes<HTMLFormElement> {
115117
references: Record<string, RequestSchema>;
116118
proxyUrl?: string;
117119

118-
/**
119-
* Request timeout in seconds (default: 10s)
120-
*/
121-
requestTimeout?: number;
120+
// TODO: redesign `fields`
122121
fields?: {
123122
parameter?: CustomField<
124123
`${ParameterField['in']}.${string}`,
125124
ParameterField
126125
>;
127-
auth?: CustomField<FieldPath<FormValues>, RequestSchema>;
128126
body?: CustomField<'body', RequestSchema>;
129127
};
128+
}
129+
130+
export interface PlaygroundClientOptions {
131+
/**
132+
* transform fields for auth-specific parameters (e.g. header)
133+
*/
134+
transformAuthInputs?: (fields: AuthField[]) => AuthField[];
135+
136+
/**
137+
* Request timeout in seconds (default: 10s)
138+
*/
139+
requestTimeout?: number;
130140

131141
components?: Partial<{
132142
ResultDisplay: FC<{ data: FetchResult }>;
133143
}>;
134144
}
135145

136-
const AuthPrefix = '__fumadocs_auth';
137-
138146
const OauthDialog = lazy(() =>
139147
import('./components/oauth-dialog').then((mod) => ({
140148
default: mod.OauthDialog,
@@ -157,6 +165,7 @@ export default function Client({
157165
proxyUrl,
158166
components: { ResultDisplay = DefaultResultDisplay } = {},
159167
requestTimeout = 10,
168+
transformAuthInputs,
160169
...rest
161170
}: ClientProps) {
162171
const { server } = useServerSelectContext();
@@ -165,7 +174,10 @@ export default function Client({
165174
const fieldInfoMap = useMemo(() => new Map<string, FieldInfo>(), []);
166175
const { mediaAdapters } = useApiContext();
167176
const [securityId, setSecurityId] = useState(0);
168-
const { inputs, mapInputs } = useAuthInputs(securities[securityId]);
177+
const { inputs, mapInputs } = useAuthInputs(
178+
securities[securityId],
179+
transformAuthInputs,
180+
);
169181

170182
const defaultValues: FormValues = useMemo(
171183
() => ({
@@ -214,7 +226,7 @@ export default function Client({
214226

215227
if (value) {
216228
localStorage.setItem(
217-
AuthPrefix + item.original.id,
229+
getAuthFieldStorageKey(item),
218230
JSON.stringify(value),
219231
);
220232
}
@@ -521,17 +533,20 @@ function BodyInput({ field: _field }: { field: RequestSchema }) {
521533
);
522534
}
523535

524-
interface AuthField {
536+
export interface AuthField {
525537
fieldName: string;
526538
defaultValue: unknown;
527539

528-
original: SecurityEntry;
540+
original?: SecurityEntry;
529541
children: ReactNode;
530542

531543
mapOutput?: (values: unknown) => unknown;
532544
}
533545

534-
function useAuthInputs(securities?: SecurityEntry[]) {
546+
function useAuthInputs(
547+
securities?: SecurityEntry[],
548+
transform?: (fields: AuthField[]) => AuthField[],
549+
) {
535550
const inputs = useMemo(() => {
536551
const result: AuthField[] = [];
537552
if (!securities) return result;
@@ -672,8 +687,8 @@ function useAuthInputs(securities?: SecurityEntry[]) {
672687
}
673688
}
674689

675-
return result;
676-
}, [securities]);
690+
return transform ? transform(result) : result;
691+
}, [securities, transform]);
677692

678693
const mapInputs = (values: FormValues) => {
679694
const cloned = structuredClone(values);
@@ -690,9 +705,13 @@ function useAuthInputs(securities?: SecurityEntry[]) {
690705
return { inputs, mapInputs };
691706
}
692707

708+
function getAuthFieldStorageKey(field: AuthField) {
709+
return '__fumadocs_auth' + (field.original?.id ?? field.fieldName);
710+
}
711+
693712
function initAuthValues(values: FormValues, inputs: AuthField[]) {
694713
for (const item of inputs) {
695-
const stored = localStorage.getItem(AuthPrefix + item.original.id);
714+
const stored = localStorage.getItem(getAuthFieldStorageKey(item));
696715

697716
if (stored) {
698717
const parsed = JSON.parse(stored);

packages/openapi/src/playground/index.tsx

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,6 @@ export interface APIPlaygroundProps {
2929
path: string;
3030
method: MethodInformation;
3131
ctx: RenderContext;
32-
33-
client?: Partial<ClientProps>;
3432
}
3533

3634
export type { ClientProps, CustomField } from './client';
@@ -40,12 +38,7 @@ export type SecurityEntry = SecuritySchemeObject & {
4038
id: string;
4139
};
4240

43-
export async function APIPlayground({
44-
path,
45-
method,
46-
ctx,
47-
client,
48-
}: APIPlaygroundProps) {
41+
export async function APIPlayground({ path, method, ctx }: APIPlaygroundProps) {
4942
let currentId = 0;
5043
const bodyContent = method.requestBody?.content;
5144
const mediaType = bodyContent ? getPreferredType(bodyContent) : undefined;
@@ -75,7 +68,7 @@ export async function APIPlayground({
7568
: undefined,
7669
references: context.references,
7770
proxyUrl: ctx.proxyUrl,
78-
...client,
71+
...ctx.client?.playground,
7972
};
8073

8174
return <ClientLazy {...props} />;

packages/openapi/src/ui/api-page.tsx

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,19 +11,12 @@ import type {
1111
HighlightOptionsCommon,
1212
HighlightOptionsThemes,
1313
} from 'fumadocs-core/highlight';
14-
import { OpenAPIServer } from '@/server';
14+
import type { OpenAPIServer } from '@/server';
15+
import type { APIPageClientOptions } from './client';
1516

1617
type Awaitable<T> = T | Promise<T>;
1718

1819
export interface CreateAPIPageOptions {
19-
/**
20-
* Disable API Playground
21-
*
22-
* @defaultValue false
23-
* @deprecated Use `playground.enabled` instead
24-
*/
25-
disablePlayground?: boolean;
26-
2720
/**
2821
* Generate TypeScript definitions from response schema.
2922
*
@@ -85,6 +78,8 @@ export interface CreateAPIPageOptions {
8578
ctx: RenderContext;
8679
}) => ReactNode | Promise<ReactNode>;
8780
};
81+
82+
client?: APIPageClientOptions;
8883
}
8984

9085
export interface ApiPageProps {
@@ -139,7 +134,6 @@ export function createAPIPage(
139134
const ctx: RenderContext = {
140135
schema: processed,
141136
proxyUrl: server.options.proxyUrl,
142-
disablePlayground: options.disablePlayground,
143137
showResponseSchema: options.showResponseSchema,
144138
renderer: {
145139
...createRenders(),
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import type { PlaygroundClientOptions } from '@/playground/client';
2+
3+
export interface APIPageClientOptions {
4+
playground?: PlaygroundClientOptions;
5+
}
6+
7+
export function defineClientConfig(
8+
options: APIPageClientOptions,
9+
): APIPageClientOptions {
10+
return options;
11+
}

packages/openapi/src/ui/operation/index.tsx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -256,20 +256,20 @@ export function Operation({
256256
);
257257
}
258258

259+
const playgroundEnabled = ctx.playground?.enabled ?? true;
259260
const info = (
260261
<ctx.renderer.APIInfo head={headNode} method={method.method} route={path}>
261-
{type === 'operation' ? (
262-
ctx.disablePlayground ? (
262+
{type === 'operation' &&
263+
(playgroundEnabled ? (
264+
<ctx.renderer.APIPlayground path={path} method={method} ctx={ctx} />
265+
) : (
263266
<div className="flex flex-row items-center gap-2.5 p-3 rounded-xl border bg-fd-card text-fd-card-foreground not-prose">
264267
<MethodLabel className="text-xs">{method.method}</MethodLabel>
265268
<code className="flex-1 overflow-auto text-nowrap text-[13px] text-fd-muted-foreground">
266269
{path}
267270
</code>
268271
</div>
269-
) : (
270-
<ctx.renderer.APIPlayground path={path} method={method} ctx={ctx} />
271-
)
272-
) : null}
272+
))}
273273
{authNode}
274274
{parameterNode}
275275
{bodyNode}

0 commit comments

Comments
 (0)