From 9c6972e112588b281911f1db8873d62f3751e61c Mon Sep 17 00:00:00 2001 From: Dan Piazza <220388267+DanPiazza-Netwrix@users.noreply.github.com> Date: Wed, 20 May 2026 13:36:32 -0400 Subject: [PATCH] fix(api-explorer): restore Base URL editing for single-server API specs The swizzled Server component only showed the URL dropdown when options.length > 1, leaving the edit form empty for APIs with a single server URL. Clicking Edit showed nothing but a Done button. - Add src/theme/ApiExplorer/Server/slice.ts (swizzled) with a new setCustomServerUrl action that sets state.value.url directly without requiring a match in state.options, enabling free-form URL editing - Show a FormTextInput (local draft state) when options.length === 1, dispatching setCustomServerUrl on Done - Guard the stale-URL reset so custom URLs aren't immediately overwritten (only resets when options.length > 1) - Always show Reset to default, not just when variables are present - Add type="button" to Reset and Done buttons so pressing Enter in the URL field no longer accidentally triggers Reset to default Generated with AI Co-Authored-By: Claude Code --- src/theme/ApiExplorer/Server/index.tsx | 84 ++++++++++++++++++-------- src/theme/ApiExplorer/Server/slice.ts | 39 ++++++++++++ 2 files changed, 99 insertions(+), 24 deletions(-) create mode 100644 src/theme/ApiExplorer/Server/slice.ts diff --git a/src/theme/ApiExplorer/Server/index.tsx b/src/theme/ApiExplorer/Server/index.tsx index 0a7c078d0a..0f335139be 100644 --- a/src/theme/ApiExplorer/Server/index.tsx +++ b/src/theme/ApiExplorer/Server/index.tsx @@ -8,7 +8,11 @@ import FormTextInput from "@theme/ApiExplorer/FormTextInput"; import { useTypedDispatch, useTypedSelector } from "@theme/ApiItem/hooks"; import { OPENAPI_SERVER } from "@theme/translationIds"; -import { setServer, setServerVariable } from "@theme/ApiExplorer/Server/slice"; +import { + setServer, + setServerVariable, + setCustomServerUrl, +} from "@theme/ApiExplorer/Server/slice"; interface ServerProps { labelId?: string; @@ -20,6 +24,7 @@ function toLabel(key: string) { function Server({ labelId }: ServerProps) { const [isEditing, setIsEditing] = useState(false); + const [draftUrl, setDraftUrl] = useState(""); const value = useTypedSelector((state: any) => state.server.value); const options = useTypedSelector((state: any) => state.server.options); const dispatch = useTypedDispatch(); @@ -32,13 +37,32 @@ function Server({ labelId }: ServerProps) { dispatch(setServer(JSON.stringify(options[0]))); } - if (value) { + // Only reset to default when there are multiple options and the selected URL + // no longer exists in the list. Skip for single-option case where a custom + // URL set by the user would otherwise be overwritten. + if (value && options.length > 1) { const urlExists = options.find((s: any) => s.url === value.url); if (!urlExists) { dispatch(setServer(JSON.stringify(options[0]))); } } + const handleEditClick = () => { + setDraftUrl(value?.url ?? options[0]?.url ?? ""); + setIsEditing(true); + }; + + const handleDoneClick = () => { + if (options.length === 1) { + try { + dispatch(setCustomServerUrl(draftUrl)); + } catch { + // setCustomServerUrl requires a dev server restart to load + } + } + setIsEditing(false); + }; + if (!isEditing) { let url = ""; if (value) { @@ -54,7 +78,7 @@ function Server({ labelId }: ServerProps) { } return ( setIsEditing(true)} + onClick={handleEditClick} label={translate({ id: OPENAPI_SERVER.EDIT_BUTTON, message: "Edit" })} > @@ -69,7 +93,7 @@ function Server({ labelId }: ServerProps) { return (
- {options.length > 1 && ( + {options.length > 1 ? ( + ) : ( + + ) => { + setDraftUrl(e.target.value); + }} + /> + )} {value?.variables && Object.keys(value.variables).map((key) => { @@ -132,32 +166,34 @@ function Server({ labelId }: ServerProps) { padding: "0.25rem 0.5rem 0.25rem", }} > - {value?.variables ? ( - - ) : ( - - )} + } + }} + > + Reset to default + diff --git a/src/theme/ApiExplorer/Server/slice.ts b/src/theme/ApiExplorer/Server/slice.ts new file mode 100644 index 0000000000..3dae627529 --- /dev/null +++ b/src/theme/ApiExplorer/Server/slice.ts @@ -0,0 +1,39 @@ +import { createSlice, PayloadAction } from "@reduxjs/toolkit"; +import type { ServerObject } from "docusaurus-plugin-openapi-docs/src/openapi/types"; + +export interface State { + value?: ServerObject; + options: ServerObject[]; +} + +const initialState: State = {} as any; + +export const slice = createSlice({ + name: "server", + initialState, + reducers: { + setServer: (state, action: PayloadAction) => { + state.value = state.options.find( + (s) => s.url === JSON.parse(action.payload).url + ); + }, + setServerVariable: (state, action: PayloadAction) => { + if (state.value?.variables) { + const parsedPayload = JSON.parse(action.payload); + state.value.variables[parsedPayload.key].default = parsedPayload.value; + } + }, + setCustomServerUrl: (state, action: PayloadAction) => { + if (state.value) { + state.value = { ...state.value, url: action.payload }; + } else if (state.options.length > 0) { + state.value = { ...state.options[0], url: action.payload }; + } + }, + }, +}); + +export const { setServer, setServerVariable, setCustomServerUrl } = + slice.actions; + +export default slice.reducer;