mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-09-21 10:03:31 +00:00
Sync responses selector with the Responses collapsibles (#4376)
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Sync the API reference responses selector with the "Responses" collapsibles, and keep the selected response in sync across every operation on the page (like the code sample language selector). Selecting a status code now expands the matching response section and applies to all operations at once.
|
||||
@@ -59,10 +59,11 @@ export function OpenAPIDisclosureGroup(props: DisclosureGroupProps & Props) {
|
||||
onExpandedChange,
|
||||
} = props;
|
||||
|
||||
const initialKeys =
|
||||
expandedKeys || defaultExpandedKeys
|
||||
? new Set(expandedKeys || defaultExpandedKeys)
|
||||
: undefined;
|
||||
// When `expandedKeys` is provided, the group is controlled by the parent. Otherwise it owns
|
||||
// its expanded state in a store keyed by `stateKey`.
|
||||
const isControlled = expandedKeys !== undefined;
|
||||
|
||||
const initialKeys = defaultExpandedKeys ? new Set(defaultExpandedKeys) : undefined;
|
||||
const { expandedKeys: storeExpandedKeys, setExpandedKeys } = useDisclosureGroupStore(
|
||||
stateKey,
|
||||
initialKeys
|
||||
@@ -70,9 +71,11 @@ export function OpenAPIDisclosureGroup(props: DisclosureGroupProps & Props) {
|
||||
|
||||
const state = useDisclosureGroupState({
|
||||
...props,
|
||||
expandedKeys: storeExpandedKeys,
|
||||
expandedKeys: isControlled ? expandedKeys : storeExpandedKeys,
|
||||
onExpandedChange: (keys) => {
|
||||
setExpandedKeys(keys);
|
||||
if (!isControlled) {
|
||||
setExpandedKeys(keys);
|
||||
}
|
||||
onExpandedChange?.(keys);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -93,13 +93,7 @@ export function OpenAPIResponseExample(props: {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<OpenAPIResponseExampleContent
|
||||
selectIcon={context.icons.chevronDown}
|
||||
blockKey={context.blockKey}
|
||||
items={tabs}
|
||||
/>
|
||||
);
|
||||
return <OpenAPIResponseExampleContent selectIcon={context.icons.chevronDown} items={tabs} />;
|
||||
}
|
||||
|
||||
function OpenAPIResponse(props: {
|
||||
|
||||
@@ -14,42 +14,31 @@ type OpenAPIResponseExampleItem = OpenAPISelectItem & {
|
||||
/**
|
||||
* Get the state of the response examples select.
|
||||
*/
|
||||
export function useResponseExamplesState(
|
||||
blockKey: string | undefined,
|
||||
initialKey: Key = 'default'
|
||||
) {
|
||||
return useSelectState(getResponseExampleStateKey(blockKey), initialKey);
|
||||
export function useResponseExamplesState(initialKey: Key = 'default') {
|
||||
return useSelectState(getResponseExampleStateKey(), initialKey);
|
||||
}
|
||||
|
||||
export function OpenAPIResponseExampleContent(props: {
|
||||
items: OpenAPIResponseExampleItem[];
|
||||
blockKey?: string;
|
||||
selectIcon?: React.ReactNode;
|
||||
}) {
|
||||
const { blockKey, items, selectIcon } = props;
|
||||
const { items, selectIcon } = props;
|
||||
|
||||
return (
|
||||
<StaticSection
|
||||
header={
|
||||
<OpenAPIResponseExampleHeader
|
||||
selectIcon={selectIcon}
|
||||
blockKey={blockKey}
|
||||
items={items}
|
||||
/>
|
||||
}
|
||||
header={<OpenAPIResponseExampleHeader selectIcon={selectIcon} items={items} />}
|
||||
className="openapi-response-examples"
|
||||
>
|
||||
<OpenAPIResponseExampleBody blockKey={blockKey} items={items} />
|
||||
<OpenAPIResponseExampleBody items={items} />
|
||||
</StaticSection>
|
||||
);
|
||||
}
|
||||
|
||||
function OpenAPIResponseExampleHeader(props: {
|
||||
items: OpenAPIResponseExampleItem[];
|
||||
blockKey?: string;
|
||||
selectIcon?: React.ReactNode;
|
||||
}) {
|
||||
const { items, blockKey, selectIcon } = props;
|
||||
const { items, selectIcon } = props;
|
||||
|
||||
if (items.length === 1) {
|
||||
const item = items[0];
|
||||
@@ -69,7 +58,7 @@ function OpenAPIResponseExampleHeader(props: {
|
||||
<OpenAPISelect
|
||||
items={items}
|
||||
icon={selectIcon}
|
||||
stateKey={getResponseExampleStateKey(blockKey)}
|
||||
stateKey={getResponseExampleStateKey()}
|
||||
placement="bottom start"
|
||||
>
|
||||
{items.map((item) => (
|
||||
@@ -103,10 +92,9 @@ function OpenAPIResponseExampleItem(props: {
|
||||
|
||||
function OpenAPIResponseExampleBody(props: {
|
||||
items: OpenAPIResponseExampleItem[];
|
||||
blockKey?: string;
|
||||
}) {
|
||||
const { blockKey, items } = props;
|
||||
const state = useResponseExamplesState(blockKey, items[0]?.key);
|
||||
const { items } = props;
|
||||
const state = useResponseExamplesState(items[0]?.key);
|
||||
|
||||
const selectedItem = items.find((item) => item.key === state.key) ?? items[0];
|
||||
|
||||
@@ -118,8 +106,9 @@ function OpenAPIResponseExampleBody(props: {
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the state key for the response examples.
|
||||
* Return the state key for the response examples. Not scoped to a block so the selected response
|
||||
* stays in sync across every operation on the page (like the code sample language selector).
|
||||
*/
|
||||
function getResponseExampleStateKey(blockKey: string | undefined) {
|
||||
return createStateKey('openapi-responses', blockKey);
|
||||
function getResponseExampleStateKey() {
|
||||
return createStateKey('openapi-responses');
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import type { OpenAPIV3, OpenAPIV3_1 } from '@gitbook/openapi-parser';
|
||||
import clsx from 'classnames';
|
||||
import type { Key } from 'react-aria';
|
||||
import { Markdown } from './Markdown';
|
||||
import { OpenAPIDisclosureGroup } from './OpenAPIDisclosureGroup';
|
||||
import { OpenAPIResponse } from './OpenAPIResponse';
|
||||
@@ -105,29 +106,47 @@ export function OpenAPIResponses(props: {
|
||||
};
|
||||
});
|
||||
|
||||
const state = useResponseExamplesState(context.blockKey, groups[0]?.key);
|
||||
const state = useResponseExamplesState(groups[0]?.key);
|
||||
|
||||
const expandAll = context.expandAllResponses;
|
||||
const expandedKeys = expandAll
|
||||
? new Set(groups.map((g) => g.key))
|
||||
: state.key
|
||||
? new Set([state.key])
|
||||
: new Set<string>();
|
||||
|
||||
// In expand-all mode the group manages its own (multiple) expanded rows; otherwise it's
|
||||
// controlled and stays in sync with the page-wide responses selector.
|
||||
const disclosureProps = expandAll
|
||||
? {
|
||||
stateKey: createStateKey('openapi-responses-disclosure', context.blockKey),
|
||||
defaultExpandedKeys: groups.map((g) => g.key),
|
||||
}
|
||||
: {
|
||||
expandedKeys: getExpandedResponseKeys(groups, state.key),
|
||||
onExpandedChange: (keys: Set<Key>) => {
|
||||
state.setKey(keys.values().next().value ?? null);
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<StaticSection header={t(context.translation, 'responses')} className="openapi-responses">
|
||||
<OpenAPIDisclosureGroup
|
||||
icon={context.icons.chevronRight}
|
||||
allowsMultipleExpanded={expandAll}
|
||||
expandedKeys={expandedKeys}
|
||||
onExpandedChange={(keys) => {
|
||||
const key = keys.values().next().value ?? null;
|
||||
state.setKey(key);
|
||||
}}
|
||||
groups={groups}
|
||||
selectIcon={context.icons.chevronDown}
|
||||
selectStateKey={createStateKey('response-media-types', context.blockKey)}
|
||||
{...disclosureProps}
|
||||
/>
|
||||
</StaticSection>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the expanded response for the controlled accordion: nothing when the selection is
|
||||
* cleared, otherwise the selected status code, falling back to the first response when this
|
||||
* operation doesn't define it.
|
||||
*/
|
||||
function getExpandedResponseKeys(groups: { key: string }[], selectedKey: Key | null): Set<string> {
|
||||
if (selectedKey == null) {
|
||||
return new Set<string>();
|
||||
}
|
||||
const key = groups.find((g) => g.key === selectedKey)?.key ?? groups[0]?.key;
|
||||
return key ? new Set([key]) : new Set<string>();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user