mirror of
https://github.com/Redocly/redoc.git
synced 2025-08-09 06:34:53 +03:00
Extended search
This commit is contained in:
parent
ffacdc09be
commit
9e6005ff72
|
@ -36,6 +36,8 @@ export class ContentItem extends React.Component<ContentItemProps> {
|
|||
case 'group':
|
||||
content = null;
|
||||
break;
|
||||
case 'field':
|
||||
return null;
|
||||
case 'tag':
|
||||
case 'section':
|
||||
content = <SectionItem {...this.props} />;
|
||||
|
|
|
@ -61,7 +61,7 @@ export class Field extends React.Component<FieldProps> {
|
|||
<>
|
||||
<tr className={isLast ? 'last ' + className : className}>
|
||||
{paramName}
|
||||
<PropertyDetailsCell>
|
||||
<PropertyDetailsCell data-section-id={this.props.field.id}>
|
||||
<FieldDetails {...this.props} />
|
||||
</PropertyDetailsCell>
|
||||
</tr>
|
||||
|
|
|
@ -42,6 +42,9 @@ export class MenuItem extends React.Component<MenuItemProps> {
|
|||
|
||||
render() {
|
||||
const { item, withoutChildren } = this.props;
|
||||
if (item.type === 'field' && this.props['data-role'] !== 'search:result') {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<MenuItemLi
|
||||
onClick={this.activate}
|
||||
|
|
|
@ -6,14 +6,17 @@ exports[`Components SchemaView discriminator should correctly render discriminat
|
|||
<Field
|
||||
field={
|
||||
FieldModel {
|
||||
"active": false,
|
||||
"deprecated": false,
|
||||
"description": "",
|
||||
"example": undefined,
|
||||
"expanded": false,
|
||||
"explode": false,
|
||||
"in": undefined,
|
||||
"items": Array [],
|
||||
"kind": "field",
|
||||
"name": "packSize",
|
||||
"ready": true,
|
||||
"required": false,
|
||||
"schema": SchemaModel {
|
||||
"activeOneOf": 0,
|
||||
|
@ -47,6 +50,7 @@ exports[`Components SchemaView discriminator should correctly render discriminat
|
|||
"typePrefix": "",
|
||||
"writeOnly": false,
|
||||
},
|
||||
"type": "field",
|
||||
}
|
||||
}
|
||||
isLast={false}
|
||||
|
@ -56,14 +60,17 @@ exports[`Components SchemaView discriminator should correctly render discriminat
|
|||
<Field
|
||||
field={
|
||||
FieldModel {
|
||||
"active": false,
|
||||
"deprecated": false,
|
||||
"description": "",
|
||||
"example": undefined,
|
||||
"expanded": false,
|
||||
"explode": false,
|
||||
"in": undefined,
|
||||
"items": Array [],
|
||||
"kind": "field",
|
||||
"name": "type",
|
||||
"ready": true,
|
||||
"required": true,
|
||||
"schema": SchemaModel {
|
||||
"activeOneOf": 0,
|
||||
|
@ -97,6 +104,7 @@ exports[`Components SchemaView discriminator should correctly render discriminat
|
|||
"typePrefix": "",
|
||||
"writeOnly": false,
|
||||
},
|
||||
"type": "field",
|
||||
}
|
||||
}
|
||||
isLast={true}
|
||||
|
|
|
@ -84,7 +84,7 @@ export class AppStore {
|
|||
if (!this.options.disableSearch) {
|
||||
this.search = new SearchStore();
|
||||
if (createSearchIndex) {
|
||||
this.search.indexItems(this.menu.items);
|
||||
this.search.indexItems(this.menu.flatItems);
|
||||
}
|
||||
|
||||
this.disposer = observe(this.menu, 'activeItemIdx', change => {
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import { OpenAPIOperation, OpenAPIParameter, OpenAPISpec, OpenAPITag, Referenced } from '../types';
|
||||
import {
|
||||
isOperationName,
|
||||
isOperationName, safeSlugify,
|
||||
SECURITY_DEFINITIONS_COMPONENT_NAME,
|
||||
setSecuritySchemePrefix,
|
||||
} from '../utils';
|
||||
import { MarkdownRenderer } from './MarkdownRenderer';
|
||||
import { GroupModel, OperationModel } from './models';
|
||||
import { FieldModel, GroupModel, OperationModel } from './models';
|
||||
import { OpenAPIParser } from './OpenAPIParser';
|
||||
import { RedocNormalizedOptions } from './RedocNormalizedOptions';
|
||||
|
||||
|
@ -28,12 +28,13 @@ export interface TagGroup {
|
|||
}
|
||||
|
||||
export const GROUP_DEPTH = 0;
|
||||
export type ContentItemModel = GroupModel | OperationModel;
|
||||
export type ContentItemModel = GroupModel | OperationModel | FieldModel;
|
||||
|
||||
export class MenuBuilder {
|
||||
/**
|
||||
* Builds page content structure based on tags
|
||||
*/
|
||||
|
||||
static buildStructure(
|
||||
parser: OpenAPIParser,
|
||||
options: RedocNormalizedOptions,
|
||||
|
@ -139,7 +140,7 @@ export class MenuBuilder {
|
|||
return tagsMap[tagName];
|
||||
});
|
||||
|
||||
const res: Array<GroupModel | OperationModel> = [];
|
||||
const res: Array<GroupModel | OperationModel | FieldModel> = [];
|
||||
for (const tag of tags) {
|
||||
if (!tag) {
|
||||
continue;
|
||||
|
@ -179,20 +180,144 @@ export class MenuBuilder {
|
|||
tag: TagInfo,
|
||||
depth: number,
|
||||
options: RedocNormalizedOptions,
|
||||
): OperationModel[] {
|
||||
): ContentItemModel[] {
|
||||
if (tag.operations.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const res: OperationModel[] = [];
|
||||
const res: ContentItemModel[] = [];
|
||||
for (const operationInfo of tag.operations) {
|
||||
const operation = new OperationModel(parser, operationInfo, parent, options);
|
||||
operation.depth = depth;
|
||||
res.push(operation);
|
||||
res.push(...this.getOperationFields(operation, depth + 1));
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
static getOperationFields(
|
||||
parent: OperationModel,
|
||||
depth: number,
|
||||
): FieldModel[] {
|
||||
|
||||
const fields: FieldModel[] = [];
|
||||
|
||||
if (parent.parameters !== undefined) {
|
||||
const parameters = parent.parameters;
|
||||
fields.push(...this.getFields(parameters, parent, 'parameters', depth));
|
||||
}
|
||||
|
||||
if (parent.requestBody !== undefined) {
|
||||
const body = parent.requestBody;
|
||||
const bodyFields: FieldModel[] = [];
|
||||
|
||||
if (body.content) {
|
||||
const mediaTypes = body.content.mediaTypes;
|
||||
mediaTypes.forEach(mediaType => {
|
||||
const type = mediaType.name.split('/')[1];
|
||||
const schema = mediaType.schema;
|
||||
if (schema && schema.oneOf) { // One of
|
||||
let active = 0;
|
||||
schema.oneOf.forEach(s => {
|
||||
bodyFields.push(...this.getFields(s.fields, parent, 'body/' + type + '/' + s.title, depth).map(f => {
|
||||
f.containerContentModel = body.content;
|
||||
f.activeContentModel = mediaTypes.indexOf(mediaType);
|
||||
f.containerOneOf = schema;
|
||||
f.activeOneOf = active;
|
||||
return f;
|
||||
}));
|
||||
active++;
|
||||
});
|
||||
} else if (schema && schema.fields) {
|
||||
bodyFields.push(...this.getFields(schema.fields, parent, 'body/' + type, depth).map(f => {
|
||||
f.containerContentModel = body.content;
|
||||
f.activeContentModel = mediaTypes.indexOf(mediaType);
|
||||
return f;
|
||||
}));
|
||||
}
|
||||
});
|
||||
fields.push(...bodyFields);
|
||||
}
|
||||
}
|
||||
|
||||
if (parent.responses !== undefined) {
|
||||
const responses = parent.responses;
|
||||
const responseFields: FieldModel[] = [];
|
||||
|
||||
responses.forEach(response => {
|
||||
responseFields.push(...this.getFields(response.headers, parent, 'responses/' + response.code + '/headers', depth).map(r => {
|
||||
r.responseContainer = response;
|
||||
return r;
|
||||
}));
|
||||
|
||||
if (response.content) {
|
||||
const mediaTypes = response.content.mediaTypes;
|
||||
mediaTypes.forEach(mediaType => {
|
||||
const type = mediaType.name.split('/')[1];
|
||||
const schema = mediaType.schema;
|
||||
if (schema && schema.oneOf) { // One of
|
||||
let active = 0;
|
||||
schema.oneOf.forEach(s => {
|
||||
responseFields.push(...this.getFields(s.fields, parent, 'responses/' + response.code + '/' + type + '/' + s.title, depth).map(f => {
|
||||
f.responseContainer = response;
|
||||
f.containerContentModel = response.content;
|
||||
f.activeContentModel = mediaTypes.indexOf(mediaType);
|
||||
f.containerOneOf = schema;
|
||||
f.activeOneOf = active;
|
||||
return f;
|
||||
}));
|
||||
active++;
|
||||
});
|
||||
} else if (schema && schema.fields) {
|
||||
responseFields.push(...this.getFields(schema.fields, parent, 'responses/' + response.code + '/' + type, depth).map(f => {
|
||||
f.responseContainer = response;
|
||||
f.containerContentModel = response.content;
|
||||
f.activeContentModel = mediaTypes.indexOf(mediaType);
|
||||
return f;
|
||||
}));
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
fields.push(...responseFields);
|
||||
}
|
||||
|
||||
return fields;
|
||||
}
|
||||
|
||||
static getFields(fields, parent, section, depth): FieldModel[] {
|
||||
const temp: FieldModel[] = [];
|
||||
fields.forEach(field => {
|
||||
temp.push(...this.getDeepFields(field, parent, section, depth));
|
||||
});
|
||||
return temp.filter((field, index, self) => {
|
||||
return index === self.findIndex(f => {
|
||||
return f.id === field.id;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
static getDeepFields(field: FieldModel, parent: ContentItemModel, section: string, depth: number): FieldModel[] {
|
||||
const temp: FieldModel[] = [];
|
||||
|
||||
field.id = parent.id.includes(section) ? parent.id + '/' + safeSlugify(field.name) : parent.id + '/' + section + '/' + safeSlugify(field.name);
|
||||
field.parent = parent;
|
||||
temp.push(field);
|
||||
|
||||
if (field.schema.fields !== undefined) {
|
||||
field.schema.fields.forEach(fieldInner => {
|
||||
temp.push(...this.getDeepFields(fieldInner, field, section, depth + 1));
|
||||
});
|
||||
}
|
||||
if (field.schema.items !== undefined && field.schema.items.fields !== undefined) {
|
||||
field.schema.items.fields.forEach(fieldInner => {
|
||||
temp.push(...this.getDeepFields(fieldInner, field, section, depth + 1));
|
||||
});
|
||||
}
|
||||
|
||||
return temp;
|
||||
}
|
||||
|
||||
/**
|
||||
* collects tags and maps each tag to list of operations belonging to this tag
|
||||
*/
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { action, observable } from 'mobx';
|
||||
import { querySelector } from '../utils/dom';
|
||||
import { SpecStore } from './models';
|
||||
import { FieldModel, SpecStore } from './models';
|
||||
|
||||
import { history as historyInst, HistoryService } from './HistoryService';
|
||||
import { ScrollService } from './ScrollService';
|
||||
|
@ -9,7 +9,7 @@ import { flattenByProp, SECURITY_SCHEMES_SECTION_PREFIX } from '../utils';
|
|||
import { GROUP_DEPTH } from './MenuBuilder';
|
||||
|
||||
export type MenuItemGroupType = 'group' | 'tag' | 'section';
|
||||
export type MenuItemType = MenuItemGroupType | 'operation';
|
||||
export type MenuItemType = MenuItemGroupType | 'operation' | 'field';
|
||||
|
||||
/** Generic interface for MenuItems */
|
||||
export interface IMenuItem {
|
||||
|
@ -64,6 +64,8 @@ export class MenuStore {
|
|||
items: IMenuItem[];
|
||||
flatItems: IMenuItem[];
|
||||
|
||||
imagesLoaded: boolean = false;
|
||||
|
||||
/**
|
||||
* cached flattened menu items to support absolute indexing
|
||||
*/
|
||||
|
@ -129,7 +131,16 @@ export class MenuStore {
|
|||
itemIdx += step;
|
||||
}
|
||||
|
||||
this.activate(this.flatItems[itemIdx], true, true);
|
||||
let item: FieldModel | undefined = this.flatItems[itemIdx] as FieldModel;
|
||||
while (item && item.type === 'field' && item.parent && item.parent.type === 'field' && !item.parent.expanded ||
|
||||
item && item.containerContentModel !== undefined && item.containerContentModel.activeMimeIdx !== item.activeContentModel ||
|
||||
item && item.responseContainer && !item.responseContainer.expanded ||
|
||||
item && item.containerOneOf !== undefined && item.containerOneOf.activeOneOf !== item.activeOneOf) {
|
||||
itemIdx += step;
|
||||
item = this.flatItems[itemIdx] as FieldModel;
|
||||
}
|
||||
|
||||
this.activate(item, true, true);
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -142,6 +153,12 @@ export class MenuStore {
|
|||
}
|
||||
let item: IMenuItem | undefined;
|
||||
|
||||
// Make jumps possible even if last char in URL is '/'
|
||||
if (id[id.length - 1] === '/') {
|
||||
id = id.slice(0, -1);
|
||||
this.history.replace(id, false);
|
||||
}
|
||||
|
||||
item = this.flatItems.find(i => i.id === id);
|
||||
if (item) {
|
||||
this.activateAndScroll(item, false);
|
||||
|
@ -178,7 +195,7 @@ export class MenuStore {
|
|||
* activate menu item
|
||||
* @param item item to activate
|
||||
* @param updateLocation [true] whether to update location
|
||||
* @param rewriteHistory [false] whether to rewrite browser history (do not create new enrty)
|
||||
* @param rewriteHistory [false] whether to rewrite browser history (do not create new entry)
|
||||
*/
|
||||
@action
|
||||
activate(
|
||||
|
@ -220,7 +237,9 @@ export class MenuStore {
|
|||
}
|
||||
item.deactivate();
|
||||
while (item !== undefined) {
|
||||
item.collapse();
|
||||
if (item.type !== 'field') {
|
||||
item.collapse();
|
||||
}
|
||||
item = item.parent;
|
||||
}
|
||||
}
|
||||
|
@ -248,7 +267,17 @@ export class MenuStore {
|
|||
* scrolls to active section
|
||||
*/
|
||||
scrollToActive(): void {
|
||||
this.scroll.scrollIntoView(this.getElementAt(this.activeItemIdx));
|
||||
const active = this.activeItemIdx;
|
||||
const element = this.getElementAt(active);
|
||||
if (element === null || !this.imagesLoaded) {
|
||||
setTimeout(() => {
|
||||
this.activeItemIdx = active;
|
||||
this.scrollToActive();
|
||||
this.imagesLoaded = true;
|
||||
}, this.imagesLoaded ? 500 : 100);
|
||||
} else {
|
||||
this.scroll.scrollIntoView(element);
|
||||
}
|
||||
}
|
||||
|
||||
dispose() {
|
||||
|
|
|
@ -21,16 +21,13 @@ export class SearchStore<T> {
|
|||
searchWorker = new worker();
|
||||
|
||||
indexItems(groups: Array<IMenuItem | OperationModel>) {
|
||||
const recurse = items => {
|
||||
items.forEach(group => {
|
||||
if (group.type !== 'group') {
|
||||
this.add(group.name, group.description || '', group.id);
|
||||
}
|
||||
recurse(group.items);
|
||||
});
|
||||
};
|
||||
groups.forEach(group => {
|
||||
if (group.type !== 'group') {
|
||||
// @ts-ignore
|
||||
this.add(group.name, group.description || '', group.id);
|
||||
}
|
||||
});
|
||||
|
||||
recurse(groups);
|
||||
this.searchWorker.done();
|
||||
}
|
||||
|
||||
|
|
|
@ -6,10 +6,13 @@ import {
|
|||
OpenAPIParameterStyle,
|
||||
Referenced,
|
||||
} from '../../types';
|
||||
import { IMenuItem } from '../MenuStore';
|
||||
import { RedocNormalizedOptions } from '../RedocNormalizedOptions';
|
||||
|
||||
import { extractExtensions } from '../../utils/openapi';
|
||||
import { OpenAPIParser } from '../OpenAPIParser';
|
||||
import { MediaContentModel } from './MediaContent';
|
||||
import { ResponseModel } from './Response';
|
||||
import { SchemaModel } from './Schema';
|
||||
|
||||
function getDefaultStyleValue(parameterLocation: OpenAPIParameterLocation): OpenAPIParameterStyle {
|
||||
|
@ -28,10 +31,28 @@ function getDefaultStyleValue(parameterLocation: OpenAPIParameterLocation): Open
|
|||
/**
|
||||
* Field or Parameter model ready to be used by components
|
||||
*/
|
||||
export class FieldModel {
|
||||
export class FieldModel implements IMenuItem {
|
||||
@observable
|
||||
expanded: boolean = false;
|
||||
|
||||
depth: number;
|
||||
items = [];
|
||||
|
||||
ready?: boolean = true;
|
||||
active: boolean = false;
|
||||
|
||||
id: string;
|
||||
absoluteIdx?: number;
|
||||
parent?: IMenuItem;
|
||||
|
||||
containerContentModel?: MediaContentModel;
|
||||
containerOneOf?: SchemaModel;
|
||||
activeContentModel?: number;
|
||||
activeOneOf?: number;
|
||||
responseContainer?: ResponseModel;
|
||||
|
||||
type = 'field' as 'field';
|
||||
|
||||
schema: SchemaModel;
|
||||
name: string;
|
||||
required: boolean;
|
||||
|
@ -92,4 +113,41 @@ export class FieldModel {
|
|||
toggle() {
|
||||
this.expanded = !this.expanded;
|
||||
}
|
||||
|
||||
@action
|
||||
activate() {
|
||||
if (this.parent) {
|
||||
this.parent.activate();
|
||||
if (this.responseContainer !== undefined) {
|
||||
this.responseContainer.expand();
|
||||
}
|
||||
if (this.containerContentModel !== undefined && this.activeContentModel !== undefined) {
|
||||
this.containerContentModel.activate(this.activeContentModel);
|
||||
}
|
||||
if (this.containerOneOf !== undefined && this.activeOneOf !== undefined) {
|
||||
this.containerOneOf.activateOneOf(this.activeOneOf);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
deactivate() {
|
||||
if (this.parent) {
|
||||
this.parent.deactivate();
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
expand() {
|
||||
if (this.parent) {
|
||||
if (this.parent.type === 'field') {
|
||||
this.parent.expanded = true;
|
||||
}
|
||||
this.parent.expand();
|
||||
}
|
||||
}
|
||||
|
||||
collapse() {
|
||||
// Do nothing
|
||||
}
|
||||
}
|
||||
|
|
|
@ -126,6 +126,7 @@ export class OperationModel implements IMenuItem {
|
|||
this.active = false;
|
||||
}
|
||||
|
||||
@action
|
||||
expand() {
|
||||
if (this.parent) {
|
||||
this.parent.expand();
|
||||
|
|
|
@ -58,4 +58,8 @@ export class ResponseModel {
|
|||
toggle() {
|
||||
this.expanded = !this.expanded;
|
||||
}
|
||||
|
||||
expand() {
|
||||
this.expanded = true;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user