Skip to content

Commit 6ebffbb

Browse files
authored
feat: dynamically update request body when anyOf/oneOf tab changes (#1235) (#1287)
When a schema has anyOf or oneOf, the request body editor now updates dynamically when the user selects a different option in the schema tabs. - Add SchemaSelection Redux slice to track selected anyOf/oneOf indices - Add resolveSchemaWithSelections utility to resolve schema based on selections - Update Schema component to dispatch selection changes when tabs are clicked - Update Body component to regenerate examples when selection changes - Add onChange callback to SchemaTabs component
1 parent 6a466fe commit 6ebffbb

File tree

9 files changed

+626
-178
lines changed

9 files changed

+626
-178
lines changed

packages/docusaurus-theme-openapi-docs/src/theme/ApiExplorer/Body/index.tsx

Lines changed: 205 additions & 145 deletions
Large diffs are not rendered by default.
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
/* ============================================================================
2+
* Copyright (c) Palo Alto Networks
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
* ========================================================================== */
7+
8+
import { SchemaObject } from "docusaurus-plugin-openapi-docs/src/openapi/types";
9+
import merge from "lodash/merge";
10+
11+
export interface SchemaSelections {
12+
[schemaPath: string]: number;
13+
}
14+
15+
/**
16+
* Resolves a schema by replacing anyOf/oneOf with the selected option based on user selections.
17+
*
18+
* @param schema - The original schema object
19+
* @param selections - Map of schema paths to selected indices
20+
* @param basePath - The base path for this schema (used for looking up selections)
21+
* @returns A new schema with anyOf/oneOf resolved to selected options
22+
*/
23+
export function resolveSchemaWithSelections(
24+
schema: SchemaObject | undefined,
25+
selections: SchemaSelections,
26+
basePath: string = "requestBody"
27+
): SchemaObject | undefined {
28+
if (!schema) {
29+
return schema;
30+
}
31+
32+
// Deep clone to avoid mutating the original schema
33+
const schemaCopy = JSON.parse(JSON.stringify(schema)) as SchemaObject;
34+
35+
return resolveSchemaRecursive(schemaCopy, selections, basePath);
36+
}
37+
38+
function resolveSchemaRecursive(
39+
schema: SchemaObject,
40+
selections: SchemaSelections,
41+
currentPath: string
42+
): SchemaObject {
43+
// Handle oneOf
44+
if (schema.oneOf && Array.isArray(schema.oneOf)) {
45+
const selectedIndex = selections[currentPath] ?? 0;
46+
const selectedSchema = schema.oneOf[selectedIndex] as SchemaObject;
47+
48+
if (selectedSchema) {
49+
// If there are shared properties, merge them with the selected schema
50+
if (schema.properties) {
51+
const mergedSchema = merge({}, schema, selectedSchema);
52+
delete mergedSchema.oneOf;
53+
54+
// Continue resolving nested schemas in the merged result
55+
return resolveSchemaRecursive(
56+
mergedSchema,
57+
selections,
58+
`${currentPath}.${selectedIndex}`
59+
);
60+
}
61+
62+
// No shared properties, just use the selected schema
63+
// Continue resolving in case there are nested anyOf/oneOf
64+
return resolveSchemaRecursive(
65+
selectedSchema,
66+
selections,
67+
`${currentPath}.${selectedIndex}`
68+
);
69+
}
70+
}
71+
72+
// Handle anyOf
73+
if (schema.anyOf && Array.isArray(schema.anyOf)) {
74+
const selectedIndex = selections[currentPath] ?? 0;
75+
const selectedSchema = schema.anyOf[selectedIndex] as SchemaObject;
76+
77+
if (selectedSchema) {
78+
// If there are shared properties, merge them with the selected schema
79+
if (schema.properties) {
80+
const mergedSchema = merge({}, schema, selectedSchema);
81+
delete mergedSchema.anyOf;
82+
83+
// Continue resolving nested schemas in the merged result
84+
return resolveSchemaRecursive(
85+
mergedSchema,
86+
selections,
87+
`${currentPath}.${selectedIndex}`
88+
);
89+
}
90+
91+
// No shared properties, just use the selected schema
92+
// Continue resolving in case there are nested anyOf/oneOf
93+
return resolveSchemaRecursive(
94+
selectedSchema,
95+
selections,
96+
`${currentPath}.${selectedIndex}`
97+
);
98+
}
99+
}
100+
101+
// Handle allOf - merge all schemas and continue resolving
102+
if (schema.allOf && Array.isArray(schema.allOf)) {
103+
// Process each allOf item, resolving any anyOf/oneOf within them
104+
const resolvedItems = schema.allOf.map((item, index) => {
105+
return resolveSchemaRecursive(
106+
item as SchemaObject,
107+
selections,
108+
`${currentPath}.allOf.${index}`
109+
);
110+
});
111+
112+
// Merge all resolved items
113+
const mergedSchema = resolvedItems.reduce(
114+
(acc, item) => merge(acc, item),
115+
{} as SchemaObject
116+
);
117+
118+
// Preserve any top-level properties from the original schema
119+
if (schema.properties) {
120+
mergedSchema.properties = merge(
121+
{},
122+
mergedSchema.properties,
123+
schema.properties
124+
);
125+
}
126+
127+
return mergedSchema;
128+
}
129+
130+
// Handle object properties recursively
131+
if (schema.properties) {
132+
const resolvedProperties: { [key: string]: SchemaObject } = {};
133+
134+
for (const [propName, propSchema] of Object.entries(schema.properties)) {
135+
resolvedProperties[propName] = resolveSchemaRecursive(
136+
propSchema as SchemaObject,
137+
selections,
138+
`${currentPath}.${propName}`
139+
);
140+
}
141+
142+
schema.properties = resolvedProperties;
143+
}
144+
145+
// Handle array items recursively
146+
if (schema.items) {
147+
schema.items = resolveSchemaRecursive(
148+
schema.items as SchemaObject,
149+
selections,
150+
`${currentPath}.items`
151+
);
152+
}
153+
154+
return schema;
155+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
/* ============================================================================
2+
* Copyright (c) Palo Alto Networks
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
* ========================================================================== */
7+
8+
export {
9+
default as schemaSelectionReducer,
10+
setSchemaSelection,
11+
clearSchemaSelections,
12+
} from "./slice";
13+
export type { SchemaSelectionState } from "./slice";
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/* ============================================================================
2+
* Copyright (c) Palo Alto Networks
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
* ========================================================================== */
7+
8+
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
9+
10+
export interface SchemaSelectionState {
11+
/**
12+
* Maps schema path (e.g., "requestBody", "requestBody.anyOf.0.layer3")
13+
* to the selected anyOf/oneOf option index
14+
*/
15+
selections: { [schemaPath: string]: number };
16+
}
17+
18+
const initialState: SchemaSelectionState = {
19+
selections: {},
20+
};
21+
22+
export const slice = createSlice({
23+
name: "schemaSelection",
24+
initialState,
25+
reducers: {
26+
/**
27+
* Set the selected index for a specific schema path
28+
*/
29+
setSchemaSelection: (
30+
state,
31+
action: PayloadAction<{ path: string; index: number }>
32+
) => {
33+
state.selections[action.payload.path] = action.payload.index;
34+
},
35+
/**
36+
* Clear all schema selections (useful when navigating to a new API endpoint)
37+
*/
38+
clearSchemaSelections: (state) => {
39+
state.selections = {};
40+
},
41+
},
42+
});
43+
44+
export const { setSchemaSelection, clearSchemaSelections } = slice.actions;
45+
46+
export default slice.reducer;

packages/docusaurus-theme-openapi-docs/src/theme/ApiItem/index.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,7 @@ export default function ApiItem(props: Props): JSX.Element {
158158
body: { type: "empty" },
159159
params,
160160
auth,
161+
schemaSelection: { selections: {} },
161162
},
162163
[persistenceMiddleware]
163164
);

packages/docusaurus-theme-openapi-docs/src/theme/ApiItem/store.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import body from "@theme/ApiExplorer/Body/slice";
1212
import contentType from "@theme/ApiExplorer/ContentType/slice";
1313
import params from "@theme/ApiExplorer/ParamOptions/slice";
1414
import response from "@theme/ApiExplorer/Response/slice";
15+
import schemaSelection from "@theme/ApiExplorer/SchemaSelection/slice";
1516
import server from "@theme/ApiExplorer/Server/slice";
1617

1718
const rootReducer = combineReducers({
@@ -22,6 +23,7 @@ const rootReducer = combineReducers({
2223
body,
2324
params,
2425
auth,
26+
schemaSelection,
2527
});
2628

2729
export type RootState = ReturnType<typeof rootReducer>;

packages/docusaurus-theme-openapi-docs/src/theme/RequestSchema/index.tsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,11 @@ const RequestSchemaComponent: React.FC<Props> = ({ title, body, style }) => {
9393
)}
9494
</div>
9595
<ul style={{ marginLeft: "1rem" }}>
96-
<SchemaNode schema={firstBody} schemaType="request" />
96+
<SchemaNode
97+
schema={firstBody}
98+
schemaType="request"
99+
schemaPath="requestBody"
100+
/>
97101
</ul>
98102
</Details>
99103
</div>
@@ -153,7 +157,11 @@ const RequestSchemaComponent: React.FC<Props> = ({ title, body, style }) => {
153157
)}
154158
</div>
155159
<ul style={{ marginLeft: "1rem" }}>
156-
<SchemaNode schema={firstBody} schemaType="request" />
160+
<SchemaNode
161+
schema={firstBody}
162+
schemaType="request"
163+
schemaPath="requestBody"
164+
/>
157165
</ul>
158166
</Details>
159167
</TabItem>

0 commit comments

Comments
 (0)