feat: allow for simple collapsible groups

This commit is contained in:
Braden Napier 2019-03-18 20:48:23 -07:00
parent 2f65f051e0
commit cc781aba91
9 changed files with 213 additions and 151 deletions

View File

@ -17,14 +17,14 @@ info:
It was **extended** to illustrate features of [generator-openapi-repo](https://github.com/Rebilly/generator-openapi-repo)
tool and [ReDoc](https://github.com/Rebilly/ReDoc) documentation. In addition to standard
OpenAPI syntax we use a few [vendor extensions](https://github.com/Rebilly/ReDoc/blob/master/docs/redoc-vendor-extensions.md).
# OpenAPI Specification
This API is documented in **OpenAPI format** and is based on
[Petstore sample](http://petstore.swagger.io/) provided by [swagger.io](http://swagger.io) team.
It was **extended** to illustrate features of [generator-openapi-repo](https://github.com/Rebilly/generator-openapi-repo)
tool and [ReDoc](https://github.com/Rebilly/ReDoc) documentation. In addition to standard
OpenAPI syntax we use a few [vendor extensions](https://github.com/Rebilly/ReDoc/blob/master/docs/redoc-vendor-extensions.md).
# Cross-Origin Resource Sharing
This API features Cross-Origin Resource Sharing (CORS) implemented in compliance with [W3C spec](https://www.w3.org/TR/cors/).
And that allows cross-domain communication from the browser.
@ -38,24 +38,24 @@ info:
OAuth2 - an open protocol to allow secure authorization in a simple
and standard method from web, mobile and desktop applications.
<security-definitions />
<security-definitions />
version: 1.0.0
title: Swagger Petstore
termsOfService: 'http://swagger.io/terms/'
termsOfService: "http://swagger.io/terms/"
contact:
name: API Support
email: apiteam@swagger.io
url: https://github.com/Rebilly/ReDoc
x-logo:
url: 'https://rebilly.github.io/ReDoc/petstore-logo.png'
url: "https://rebilly.github.io/ReDoc/petstore-logo.png"
altText: Petstore logo
license:
name: Apache 2.0
url: 'http://www.apache.org/licenses/LICENSE-2.0.html'
url: "http://www.apache.org/licenses/LICENSE-2.0.html"
externalDocs:
description: Find out how to create Github repo for your OpenAPI spec.
url: 'https://github.com/Rebilly/generator-openapi-repo'
url: "https://github.com/Rebilly/generator-openapi-repo"
tags:
- name: pet
description: Everything about your Pets
@ -63,14 +63,26 @@ tags:
description: Access to Petstore orders
- name: user
description: Operations about user
- name: SubItem One
description: |
SubItem One Description!
- name: SubItem Two
description: |
SubItem Two Description!
x-tagGroups:
- name: General
tags:
- pet
- store
- name: User Management
- name: Collapsible Group
collapsible: true
tags:
- user
- SubItem One
- SubItem Two
# - name: User Management
# tags:
# - user
paths:
/pet:
parameters:
@ -88,14 +100,14 @@ paths:
description: Add new pet to the store inventory.
operationId: addPet
responses:
'405':
"405":
description: Invalid input
security:
- petstore_auth:
- 'write:pets'
- 'read:pets'
- "write:pets"
- "read:pets"
x-code-samples:
- lang: 'C#'
- lang: "C#"
source: |
PetStore.v1.Pet pet = new PetStore.v1.Pet();
pet.setApiKey("your api key");
@ -124,24 +136,24 @@ paths:
var_dump($e->getErrors());
}
requestBody:
$ref: '#/components/requestBodies/Pet'
$ref: "#/components/requestBodies/Pet"
put:
tags:
- pet
summary: Update an existing pet
description: ''
description: ""
operationId: updatePet
responses:
'400':
"400":
description: Invalid ID supplied
'404':
"404":
description: Pet not found
'405':
"405":
description: Validation exception
security:
- petstore_auth:
- 'write:pets'
- 'read:pets'
- "write:pets"
- "read:pets"
x-code-samples:
- lang: PHP
source: |
@ -156,8 +168,8 @@ paths:
var_dump($e->getErrors());
}
requestBody:
$ref: '#/components/requestBodies/Pet'
'/pet/{petId}':
$ref: "#/components/requestBodies/Pet"
"/pet/{petId}":
get:
tags:
- pet
@ -174,18 +186,18 @@ paths:
type: integer
format: int64
responses:
'200':
"200":
description: successful operation
content:
application/json:
schema:
$ref: '#/components/schemas/Pet'
$ref: "#/components/schemas/Pet"
application/xml:
schema:
$ref: '#/components/schemas/Pet'
'400':
$ref: "#/components/schemas/Pet"
"400":
description: Invalid ID supplied
'404':
"404":
description: Pet not found
security:
- api_key: []
@ -193,7 +205,7 @@ paths:
tags:
- pet
summary: Updates a pet in the store with form data
description: ''
description: ""
operationId: updatePetWithForm
parameters:
- name: petId
@ -204,12 +216,12 @@ paths:
type: integer
format: int64
responses:
'405':
"405":
description: Invalid input
security:
- petstore_auth:
- 'write:pets'
- 'read:pets'
- "write:pets"
- "read:pets"
requestBody:
content:
application/x-www-form-urlencoded:
@ -226,7 +238,7 @@ paths:
tags:
- pet
summary: Deletes a pet
description: ''
description: ""
operationId: deletePet
parameters:
- name: api_key
@ -243,18 +255,18 @@ paths:
type: integer
format: int64
responses:
'400':
"400":
description: Invalid pet value
security:
- petstore_auth:
- 'write:pets'
- 'read:pets'
'/pet/{petId}/uploadImage':
- "write:pets"
- "read:pets"
"/pet/{petId}/uploadImage":
post:
tags:
- pet
summary: uploads an image
description: ''
description: ""
operationId: uploadFile
parameters:
- name: petId
@ -265,16 +277,16 @@ paths:
type: integer
format: int64
responses:
'200':
"200":
description: successful operation
content:
application/json:
schema:
$ref: '#/components/schemas/ApiResponse'
$ref: "#/components/schemas/ApiResponse"
security:
- petstore_auth:
- 'write:pets'
- 'read:pets'
- "write:pets"
- "read:pets"
requestBody:
content:
application/octet-stream:
@ -306,25 +318,25 @@ paths:
- sold
default: available
responses:
'200':
"200":
description: successful operation
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Pet'
$ref: "#/components/schemas/Pet"
application/xml:
schema:
type: array
items:
$ref: '#/components/schemas/Pet'
'400':
$ref: "#/components/schemas/Pet"
"400":
description: Invalid status value
security:
- petstore_auth:
- 'write:pets'
- 'read:pets'
- "write:pets"
- "read:pets"
/pet/findByTags:
get:
tags:
@ -346,25 +358,25 @@ paths:
items:
type: string
responses:
'200':
"200":
description: successful operation
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Pet'
$ref: "#/components/schemas/Pet"
application/xml:
schema:
type: array
items:
$ref: '#/components/schemas/Pet'
'400':
$ref: "#/components/schemas/Pet"
"400":
description: Invalid tag value
security:
- petstore_auth:
- 'write:pets'
- 'read:pets'
- "write:pets"
- "read:pets"
/store/inventory:
get:
tags:
@ -373,7 +385,7 @@ paths:
description: Returns a map of status codes to quantities
operationId: getInventory
responses:
'200':
"200":
description: successful operation
content:
application/json:
@ -389,19 +401,19 @@ paths:
tags:
- store
summary: Place an order for a pet
description: ''
description: ""
operationId: placeOrder
responses:
'200':
"200":
description: successful operation
content:
application/json:
schema:
$ref: '#/components/schemas/Order'
$ref: "#/components/schemas/Order"
application/xml:
schema:
$ref: '#/components/schemas/Order'
'400':
$ref: "#/components/schemas/Order"
"400":
description: Invalid Order
content:
application/json:
@ -412,10 +424,10 @@ paths:
content:
application/json:
schema:
$ref: '#/components/schemas/Order'
$ref: "#/components/schemas/Order"
description: order placed for purchasing the pet
required: true
'/store/order/{orderId}':
"/store/order/{orderId}":
get:
tags:
- store
@ -435,18 +447,18 @@ paths:
minimum: 1
maximum: 5
responses:
'200':
"200":
description: successful operation
content:
application/json:
schema:
$ref: '#/components/schemas/Order'
$ref: "#/components/schemas/Order"
application/xml:
schema:
$ref: '#/components/schemas/Order'
'400':
$ref: "#/components/schemas/Order"
"400":
description: Invalid ID supplied
'404':
"404":
description: Order not found
delete:
tags:
@ -465,9 +477,9 @@ paths:
type: string
minimum: 1
responses:
'400':
"400":
description: Invalid ID supplied
'404':
"404":
description: Order not found
/user:
post:
@ -483,36 +495,36 @@ paths:
content:
application/json:
schema:
$ref: '#/components/schemas/User'
$ref: "#/components/schemas/User"
description: Created user object
required: true
'/user/{username}':
"/user/{username}":
get:
tags:
- user
summary: Get user by user name
description: ''
description: ""
operationId: getUserByName
parameters:
- name: username
in: path
description: 'The name that needs to be fetched. Use user1 for testing. '
description: "The name that needs to be fetched. Use user1 for testing. "
required: true
schema:
type: string
responses:
'200':
"200":
description: successful operation
content:
application/json:
schema:
$ref: '#/components/schemas/User'
$ref: "#/components/schemas/User"
application/xml:
schema:
$ref: '#/components/schemas/User'
'400':
$ref: "#/components/schemas/User"
"400":
description: Invalid username supplied
'404':
"404":
description: User not found
put:
tags:
@ -528,15 +540,15 @@ paths:
schema:
type: string
responses:
'400':
"400":
description: Invalid user supplied
'404':
"404":
description: User not found
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/User'
$ref: "#/components/schemas/User"
description: Updated user object
required: true
delete:
@ -553,40 +565,40 @@ paths:
schema:
type: string
responses:
'400':
"400":
description: Invalid username supplied
'404':
"404":
description: User not found
/user/createWithArray:
post:
tags:
- user
summary: Creates list of users with given input array
description: ''
description: ""
operationId: createUsersWithArrayInput
responses:
default:
description: successful operation
requestBody:
$ref: '#/components/requestBodies/UserArray'
$ref: "#/components/requestBodies/UserArray"
/user/createWithList:
post:
tags:
- user
summary: Creates list of users with given input array
description: ''
description: ""
operationId: createUsersWithListInput
responses:
default:
description: successful operation
requestBody:
$ref: '#/components/requestBodies/UserArray'
$ref: "#/components/requestBodies/UserArray"
/user/login:
get:
tags:
- user
summary: Logs user into the system
description: ''
description: ""
operationId: loginUser
parameters:
- name: username
@ -602,7 +614,7 @@ paths:
schema:
type: string
responses:
'200':
"200":
description: successful operation
headers:
X-Rate-Limit:
@ -627,19 +639,19 @@ paths:
type: string
examples:
response:
value: <Message> OK </Message>
value: <Message> OK </Message>
text/plain:
examples:
response:
value: OK
'400':
value: OK
"400":
description: Invalid username/password supplied
/user/logout:
get:
tags:
- user
summary: Logs out current logged in user session
description: ''
description: ""
operationId: logoutUser
responses:
default:
@ -659,7 +671,7 @@ components:
Cat:
description: A representation of a cat
allOf:
- $ref: '#/components/schemas/Pet'
- $ref: "#/components/schemas/Pet"
- type: object
properties:
huntingSkill:
@ -679,7 +691,7 @@ components:
id:
description: Category ID
allOf:
- $ref: '#/components/schemas/Id'
- $ref: "#/components/schemas/Id"
name:
description: Category name
type: string
@ -696,7 +708,7 @@ components:
Dog:
description: A representation of a dog
allOf:
- $ref: '#/components/schemas/Pet'
- $ref: "#/components/schemas/Pet"
- type: object
properties:
packSize:
@ -710,7 +722,7 @@ components:
HoneyBee:
description: A representation of a honey bee
allOf:
- $ref: '#/components/schemas/Pet'
- $ref: "#/components/schemas/Pet"
- type: object
properties:
honeyPerDay:
@ -729,11 +741,11 @@ components:
id:
description: Order ID
allOf:
- $ref: '#/components/schemas/Id'
- $ref: "#/components/schemas/Id"
petId:
description: Pet ID
allOf:
- $ref: '#/components/schemas/Id'
- $ref: "#/components/schemas/Id"
quantity:
type: integer
format: int32
@ -764,9 +776,9 @@ components:
discriminator:
propertyName: petType
mapping:
cat: '#/components/schemas/Cat'
dog: '#/components/schemas/Dog'
bee: '#/components/schemas/HoneyBee'
cat: "#/components/schemas/Cat"
dog: "#/components/schemas/Dog"
bee: "#/components/schemas/HoneyBee"
properties:
id:
externalDocs:
@ -774,11 +786,11 @@ components:
url: "https://example.com"
description: Pet ID
allOf:
- $ref: '#/components/schemas/Id'
- $ref: "#/components/schemas/Id"
category:
description: Categories this pet belongs to
allOf:
- $ref: '#/components/schemas/Category'
- $ref: "#/components/schemas/Category"
name:
description: The name given to a pet
type: string
@ -795,7 +807,7 @@ components:
format: url
friend:
allOf:
- $ref: '#/components/schemas/Pet'
- $ref: "#/components/schemas/Pet"
tags:
description: Tags attached to the pet
type: array
@ -804,7 +816,7 @@ components:
name: tag
wrapped: true
items:
$ref: '#/components/schemas/Tag'
$ref: "#/components/schemas/Tag"
status:
type: string
description: Pet status in the store
@ -823,7 +835,7 @@ components:
id:
description: Tag ID
allOf:
- $ref: '#/components/schemas/Id'
- $ref: "#/components/schemas/Id"
name:
description: Tag name
type: string
@ -834,11 +846,11 @@ components:
type: object
properties:
id:
$ref: '#/components/schemas/Id'
$ref: "#/components/schemas/Id"
pet:
oneOf:
- $ref: '#/components/schemas/Pet'
- $ref: '#/components/schemas/Tag'
- $ref: "#/components/schemas/Pet"
- $ref: "#/components/schemas/Tag"
username:
description: User supplied username
type: string
@ -866,7 +878,7 @@ components:
as well as digits
format: password
minLength: 8
pattern: '(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])'
pattern: "(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])"
example: drowssaP123
phone:
description: User phone number in international format
@ -888,10 +900,10 @@ components:
allOf:
- description: My Pet
title: Pettie
- $ref: '#/components/schemas/Pet'
- $ref: "#/components/schemas/Pet"
application/xml:
schema:
type: 'object'
type: "object"
properties:
name:
type: string
@ -904,7 +916,7 @@ components:
schema:
type: array
items:
$ref: '#/components/schemas/User'
$ref: "#/components/schemas/User"
description: List of user object
required: true
securitySchemes:
@ -915,10 +927,10 @@ components:
type: oauth2
flows:
implicit:
authorizationUrl: 'http://petstore.swagger.io/api/oauth/dialog'
authorizationUrl: "http://petstore.swagger.io/api/oauth/dialog"
scopes:
'write:pets': modify pets in your account
'read:pets': read your pets
"write:pets": modify pets in your account
"read:pets": read your pets
api_key:
description: >
For this sample, you can use the api key `special-key` to test the

View File

@ -22,7 +22,8 @@ Information about tags group
| Field Name | Type | Description |
| :---------- | :--------: | :---------- |
| name | string | The group name |
| tags | [ string ] | List of tags to include in this group
| collapsible | boolean | Makes the group collapsible in the menu when true |
| tags | [ string ] | List of tags to include in this group |
###### x-tagGroups example
json
@ -35,6 +36,7 @@ json
},
{
"name": "Statistics",
"collapsible": true,
"tags": ["Main Stats", "Secondary Stats"]
}
]

View File

@ -52,27 +52,29 @@ export class MenuItem extends React.Component<MenuItemProps> {
{item.type === 'operation' ? (
<OperationMenuItemContent {...this.props} item={item as OperationModel} />
) : (
<MenuItemLabel depth={item.depth} active={item.active} type={item.type}>
<MenuItemLabel
depth={item.depth}
active={item.active}
type={item.type}
collapsible={item.collapsible}
>
<MenuItemTitle title={item.name}>
{item.name}
{this.props.children}
</MenuItemTitle>
{(item.depth > 0 &&
item.items.length > 0 && (
<ShelfIcon float={'right'} direction={item.expanded ? 'down' : 'right'} />
)) ||
{(item.collapsible === true && item.items.length > 0 && (
<ShelfIcon float={'right'} direction={item.expanded ? 'down' : 'right'} />
)) ||
null}
</MenuItemLabel>
)}
{!withoutChildren &&
item.items &&
item.items.length > 0 && (
<MenuItems
expanded={item.expanded}
items={item.items}
onActivate={this.props.onActivate}
/>
)}
{!withoutChildren && item.items && item.items.length > 0 && (
<MenuItems
expanded={item.expanded}
items={item.items}
onActivate={this.props.onActivate}
/>
)}
</MenuItemLi>
);
}
@ -87,7 +89,12 @@ class OperationMenuItemContent extends React.Component<OperationMenuItemContentP
render() {
const { item } = this.props;
return (
<MenuItemLabel depth={item.depth} active={item.active} deprecated={item.deprecated}>
<MenuItemLabel
type={item.type}
depth={item.depth}
active={item.active}
deprecated={item.deprecated}
>
<OperationBadge type={item.httpVerb}>{shortenHTTPVerb(item.httpVerb)}</OperationBadge>
<MenuItemTitle width="calc(100% - 38px)">
{item.name}

View File

@ -32,7 +32,11 @@ export class SideMenu extends React.Component<{ menu: MenuStore; className?: str
}
activate = (item: IMenuItem) => {
this.props.menu.activateAndScroll(item, true);
if (item.expanded === true && item.items.length > 0) {
this.props.menu.collapse(item);
} else {
this.props.menu.activateAndScroll(item, true);
}
setTimeout(() => {
if (this._updateScroll) {
this._updateScroll();

View File

@ -116,6 +116,8 @@ export interface MenuItemLabelType {
depth: number;
active: boolean;
deprecated?: boolean;
collapsible?: boolean;
type?: string;
}
@ -128,9 +130,12 @@ export const MenuItemLabel = styled.label.attrs((props: MenuItemLabelType) => ({
cursor: pointer;
color: ${props => (props.active ? props.theme.colors.primary.main : props.theme.menu.textColor)};
margin: 0;
margin-left: ${({ type, depth }) => (type === 'operation' && depth > 1 ? `${depth * 5}px` : '')};
padding: 12.5px ${props => props.theme.spacing.unit * 4}px;
${({ depth, type, theme }) =>
(type === 'section' && depth > 1 && 'padding-left: ' + theme.spacing.unit * 8 + 'px;') || ''}
${({ depth, type, collapsible, theme }) =>
(((type === 'section' && depth > 1) || (type === 'tag' && collapsible === true)) &&
'padding-left: ' + theme.spacing.unit * 8 + 'px;') ||
''}
display: flex;
justify-content: space-between;
font-family: ${props => props.theme.typography.headings.fontFamily};

View File

@ -99,6 +99,7 @@ export class MenuBuilder {
const res: GroupModel[] = [];
for (const group of groups) {
const item = new GroupModel('group', group, parent);
// item.depth = item.collapsible ? 1 : GROUP_DEPTH;
item.depth = GROUP_DEPTH;
item.items = MenuBuilder.getTagsItems(parser, tags, item, group, options);
res.push(item);
@ -143,7 +144,7 @@ export class MenuBuilder {
continue;
}
const item = new GroupModel('tag', tag, parent);
item.depth = GROUP_DEPTH + 1;
item.depth = ((item.parent && item.parent.depth) || GROUP_DEPTH) + 1;
item.items = this.getOperationsItems(parser, item, tag, item.depth + 1, options);
// don't put empty tag into content, instead put its operations

View File

@ -6,7 +6,7 @@ import { history as historyInst, HistoryService } from './HistoryService';
import { ScrollService } from './ScrollService';
import { flattenByProp, SECURITY_SCHEMES_SECTION_PREFIX } from '../utils';
import { GROUP_DEPTH } from './MenuBuilder';
// import { GROUP_DEPTH } from './MenuBuilder';
export type MenuItemGroupType = 'group' | 'tag' | 'section';
export type MenuItemType = MenuItemGroupType | 'operation';
@ -22,6 +22,9 @@ export interface IMenuItem {
expanded: boolean;
items: IMenuItem[];
parent?: IMenuItem;
collapsible?: boolean;
deprecated?: boolean;
type: MenuItemType;
@ -128,7 +131,6 @@ export class MenuStore {
}
itemIdx += step;
}
this.activate(this.flatItems[itemIdx], true, true);
};
@ -160,6 +162,12 @@ export class MenuStore {
*/
getElementAt(idx: number): Element | null {
const item = this.flatItems[idx];
if (item.type === 'group') {
// group attempts to return first child - likely requires a smarter
// search function for highly-nested values, however at this time this
// should be enough.
return this.getElementAt(item.items[0].absoluteIdx || -1);
}
return (item && querySelector(`[${SECTION_ATTR}="${item.id}"]`)) || null;
}
@ -190,24 +198,39 @@ export class MenuStore {
return;
}
this.deactivate(this.activeItem);
if (!item) {
if (!item || item.collapsible === false) {
this.history.replace('', rewriteHistory);
this.activeItemIdx = -1;
return;
}
// do not allow activating group items
// TODO: control over options
if (item.depth <= GROUP_DEPTH) {
return;
}
this.activeItemIdx = item.absoluteIdx || -1;
this.activeItemIdx = item.absoluteIdx!;
if (updateLocation) {
this.history.replace(item.id, rewriteHistory);
}
item.activate();
item.expand();
if (item.type === 'group') {
this.activate(item.items[0], updateLocation, rewriteHistory);
} else {
item.activate();
item.expand();
}
}
collapse(item: IMenuItem) {
this.activate(item.parent, true, true);
const activeItem = this.activeItem;
if (!activeItem) {
if (item.items.length > 0) {
this.scroll.scrollIntoView(this.getElementAt(item.absoluteIdx || -1));
} else if (item.parent && item.parent.type === 'group') {
this.scroll.scrollIntoView(this.getElementAt(item.parent.absoluteIdx || -1));
}
} else {
this.scroll.scrollIntoView(this.getElementAt(activeItem.absoluteIdx || -1));
}
}
/**

View File

@ -19,6 +19,8 @@ export class GroupModel implements IMenuItem {
items: ContentItemModel[] = [];
parent?: GroupModel;
collapsible: boolean = true;
externalDocs?: OpenAPIExternalDocumentation;
@observable
@ -43,10 +45,14 @@ export class GroupModel implements IMenuItem {
this.description = tagOrGroup.description || '';
this.parent = parent;
this.externalDocs = (tagOrGroup as OpenAPITag).externalDocs;
// groups are active (expanded) by default
const isCollapsible = (tagOrGroup as OpenAPITag).collapsible;
if (isCollapsible != null) {
this.collapsible = Boolean(isCollapsible);
} else if (this.type === 'group') {
this.collapsible = false;
}
if (this.type === 'group') {
this.expanded = true;
this.expanded = this.collapsible ? false : true;
}
}
@ -66,7 +72,7 @@ export class GroupModel implements IMenuItem {
@action
collapse() {
// disallow collapsing groups
if (this.type === 'group') {
if (this.collapsible === false) {
return;
}
this.expanded = false;

View File

@ -248,6 +248,8 @@ export interface OpenAPISecurityScheme {
export interface OpenAPITag {
name: string;
collapsible?: boolean;
description?: string;
externalDocs?: OpenAPIExternalDocumentation;
'x-displayName'?: string;