Skip to content

Commit

Permalink
feat: refactored operation tags for hierarchy
Browse files Browse the repository at this point in the history
  • Loading branch information
kael-shipman committed Mar 5, 2021
1 parent db830fb commit 5536916
Show file tree
Hide file tree
Showing 2 changed files with 203 additions and 84 deletions.
128 changes: 110 additions & 18 deletions src/core/components/operation-tag.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ import { createDeepLinkPath, escapeDeepLinkPath, sanitizeUrl } from "core/utils"
import { buildUrl } from "core/utils/url"
import { isFunc } from "core/utils"

const SWAGGER2_OPERATION_METHODS = [
"get", "put", "post", "delete", "options", "head", "patch"
]

const OAS3_OPERATION_METHODS = SWAGGER2_OPERATION_METHODS.concat(["trace"])

export default class OperationTag extends React.Component {

static defaultProps = {
Expand All @@ -14,8 +20,10 @@ export default class OperationTag extends React.Component {
}

static propTypes = {
tagObj: ImPropTypes.map.isRequired,
tag: PropTypes.string.isRequired,
tagObj: ImPropTypes.map,
tag: PropTypes.string,
childTags: ImPropTypes.map.isRequired,
isRoot: PropTypes.bool,

oas3Selectors: PropTypes.func.isRequired,
layoutSelectors: PropTypes.object.isRequired,
Expand All @@ -25,15 +33,26 @@ export default class OperationTag extends React.Component {
getComponent: PropTypes.func.isRequired,

specUrl: PropTypes.string.isRequired,
}

children: PropTypes.element,
constructor(props) {
super(props);
this.render = this.render.bind(this);
this.renderChildTags = this.renderChildTags.bind(this);
}

render() {
// If this is the root element, just render the child tags
if (this.props.isRoot) {
return this.renderChildTags();
}

// Otherwise, we're rendering the individual elements, so proceed with full render

// Get the necessary props
const {
tagObj,
tag,
children,
oas3Selectors,
layoutSelectors,
layoutActions,
Expand All @@ -42,31 +61,33 @@ export default class OperationTag extends React.Component {
specUrl,
} = this.props

// Get the necessary configs
let {
docExpansion,
deepLinking,
} = getConfigs()

const isDeepLinkingEnabled = deepLinking && deepLinking !== "false"

// Get the necessary components
const Collapse = getComponent("Collapse")
const Markdown = getComponent("Markdown", true)
const DeepLink = getComponent("DeepLink")
const Link = getComponent("Link")

let tagDescription = tagObj.getIn(["tagDetails", "description"], null)
let tagExternalDocsDescription = tagObj.getIn(["tagDetails", "externalDocs", "description"])
let rawTagExternalDocsUrl = tagObj.getIn(["tagDetails", "externalDocs", "url"])
let tagExternalDocsUrl
if (isFunc(oas3Selectors) && isFunc(oas3Selectors.selectedServer)) {
tagExternalDocsUrl = buildUrl( rawTagExternalDocsUrl, specUrl, { selectedServer: oas3Selectors.selectedServer() } )
} else {
tagExternalDocsUrl = rawTagExternalDocsUrl
}
// Set up some helpers
const isDeepLinkingEnabled = deepLinking && deepLinking !== "false"

let isShownKey = ["operations-tag", tag]
let showTag = layoutSelectors.isShown(isShownKey, docExpansion === "full" || docExpansion === "list")
const tagDescription = tagObj ? tagObj.getIn(["tagDetails", "description"], null) : null;
const tagExternalDocsDescription = tagObj ? tagObj.getIn(["tagDetails", "externalDocs", "description"]) : null;
const rawTagExternalDocsUrl = tagObj ? tagObj.getIn(["tagDetails", "externalDocs", "url"]) : null;
const tagExternalDocsUrl = (isFunc(oas3Selectors) && isFunc(oas3Selectors.selectedServer))
? buildUrl(rawTagExternalDocsUrl, specUrl, { selectedServer: oas3Selectors.selectedServer() })
: rawTagExternalDocsUrl;
const operations = tagObj ? tagObj.get("operations") : Im.fromJS({});

const isShownKey = ["operations-tag", tag]
const showTag = layoutSelectors.isShown(isShownKey, docExpansion === "full" || docExpansion === "list")

// Finally, render
return (
<div className={showTag ? "opblock-tag-section is-open" : "opblock-tag-section"} >

Expand Down Expand Up @@ -116,9 +137,80 @@ export default class OperationTag extends React.Component {
</h4>

<Collapse isOpened={showTag}>
{children}
<div class="hierarchical-operation-tag-operations">
{
operations.map(op => {
const path = op.get("path")
const method = op.get("method")
const specPath = Im.List(["paths", path, method])


// FIXME: (someday) this logic should probably be in a selector,
// but doing so would require further opening up
// selectors to the plugin system, to allow for dynamic
// overriding of low-level selectors that other selectors
// rely on. --KS, 12/17
const validMethods = specSelectors.isOAS3() ?
OAS3_OPERATION_METHODS : SWAGGER2_OPERATION_METHODS

if(validMethods.indexOf(method) === -1) {
return null
}

return <OperationContainer
key={`${method}-${path}`}
specPath={specPath}
op={op}
path={path}
method={method}
tag={tag}
/>
}).toArray()
}
</div>

{ this.renderChildTags() }
</Collapse>
</div>
)
}

renderChildTags() {
const { childTags } = this.props;
if (!childTags || childTags.size === 0) {
return null;
}

const {
oas3Selectors,
layoutSelectors,
layoutActions,
getConfigs,
getComponent,
specSelectors,
isRoot,
} = this.props;

return (
<div className="hierarchical-operation-tags" style={isRoot ? null : {margin: "0 0 0 2rem"}}>
{
childTags.map((tag, tagName) => {
return <HierarchicalOperationTag
key={"operation-" + (tag.get("canonicalTagName") || tagName)}
tagObj={tag.get("data")}
tag={tagName}
specSelectors={specSelectors}
oas3Selectors={oas3Selectors}
layoutSelectors={layoutSelectors}
layoutActions={layoutActions}
getConfigs={getConfigs}
getComponent={getComponent}
childTags={tag.get("childTags")}
isRoot={false}
/>
})
}
</div>
)
}
}
159 changes: 93 additions & 66 deletions src/core/components/operations.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,6 @@ import React from "react"
import PropTypes from "prop-types"
import Im from "immutable"

const SWAGGER2_OPERATION_METHODS = [
"get", "put", "post", "delete", "options", "head", "patch"
]

const OAS3_OPERATION_METHODS = SWAGGER2_OPERATION_METHODS.concat(["trace"])


export default class Operations extends React.Component {

static propTypes = {
Expand Down Expand Up @@ -36,84 +29,118 @@ export default class Operations extends React.Component {
fn
} = this.props

// Get pertinent options
let {
maxDisplayedTags,
hierarchicalTags,
tagSplitterChar,
} = getConfigs();

// Set default tagSplitterChar if necessary
tagSplitterChar = tagSplitterChar || /[:|]/;

// Get a flat map of tag names to tag info and operations. Note that this will always return a
// flat list, even if the `hierarchicalTags` option is set to `true`.
let taggedOps = specSelectors.taggedOperations()

const OperationContainer = getComponent("OperationContainer", true)
const OperationTag = getComponent("OperationTag")

let {
maxDisplayedTags,
} = getConfigs()

// Filter, if requested
let filter = layoutSelectors.currentFilter()

if (filter) {
if (filter !== true && filter !== "true" && filter !== "false") {
taggedOps = fn.opsFilter(taggedOps, filter)
}
}

// Limit to [max] items, if specified
if (maxDisplayedTags && !isNaN(maxDisplayedTags) && maxDisplayedTags >= 0) {
taggedOps = taggedOps.slice(0, maxDisplayedTags)
}

return (
<div>
{
taggedOps.map( (tagObj, tag) => {
const operations = tagObj.get("operations")
return (
<OperationTag
key={"operation-" + tag}
tagObj={tagObj}
tag={tag}
oas3Selectors={oas3Selectors}
layoutSelectors={layoutSelectors}
layoutActions={layoutActions}
getConfigs={getConfigs}
getComponent={getComponent}
specUrl={specSelectors.url()}>
{
operations.map( op => {
const path = op.get("path")
const method = op.get("method")
const specPath = Im.List(["paths", path, method])


// FIXME: (someday) this logic should probably be in a selector,
// but doing so would require further opening up
// selectors to the plugin system, to allow for dynamic
// overriding of low-level selectors that other selectors
// rely on. --KS, 12/17
const validMethods = specSelectors.isOAS3() ?
OAS3_OPERATION_METHODS : SWAGGER2_OPERATION_METHODS

if(validMethods.indexOf(method) === -1) {
return null
}

return <OperationContainer
key={`${path}-${method}`}
specPath={specPath}
op={op}
path={path}
method={method}
tag={tag}
/>
}).toArray()
}


</OperationTag>
)
}).toArray()
// Convert flat object to hierarchy. We're using a "raw" object for cleanliness here, but later
// we'll convert that into an immutable map. Here are the types we're dealing with:
//
// type operationTagsRaw = TagMap;
// type TagMap = { [TagName: string]: TagData };
// type TagData = {
// canonicalName: string;
// data: TagInfoAndOperations | null;
// childTags: TagMap;
// }
// TODO: Explicitly define TagInfoAndOperations
const operationTagsRaw = {};
if (hierarchicalTags) {
// If the `hierarchicalTags` option is set, we want to break down the tags into a deep
// hierarchy

// For each raw tag....
for (const tagName in taggedOps) {
// Split the raw tag name into parts
const parts = tagName.split(tagSplitterChar);

// Set a pointer for use in traversing the hierarchy
let current = operationTagsRaw;

// Iterate through the parts defined by this tag
for (let i = 0; i < parts.length; i++) {
const part = parts[i];

// If there's no object defined for the current part, define one with just childTags as an
// empty set
if (current[part] === undefined) {
// Compose canonical name from parts up to this point
const canonicalName = parts.reduce(
(name, p, j) => ((j > i) ? name : name.concat([p])),
[]
).join("|");
current[part] = {
canonicalName,
data: null,
childTags: {}
}
}

{ taggedOps.size < 1 ? <h3> No operations defined in spec! </h3> : null }
</div>
)
}
// If this is the last part, set data on this object
if (i === parts.length - 1) {
current[part].data = taggedOps.get(tagName);
}

// Move to the next level of the hierarchy before looping around
current = current[part].childTags;
}
}
} else {
// If the `hierarchicalTags` option is not set, we just want to convert our flat tag map into
// the right format
for (const tagName in taggedOps) {
operationTagsRaw[tagName] = {
canonicalName: tagName,
data: taggedOps.get(tagName),
childTags: {}
}
}
}

// Convert to immutable map
const operationTags = Im.fromJs(operationTagsRaw);

// Return the render
return operationTags.size === 0
? <h3> No operations defined in spec!</h3>
:
<OperationTag
specSelectors={specSelectors}
oas3Selectors={oas3Selectors}
layoutSelectors={layoutSelectors}
layoutActions={layoutActions}
getConfigs={getConfigs}
getComponent={getComponent}
childTags={operationTags}
isRoot={true}
/>
}
}

Operations.propTypes = {
Expand Down

0 comments on commit 5536916

Please sign in to comment.