diff --git a/src/core/components/hierarchical-operation-tag.jsx b/src/core/components/hierarchical-operation-tag.jsx new file mode 100644 index 00000000000..0de093b63e6 --- /dev/null +++ b/src/core/components/hierarchical-operation-tag.jsx @@ -0,0 +1,218 @@ +import React from "react" +import PropTypes from "prop-types" +import ImPropTypes from "react-immutable-proptypes" +import Im from "immutable" +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 HierarchicalOperationTag extends React.Component { + + static defaultProps = { + tagObj: Im.fromJS({}), + tag: "", + } + + static propTypes = { + tagObj: ImPropTypes.map, + tag: PropTypes.string, + childTags: ImPropTypes.map.isRequired, + isRoot: PropTypes.bool, + + oas3Selectors: PropTypes.func.isRequired, + layoutSelectors: PropTypes.object.isRequired, + layoutActions: PropTypes.object.isRequired, + + getConfigs: PropTypes.func.isRequired, + getComponent: PropTypes.func.isRequired, + + specUrl: PropTypes.string.isRequired, + } + + 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, + oas3Selectors, + layoutSelectors, + layoutActions, + getConfigs, + getComponent, + specSelectors, + specUrl, + } = this.props + + // Get the necessary configs + let { + docExpansion, + deepLinking, + } = getConfigs() + + // Get the necessary components + const OperationContainer = getComponent("OperationContainer", true) + const Collapse = getComponent("Collapse") + const Markdown = getComponent("Markdown", true) + const DeepLink = getComponent("DeepLink") + const Link = getComponent("Link") + + // Set up some helpers + const isDeepLinkingEnabled = deepLinking && deepLinking !== "false" + + 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 ( +
+ +

layoutActions.show(isShownKey, !showTag)} + className={!tagDescription ? "opblock-tag no-desc" : "opblock-tag" } + id={isShownKey.map(v => escapeDeepLinkPath(v)).join("-")} + data-tag={tag} + data-is-open={showTag} + > + + { !tagDescription ? : + + + + } + +
+ { !tagExternalDocsDescription ? null : + + { tagExternalDocsDescription } + { tagExternalDocsUrl ? ": " : null } + { tagExternalDocsUrl ? + e.stopPropagation()} + target="_blank" + >{tagExternalDocsUrl} : null + } + + } +
+ + +

+ + +
+ { + 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 + }).toArray() + } +
+ + { this.renderChildTags() } +
+
+ ) + } + + renderChildTags() { + const { childTags } = this.props; + if (!childTags || childTags.size === 0) { + return null; + } + + const { + oas3Selectors, + layoutSelectors, + layoutActions, + getConfigs, + getComponent, + specSelectors, + isRoot, + } = this.props; + + return ( +
+ { + childTags.map((tag, tagName) => { + return + }).toArray() + } +
+ ) + } +} diff --git a/src/core/components/operations.jsx b/src/core/components/operations.jsx index b50e2afb4cf..8f32b9694f5 100644 --- a/src/core/components/operations.jsx +++ b/src/core/components/operations.jsx @@ -8,7 +8,6 @@ const SWAGGER2_OPERATION_METHODS = [ const OAS3_OPERATION_METHODS = SWAGGER2_OPERATION_METHODS.concat(["trace"]) - export default class Operations extends React.Component { static propTypes = { @@ -36,84 +35,163 @@ export default class Operations extends React.Component { fn } = this.props - let taggedOps = specSelectors.taggedOperations() - - const OperationContainer = getComponent("OperationContainer", true) - const OperationTag = getComponent("OperationTag") - + // Get pertinent options let { maxDisplayedTags, - } = getConfigs() + hierarchicalTags, + tagSplitterChar, + } = getConfigs(); - let filter = layoutSelectors.currentFilter() + // 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() + // 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 ( -
- { - taggedOps.map( (tagObj, tag) => { - const operations = tagObj.get("operations") - return ( - - { - 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 - }).toArray() - } - - - - ) - }).toArray() + // Render either hierarchical or flat depending on config + if (hierarchicalTags) { + // If the `hierarchicalTags` option is set, we want to break down the tags into a deep + // 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: + // + // const operationTagsRaw: TagMap; + // type TagMap = { [TagName: string]: TagData }; + // type TagData = { + // canonicalName: string; + // data: TagInfoAndOperations | null; + // childTags: TagMap; + // } + // TODO: Explicitly define TagInfoAndOperations + + const operationTagsRaw = {}; + + // For each raw tag.... + taggedOps.map((tagObj, tagName) => { + // 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 ?

No operations defined in spec!

: null } -
- ) - } + // If this is the last part, set data on this object + if (i === parts.length - 1) { + current[part].data = tagObj; + } + // Move to the next level of the hierarchy before looping around + current = current[part].childTags; + } + }); + + // Convert to immutable map + const operationTags = Im.fromJS(operationTagsRaw); + const HierarchicalOperationTag = getComponent("HierarchicalOperationTag") + return operationTags.size === 0 + ?

No operations defined in spec!

+ : + + } else { + const OperationContainer = getComponent("OperationContainer", true) + const OperationTag = getComponent("OperationTag") + return ( +
+ { + taggedOps.map( (tagObj, tag) => { + const operations = tagObj.get("operations") + return ( + + { + 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 + }).toArray() + } + + + + ) + }).toArray() + } + + { taggedOps.size < 1 ?

No operations defined in spec!

: null } +
+ ) + } + } } Operations.propTypes = { diff --git a/src/core/index.js b/src/core/index.js index 72a32d3ff03..22b4dbe338d 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -53,6 +53,8 @@ export default function SwaggerUI(opts) { showExtensions: false, showCommonExtensions: false, withCredentials: undefined, + hierarchicalTags: false, + tagSplitterChar: /[:|]/, supportedSubmitMethods: [ "get", "put", diff --git a/src/core/presets/base.js b/src/core/presets/base.js index 73565cdf7cd..6f131ef8da5 100644 --- a/src/core/presets/base.js +++ b/src/core/presets/base.js @@ -33,6 +33,7 @@ import Clear from "core/components/clear" import LiveResponse from "core/components/live-response" import OnlineValidatorBadge from "core/components/online-validator-badge" import Operations from "core/components/operations" +import HierarchicalOperationTag from "core/components/hierarchical-operation-tag" import OperationTag from "core/components/operation-tag" import Operation from "core/components/operation" import OperationSummary from "core/components/operation-summary" @@ -154,6 +155,7 @@ export default function() { OperationExtRow, ParameterExt, ParameterIncludeEmpty, + HierarchicalOperationTag, OperationTag, OperationContainer, DeepLink,