Skip to content

Commit b5c1249

Browse files
authored
feature: Topbar Edit > Convert to OpenAPI 3 menu item (via #1960)
* create `topbar-menu-edit-convert` * create `topbar` state namespace w/ modal management logic * use new ConvertDefinitionMenuItem component * add `hideCloseButton` prop to TopbarModal * drop unused Portal reference * linter fixes * add `swagger2ConverterUrl` config option * fiddle with ASYNC_TEST_DELAY default
1 parent 4abccfd commit b5c1249

File tree

9 files changed

+277
-5
lines changed

9 files changed

+277
-5
lines changed

src/index.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,8 @@ const defaults = {
5252
},
5353
showExtensions: true,
5454
swagger2GeneratorUrl: "https://generator.swagger.io/api/swagger.json",
55-
oas3GeneratorUrl: "https://generator3.swagger.io/openapi.json"
55+
oas3GeneratorUrl: "https://generator3.swagger.io/openapi.json",
56+
swagger2ConverterUrl: "https://converter.swagger.io/api/convert",
5657
}
5758

5859
module.exports = function SwaggerEditor(options) {

src/standalone/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import TopbarPlugin from "./topbar"
22
import TopbarInsertPlugin from "./topbar-insert"
3+
import TopbarMenuEditConvert from "./topbar-menu-edit-convert"
34
import StandaloneLayout from "./standalone-layout"
45

56
let StandaloneLayoutPlugin = function() {
@@ -14,6 +15,7 @@ module.exports = function () {
1415
return [
1516
TopbarPlugin,
1617
TopbarInsertPlugin,
18+
TopbarMenuEditConvert,
1719
StandaloneLayoutPlugin
1820
]
1921
}

src/standalone/topbar-insert/modal/Modal.jsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@ const Modal = (props) => (
88
<div className="modal-content">
99
<div className={classNames("modal-header", {"modal-header-border" : props.title})} >
1010
<span className="modal-title">{props.title}</span>
11-
<a type="button" className="close" aria-label="Close" onClick={props.onCloseClick}>
11+
{!props.hideCloseButton && <a type="button" className="close" aria-label="Close" onClick={props.onCloseClick}>
1212
<span aria-hidden="true">&times;</span>
13-
</a>
13+
</a> }
1414
</div>
1515
{props.children}
1616
</div>
@@ -22,6 +22,7 @@ Modal.propTypes = {
2222
title: PropTypes.string,
2323
styleName: PropTypes.string,
2424
onCloseClick: PropTypes.func,
25+
hideCloseButton: PropTypes.bool,
2526
children: PropTypes.oneOfType([
2627
PropTypes.array,
2728
PropTypes.element
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import React, { Component } from "react"
2+
import PropTypes from "prop-types"
3+
4+
export default class ConvertDefinitionMenuItem extends Component {
5+
render() {
6+
const { isSwagger2, } = this.props
7+
8+
if(!isSwagger2) {
9+
return null
10+
}
11+
12+
return <li>
13+
<button type="button" onClick={this.props.onClick}>Convert to OpenAPI 3</button>
14+
</li>
15+
}
16+
}
17+
18+
ConvertDefinitionMenuItem.propTypes = {
19+
isSwagger2: PropTypes.bool.isRequired,
20+
onClick: PropTypes.func.isRequired,
21+
}
Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
import React, { Component } from "react"
2+
import PropTypes from "prop-types"
3+
4+
export default class ConvertModal extends Component {
5+
constructor() {
6+
super()
7+
this.state = {
8+
error: null,
9+
status: "new"
10+
}
11+
}
12+
convertDefinition = async () => {
13+
this.setState({ status: "converting" })
14+
15+
try {
16+
const conversionResult = await this.performConversion()
17+
this.setState({
18+
status: "success",
19+
})
20+
this.props.updateEditorContent(conversionResult)
21+
} catch (e) {
22+
this.setState({
23+
error: e,
24+
status: "errored",
25+
})
26+
}
27+
}
28+
29+
performConversion = async () => {
30+
const res = await fetch("//converter.swagger.io/api/convert", {
31+
method: "POST",
32+
headers: {
33+
"Content-Type": "application/yaml",
34+
"Accept": "application/yaml",
35+
},
36+
body: this.props.editorContent
37+
})
38+
39+
const body = await res.text()
40+
41+
if(!res.ok) {
42+
throw new Error(body)
43+
}
44+
45+
return body
46+
}
47+
48+
render() {
49+
const { onClose, getComponent, converterUrl } = this.props
50+
51+
if(this.state.status === "new") {
52+
return <ConvertModalStepNew
53+
onClose={onClose}
54+
onContinue={() => this.convertDefinition()}
55+
getComponent={getComponent}
56+
converterUrl={converterUrl}
57+
/>
58+
}
59+
60+
if (this.state.status === "converting") {
61+
return <ConvertModalStepConverting
62+
getComponent={getComponent}
63+
/>
64+
}
65+
66+
if (this.state.status === "success") {
67+
return <ConvertModalStepSuccess
68+
onClose={onClose}
69+
getComponent={getComponent}
70+
/>
71+
}
72+
73+
if (this.state.status === "errored") {
74+
return <ConvertModalStepErrored
75+
onClose={onClose}
76+
error={this.state.error}
77+
getComponent={getComponent}
78+
/>
79+
}
80+
81+
82+
return null
83+
}
84+
}
85+
86+
ConvertModal.propTypes = {
87+
editorContent: PropTypes.string.isRequired,
88+
converterUrl: PropTypes.string.isRequired,
89+
getComponent: PropTypes.func.isRequired,
90+
updateEditorContent: PropTypes.func.isRequired,
91+
onClose: PropTypes.func.isRequired,
92+
}
93+
94+
const ConvertModalStepNew = ({ getComponent, onClose, onContinue, converterUrl }) => {
95+
const Modal = getComponent("TopbarModal")
96+
97+
return <Modal className="modal" styleName="modal-dialog-sm" onCloseClick={onClose}>
98+
<div className="container modal-message">
99+
<h2>Convert to OpenAPI 3</h2>
100+
<p>
101+
This feature uses the Swagger Converter API to convert your Swagger 2.0
102+
definition to OpenAPI 3.
103+
</p>
104+
<p>
105+
Swagger Editor's contents will be sent to <b><code>{converterUrl}</code></b> and overwritten
106+
by the conversion result.
107+
</p>
108+
</div>
109+
<div className="right">
110+
<button className="btn cancel" onClick={onClose}>Cancel</button>
111+
<button className="btn" onClick={onContinue}>Convert</button>
112+
</div>
113+
</Modal>
114+
}
115+
116+
ConvertModalStepNew.propTypes = {
117+
getComponent: PropTypes.func.isRequired,
118+
onClose: PropTypes.func.isRequired,
119+
onContinue: PropTypes.func.isRequired,
120+
converterUrl: PropTypes.string.isRequired,
121+
}
122+
123+
const ConvertModalStepConverting = ({ getComponent }) => {
124+
const Modal = getComponent("TopbarModal")
125+
126+
return <Modal className="modal" styleName="modal-dialog-sm" hideCloseButton={true}>
127+
<div className="container modal-message">
128+
<h2>Converting...</h2>
129+
<p>
130+
Please wait.
131+
</p>
132+
</div>
133+
</Modal>
134+
}
135+
136+
ConvertModalStepConverting.propTypes = {
137+
getComponent: PropTypes.func.isRequired,
138+
}
139+
140+
const ConvertModalStepSuccess = ({ getComponent, onClose }) => {
141+
const Modal = getComponent("TopbarModal")
142+
143+
return <Modal className="modal" styleName="modal-dialog-sm" onCloseClick={onClose}>
144+
<div className="container modal-message">
145+
<h2>Conversion complete</h2>
146+
<p>
147+
Your definition was successfully converted to OpenAPI 3!
148+
</p>
149+
</div>
150+
<div className="right">
151+
<button className="btn" onClick={onClose}>Close</button>
152+
</div>
153+
</Modal>
154+
}
155+
156+
157+
ConvertModalStepSuccess.propTypes = {
158+
getComponent: PropTypes.func.isRequired,
159+
onClose: PropTypes.func.isRequired,
160+
}
161+
162+
const ConvertModalStepErrored = ({ getComponent, onClose, error }) => {
163+
const Modal = getComponent("TopbarModal")
164+
165+
return <Modal className="modal" styleName="modal-dialog-sm" onCloseClick={onClose}>
166+
<div className="container modal-message">
167+
<h2>Conversion failed</h2>
168+
<p>
169+
The converter service was unable to convert your definition.
170+
</p>
171+
<p>
172+
Here's what the service told us:
173+
</p>
174+
<code>
175+
{error.toString()}
176+
</code>
177+
</div>
178+
<div className="right">
179+
<button className="btn" onClick={onClose}>Close</button>
180+
</div>
181+
</Modal>
182+
}
183+
184+
ConvertModalStepErrored.propTypes = {
185+
getComponent: PropTypes.func.isRequired,
186+
onClose: PropTypes.func.isRequired,
187+
error: PropTypes.any.isRequired,
188+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/* eslint-disable react/prop-types */
2+
3+
import React from "react"
4+
5+
import ConvertDefinitionMenuItem from "./components/convert-definition-menu-item"
6+
import ConvertModal from "./components/convert-modal"
7+
8+
export default {
9+
components: {
10+
ConvertDefinitionMenuItem,
11+
ConvertModal,
12+
},
13+
wrapComponents: {
14+
Topbar: (Ori) => props => {
15+
const ConvertModal = props.getComponent("ConvertModal")
16+
return <div>
17+
<Ori {...props} />
18+
{props.topbarSelectors.showModal("convert") && <ConvertModal
19+
getComponent={props.getComponent}
20+
editorContent={props.specSelectors.specStr()}
21+
converterUrl={props.getConfigs().swagger2ConverterUrl}
22+
updateEditorContent={content => props.specActions.updateSpec(content, "insert")}
23+
onClose={() => props.topbarActions.hideModal("convert")}
24+
/>}
25+
</div>
26+
}
27+
}
28+
}

src/standalone/topbar/index.js

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,31 @@ import Topbar from "./topbar.jsx"
22

33
export default function () {
44
return {
5+
statePlugins: {
6+
topbar: {
7+
actions: {
8+
showModal(name) {
9+
return {
10+
type: "TOPBAR_SHOW_MODAL",
11+
target: name
12+
}
13+
},
14+
hideModal(name) {
15+
return {
16+
type: "TOPBAR_HIDE_MODAL",
17+
target: name
18+
}
19+
}
20+
},
21+
reducers: {
22+
TOPBAR_SHOW_MODAL: (state, action) => state.setIn(["shownModals", action.target], true),
23+
TOPBAR_HIDE_MODAL: (state, action) => state.setIn(["shownModals", action.target], false),
24+
},
25+
selectors: {
26+
showModal: (state, name) => state.getIn(["shownModals", name], false)
27+
}
28+
}
29+
},
530
components: {
631
Topbar
732
}

src/standalone/topbar/topbar.jsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -325,10 +325,11 @@ export default class Topbar extends React.Component {
325325
}
326326

327327
render() {
328-
let { getComponent } = this.props
328+
let { getComponent, specSelectors, topbarActions } = this.props
329329
const Link = getComponent("Link")
330330
const TopbarInsert = getComponent("TopbarInsert")
331331
const Modal = getComponent("TopbarModal")
332+
const ConvertDefinitionMenuItem = getComponent("ConvertDefinitionMenuItem")
332333

333334
let showServersMenu = this.state.servers && this.state.servers.length
334335
let showClientsMenu = this.state.clients && this.state.clients.length
@@ -375,6 +376,10 @@ export default class Topbar extends React.Component {
375376
</DropdownMenu>
376377
<DropdownMenu {...makeMenuOptions("Edit")}>
377378
<li><button type="button" onClick={this.convertToYaml}>Convert to YAML</button></li>
379+
<ConvertDefinitionMenuItem
380+
isSwagger2={specSelectors.isSwagger2()}
381+
onClick={() => topbarActions.showModal("convert")}
382+
/>
378383
</DropdownMenu>
379384
<TopbarInsert {...this.props} />
380385
{ showServersMenu ? <DropdownMenu className="long" {...makeMenuOptions("Generate Server")}>
@@ -407,6 +412,7 @@ Topbar.propTypes = {
407412
specSelectors: PropTypes.object.isRequired,
408413
errSelectors: PropTypes.object.isRequired,
409414
specActions: PropTypes.object.isRequired,
415+
topbarActions: PropTypes.object.isRequired,
410416
getComponent: PropTypes.func.isRequired,
411417
getConfigs: PropTypes.func.isRequired
412418
}

test/unit/plugins/validate-semantic/validate-helper.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import ASTPlugin from "plugins/ast"
88

99
const envDelay = process.env.ASYNC_TEST_DELAY
1010

11-
const DELAY_MS = (typeof envDelay === "string" ? parseInt(envDelay) : envDelay) || 50
11+
const DELAY_MS = (typeof envDelay === "string" ? parseInt(envDelay) : envDelay) || 200
1212

1313
export default function validateHelper(spec) {
1414
return new Promise((resolve) => {

0 commit comments

Comments
 (0)