diff --git a/src/core/components/operation-tag.jsx b/src/core/components/operation-tag.jsx
new file mode 100644
index 00000000..0c4dc8b6
--- /dev/null
+++ b/src/core/components/operation-tag.jsx
@@ -0,0 +1,108 @@
+import React from "react"
+import PropTypes from "prop-types"
+import ImPropTypes from "react-immutable-proptypes"
+import Im from "immutable"
+import { createDeepLinkPath, sanitizeUrl } from "core/utils"
+
+export default class OperationTag extends React.Component {
+
+ static defaultProps = {
+ tagObj: Im.fromJS({}),
+ tag: "",
+ }
+
+ static propTypes = {
+ tagObj: ImPropTypes.map.isRequired,
+ tag: PropTypes.string.isRequired,
+
+ layoutSelectors: PropTypes.object.isRequired,
+ layoutActions: PropTypes.object.isRequired,
+
+ getConfigs: PropTypes.func.isRequired,
+ getComponent: PropTypes.func.isRequired,
+
+ children: PropTypes.element,
+ }
+
+ render() {
+ const {
+ tagObj,
+ tag,
+ children,
+
+ layoutSelectors,
+ layoutActions,
+ getConfigs,
+ getComponent,
+ } = this.props
+
+ let {
+ docExpansion,
+ deepLinking,
+ } = getConfigs()
+
+ const isDeepLinkingEnabled = deepLinking && deepLinking !== "false"
+
+ const Collapse = getComponent("Collapse")
+ const Markdown = getComponent("Markdown")
+ const DeepLink = getComponent("DeepLink")
+
+ let tagDescription = tagObj.getIn(["tagDetails", "description"], null)
+ let tagExternalDocsDescription = tagObj.getIn(["tagDetails", "externalDocs", "description"])
+ let tagExternalDocsUrl = tagObj.getIn(["tagDetails", "externalDocs", "url"])
+
+ let isShownKey = ["operations-tag", createDeepLinkPath(tag)]
+ let showTag = layoutSelectors.isShown(isShownKey, docExpansion === "full" || docExpansion === "list")
+
+ return (
+
{
taggedOps.map( (tagObj, tag) => {
- let operations = tagObj.get("operations")
- let tagDescription = tagObj.getIn(["tagDetails", "description"], null)
- let tagExternalDocsDescription = tagObj.getIn(["tagDetails", "externalDocs", "description"])
- let tagExternalDocsUrl = tagObj.getIn(["tagDetails", "externalDocs", "url"])
-
- let isShownKey = ["operations-tag", createDeepLinkPath(tag)]
- let showTag = layoutSelectors.isShown(isShownKey, docExpansion === "full" || docExpansion === "list")
-
+ const operations = tagObj.get("operations")
return (
-
-
-
layoutActions.show(isShownKey, !showTag)}
- className={!tagDescription ? "opblock-tag no-desc" : "opblock-tag" }
- id={isShownKey.join("-")}>
-
- { !tagDescription ? :
-
-
-
- }
-
-
-
-
-
-
-
- {
- operations.map( op => {
- const path = op.get("path")
- const method = op.get("method")
- const specPath = Im.List(["paths", path, method])
+
+ {
+ 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
+ // 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
- }
+ if(validMethods.indexOf(method) === -1) {
+ return null
+ }
- return
- }).toArray()
- }
-
-
- )
+ return
+ }).toArray()
+ }
+
+
+
+ )
}).toArray()
}
diff --git a/src/core/index.js b/src/core/index.js
index f4c88d92..c839ca61 100644
--- a/src/core/index.js
+++ b/src/core/index.js
@@ -27,7 +27,7 @@ module.exports = function SwaggerUI(opts) {
const defaults = {
// Some general settings, that we floated to the top
- dom_id: null,
+ dom_id: null, // eslint-disable-line camelcase
domNode: null,
spec: {},
url: "",
@@ -131,10 +131,6 @@ module.exports = function SwaggerUI(opts) {
var system = store.getSystem()
const downloadSpec = (fetchedConfig) => {
- if(typeof constructorConfig !== "object") {
- return system
- }
-
let localConfig = system.specSelectors.getLocalConfig ? system.specSelectors.getLocalConfig() : {}
let mergedConfig = deepExtend({}, localConfig, constructorConfig, fetchedConfig || {}, queryConfig)
@@ -144,6 +140,7 @@ module.exports = function SwaggerUI(opts) {
}
store.setConfigs(mergedConfig)
+ system.configsActions.loaded()
if (fetchedConfig !== null) {
if (!queryConfig.url && typeof mergedConfig.spec === "object" && Object.keys(mergedConfig.spec).length) {
@@ -171,15 +168,17 @@ module.exports = function SwaggerUI(opts) {
return system
}
- let configUrl = queryConfig.config || constructorConfig.configUrl
+ const configUrl = queryConfig.config || constructorConfig.configUrl
- if (!configUrl || !system.specActions.getConfigByUrl || system.specActions.getConfigByUrl && !system.specActions.getConfigByUrl({
+ if (!configUrl || !system.specActions || !system.specActions.getConfigByUrl || system.specActions.getConfigByUrl && !system.specActions.getConfigByUrl({
url: configUrl,
loadRemoteConfig: true,
requestInterceptor: constructorConfig.requestInterceptor,
responseInterceptor: constructorConfig.responseInterceptor,
}, downloadSpec)) {
return downloadSpec()
+ } else {
+ system.specActions.getConfigByUrl(configUrl, downloadSpec)
}
return system
diff --git a/src/core/plugins/configs/actions.js b/src/core/plugins/configs/actions.js
index 70588372..977407de 100644
--- a/src/core/plugins/configs/actions.js
+++ b/src/core/plugins/configs/actions.js
@@ -18,3 +18,7 @@ export function toggle(configName) {
payload: configName,
}
}
+
+
+// Hook
+export const loaded = () => () => {}
diff --git a/src/core/plugins/deep-linking/index.js b/src/core/plugins/deep-linking/index.js
index 8cec4dd5..ebfe2d8d 100644
--- a/src/core/plugins/deep-linking/index.js
+++ b/src/core/plugins/deep-linking/index.js
@@ -1,18 +1,23 @@
-// import reducers from "./reducers"
-// import * as actions from "./actions"
-// import * as selectors from "./selectors"
-import * as specWrapActions from "./spec-wrap-actions"
-import * as layoutWrapActions from "./layout-wrap-actions"
+import layout from "./layout"
+import OperationWrapper from "./operation-wrapper"
+import OperationTagWrapper from "./operation-tag-wrapper"
export default function() {
- return {
+ return [layout, {
statePlugins: {
- spec: {
- wrapActions: specWrapActions
- },
- layout: {
- wrapActions: layoutWrapActions
+ configs: {
+ wrapActions: {
+ loaded: (ori, system) => (...args) => {
+ ori(...args)
+ const hash = window.location.hash
+ system.layoutActions.parseDeepLinkHash(hash)
+ }
+ }
}
- }
- }
+ },
+ wrapComponents: {
+ operation: OperationWrapper,
+ OperationTag: OperationTagWrapper,
+ },
+ }]
}
diff --git a/src/core/plugins/deep-linking/layout-wrap-actions.js b/src/core/plugins/deep-linking/layout-wrap-actions.js
deleted file mode 100644
index f1e67d12..00000000
--- a/src/core/plugins/deep-linking/layout-wrap-actions.js
+++ /dev/null
@@ -1,37 +0,0 @@
-import { setHash } from "./helpers"
-import { createDeepLinkPath } from "core/utils"
-
-export const show = (ori, { getConfigs }) => (...args) => {
- ori(...args)
-
- const isDeepLinkingEnabled = getConfigs().deepLinking
- if(!isDeepLinkingEnabled || isDeepLinkingEnabled === "false") {
- return
- }
-
- try {
- let [thing, shown] = args
- let [type] = thing
-
- if(type === "operations-tag" || type === "operations") {
- if(!shown) {
- return setHash("/")
- }
-
- if(type === "operations") {
- let [, tag, operationId] = thing
- setHash(`/${createDeepLinkPath(tag)}/${createDeepLinkPath(operationId)}`)
- }
-
- if(type === "operations-tag") {
- let [, tag] = thing
- setHash(`/${createDeepLinkPath(tag)}`)
- }
- }
-
- } catch(e) {
- // This functionality is not mission critical, so if something goes wrong
- // we'll just move on
- console.error(e)
- }
-}
diff --git a/src/core/plugins/deep-linking/layout.js b/src/core/plugins/deep-linking/layout.js
new file mode 100644
index 00000000..ba405b29
--- /dev/null
+++ b/src/core/plugins/deep-linking/layout.js
@@ -0,0 +1,181 @@
+import { setHash } from "./helpers"
+import zenscroll from "zenscroll"
+import Im, { fromJS } from "immutable"
+
+const SCROLL_TO = "layout_scroll_to"
+const CLEAR_SCROLL_TO = "layout_clear_scroll"
+
+export const show = (ori, { getConfigs, layoutSelectors }) => (...args) => {
+ ori(...args)
+
+ if(!getConfigs().deepLinking) {
+ return
+ }
+
+ try {
+ let [tokenArray, shown] = args
+ //Coerce in to array
+ tokenArray = Array.isArray(tokenArray) ? tokenArray : [tokenArray]
+ // Convert into something we can put in the URL hash
+ // Or return empty, if we cannot
+ const urlHashArray = layoutSelectors.urlHashArrayFromIsShownKey(tokenArray) // Will convert
+
+ // No hash friendly list?
+ if(!urlHashArray.length)
+ return
+
+ const [type, assetName] = urlHashArray
+
+ if (!shown) {
+ return setHash("/")
+ }
+
+ if (urlHashArray.length === 2) {
+ setHash(`/${type}/${assetName}`)
+ } else if (urlHashArray.length === 1) {
+ setHash(`/${type}`)
+ }
+
+ } catch (e) {
+ // This functionality is not mission critical, so if something goes wrong
+ // we'll just move on
+ console.error(e) // eslint-disable-line no-console
+ }
+}
+
+export const scrollTo = (path) => {
+ return {
+ type: SCROLL_TO,
+ payload: Array.isArray(path) ? path : [path]
+ }
+}
+
+export const parseDeepLinkHash = (rawHash) => ({ layoutActions, layoutSelectors, getConfigs }) => {
+
+ if(!getConfigs().deepLinking) {
+ return
+ }
+
+ if(rawHash) {
+ let hash = rawHash.slice(1) // # is first character
+
+
+ if(hash[0] === "!") {
+ // Parse UI 2.x shebangs
+ hash = hash.slice(1)
+ }
+
+ if(hash[0] === "/") {
+ // "/pet/addPet" => "pet/addPet"
+ // makes the split result cleaner
+ // also handles forgotten leading slash
+ hash = hash.slice(1)
+ }
+
+ const isShownKey = layoutSelectors.isShownKeyFromUrlHashArray(hash.split("/"))
+
+ layoutActions.show(isShownKey, true) // TODO: 'show' operation tag
+ layoutActions.scrollTo(isShownKey)
+ }
+}
+
+export const readyToScroll = (isShownKey, ref) => (system) => {
+ const scrollToKey = system.layoutSelectors.getScrollToKey()
+
+ if(Im.is(scrollToKey, fromJS(isShownKey))) {
+ system.layoutActions.scrollToElement(ref)
+ system.layoutActions.clearScrollTo()
+ }
+}
+
+// Scroll to "ref" (dom node) with the scrollbar on "container" or the nearest parent
+export const scrollToElement = (ref, container) => (system) => {
+ try {
+ container = container || system.fn.getScrollParent(ref)
+ let myScroller = zenscroll.createScroller(container)
+ myScroller.to(ref)
+ } catch(e) {
+ console.error(e) // eslint-disable-line no-console
+ }
+}
+
+export const clearScrollTo = () => {
+ return {
+ type: CLEAR_SCROLL_TO,
+ }
+}
+
+// From: https://stackoverflow.com/a/42543908/3933724
+// Modified to return html instead of body element as last resort
+function getScrollParent(element, includeHidden) {
+ const LAST_RESORT = document.documentElement
+ let style = getComputedStyle(element)
+ const excludeStaticParent = style.position === "absolute"
+ const overflowRegex = includeHidden ? /(auto|scroll|hidden)/ : /(auto|scroll)/
+
+ if (style.position === "fixed")
+ return LAST_RESORT
+ for (let parent = element; (parent = parent.parentElement);) {
+ style = getComputedStyle(parent)
+ if (excludeStaticParent && style.position === "static") {
+ continue
+ }
+ if (overflowRegex.test(style.overflow + style.overflowY + style.overflowX))
+ return parent
+ }
+
+ return LAST_RESORT
+}
+
+export default {
+ fn: {
+ getScrollParent,
+ },
+ statePlugins: {
+ layout: {
+ actions: {
+ scrollToElement,
+ scrollTo,
+ clearScrollTo,
+ readyToScroll,
+ parseDeepLinkHash
+ },
+ selectors: {
+ getScrollToKey(state) {
+ return state.get("scrollToKey")
+ },
+ isShownKeyFromUrlHashArray(state, urlHashArray) {
+ const [tag, operationId] = urlHashArray
+ // We only put operations in the URL
+ if(operationId) {
+ return ["operations", tag, operationId]
+ } else if (tag) {
+ return ["operations-tag", tag]
+ }
+ return []
+ },
+ urlHashArrayFromIsShownKey(state, isShownKey) {
+ let [type, tag, operationId] = isShownKey
+ // We only put operations in the URL
+ if(type == "operations") {
+ return [tag, operationId]
+ } else if (type == "operations-tag") {
+ return [tag]
+ }
+ return []
+ },
+ },
+ reducers: {
+ [SCROLL_TO](state, action) {
+ return state.set("scrollToKey", Im.fromJS(action.payload))
+ },
+ [CLEAR_SCROLL_TO](state) {
+ return state.delete("scrollToKey")
+ }
+ },
+ wrapActions: {
+ show
+ }
+ }
+ }
+}
diff --git a/src/core/plugins/deep-linking/operation-tag-wrapper.jsx b/src/core/plugins/deep-linking/operation-tag-wrapper.jsx
new file mode 100644
index 00000000..1694683f
--- /dev/null
+++ b/src/core/plugins/deep-linking/operation-tag-wrapper.jsx
@@ -0,0 +1,25 @@
+import React from "react"
+import { PropTypes } from "prop-types"
+
+const Wrapper = (Ori, system) => class OperationTagWrapper extends React.Component {
+
+ static propTypes = {
+ tag: PropTypes.object.isRequired,
+ }
+
+ onLoad = (ref) => {
+ const { tag } = this.props
+ const isShownKey = ["operations-tag", tag]
+ system.layoutActions.readyToScroll(isShownKey, ref)
+ }
+
+ render() {
+ return (
+
+
+
+ )
+ }
+}
+
+export default Wrapper
diff --git a/src/core/plugins/deep-linking/operation-wrapper.jsx b/src/core/plugins/deep-linking/operation-wrapper.jsx
new file mode 100644
index 00000000..ab2fb8e4
--- /dev/null
+++ b/src/core/plugins/deep-linking/operation-wrapper.jsx
@@ -0,0 +1,26 @@
+import React from "react"
+import ImPropTypes from "react-immutable-proptypes"
+
+const Wrapper = (Ori, system) => class OperationWrapper extends React.Component {
+
+ static propTypes = {
+ operation: ImPropTypes.map.isRequired,
+ }
+
+ onLoad = (ref) => {
+ const { operation } = this.props
+ const { tag, operationId } = operation.toObject()
+ const isShownKey = ["operations", tag, operationId]
+ system.layoutActions.readyToScroll(isShownKey, ref)
+ }
+
+ render() {
+ return (
+
+
+
+ )
+ }
+}
+
+export default Wrapper
diff --git a/src/core/plugins/deep-linking/spec-wrap-actions.js b/src/core/plugins/deep-linking/spec-wrap-actions.js
deleted file mode 100644
index 398fbcbc..00000000
--- a/src/core/plugins/deep-linking/spec-wrap-actions.js
+++ /dev/null
@@ -1,64 +0,0 @@
-import zenscroll from "zenscroll"
-import { escapeDeepLinkPath } from "core/utils"
-
-let hasHashBeenParsed = false //TODO this forces code to only run once which may prevent scrolling if page not refreshed
-
-
-export const updateJsonSpec = (ori, { layoutActions, getConfigs }) => (...args) => {
- ori(...args)
-
- const isDeepLinkingEnabled = getConfigs().deepLinking
- if(!isDeepLinkingEnabled || isDeepLinkingEnabled === "false") {
- return
- }
- if(window.location.hash && !hasHashBeenParsed ) {
- let hash = window.location.hash.slice(1) // # is first character
-
- if(hash[0] === "!") {
- // Parse UI 2.x shebangs
- hash = hash.slice(1)
- }
-
- if(hash[0] === "/") {
- // "/pet/addPet" => "pet/addPet"
- // makes the split result cleaner
- // also handles forgotten leading slash
- hash = hash.slice(1)
- }
-
- let [tag, operationId] = hash.split("/")
-
- let swaggerUI = document.querySelector(".swagger-ui")
- let myScroller = zenscroll.createScroller(swaggerUI)
-
- let target
-
- if(tag && operationId) {
- // Pre-expand and scroll to the operation
- layoutActions.show(["operations-tag", tag], true)
- layoutActions.show(["operations", tag, operationId], true)
-
- target = document
- .getElementById(`operations-${escapeDeepLinkPath(tag)}-${escapeDeepLinkPath(operationId)}`)
- } else if(tag) {
- // Pre-expand and scroll to the tag
- layoutActions.show(["operations-tag", tag], true)
-
- target = document.getElementById(`operations-tag-${escapeDeepLinkPath(tag)}`)
- }
-
-
- if(target) {
- myScroller.to(target)
- setTimeout(() => {
- // Backup functionality: if we're still at the top of the document,
- // scroll on the entire page (not within the Swagger-UI container)
- if(zenscroll.getY() === 0) {
- zenscroll.to(target)
- }
- }, 50)
- }
- }
-
- hasHashBeenParsed = true
-}
diff --git a/src/core/presets/base.js b/src/core/presets/base.js
index f0d985e7..10fba1e3 100644
--- a/src/core/presets/base.js
+++ b/src/core/presets/base.js
@@ -31,6 +31,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 OperationTag from "core/components/operation-tag"
import Operation from "core/components/operation"
import OperationExt from "core/components/operation-extensions"
import OperationExtRow from "core/components/operation-extension-row"
@@ -128,6 +129,7 @@ export default function() {
OperationExt,
OperationExtRow,
ParameterExt,
+ OperationTag,
OperationContainer,
DeepLink,
InfoUrl,
diff --git a/test/components/operations.js b/test/components/operations.js
index bfcbed4f..844a4931 100644
--- a/test/components/operations.js
+++ b/test/components/operations.js
@@ -1,6 +1,6 @@
/* eslint-env mocha */
import React from "react"
-import expect, { createSpy } from "expect"
+import expect from "expect"
import { render } from "enzyme"
import { fromJS } from "immutable"
import DeepLink from "components/deep-link"
@@ -10,7 +10,8 @@ import {Collapse} from "components/layout-utils"
const components = {
Collapse,
DeepLink,
- OperationContainer: ({ path, method }) =>
+ OperationContainer: ({ path, method }) =>
,
+ OperationTag: "div",
}
describe("
", function(){