Added Swagger
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
This is the Editor's Standalone preset - it adds some features used by [editor.swagger.io](http://editor.swagger.io):
|
||||
|
||||
- Topbar with Swagger branding
|
||||
- URL/File import
|
||||
- YAML conversion trigger
|
||||
- JSON/YAML export
|
||||
@@ -0,0 +1,23 @@
|
||||
import TopbarPlugin from "./topbar"
|
||||
import TopbarInsertPlugin from "./topbar-insert"
|
||||
import TopbarMenuFileImportFile from "./topbar-menu-file-import_file"
|
||||
import TopbarMenuEditConvert from "./topbar-menu-edit-convert"
|
||||
import StandaloneLayout from "./standalone-layout"
|
||||
|
||||
let StandaloneLayoutPlugin = function() {
|
||||
return {
|
||||
components: {
|
||||
StandaloneLayout
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default function () {
|
||||
return [
|
||||
TopbarPlugin,
|
||||
TopbarInsertPlugin,
|
||||
TopbarMenuFileImportFile,
|
||||
TopbarMenuEditConvert,
|
||||
StandaloneLayoutPlugin
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import React from "react"
|
||||
import PropTypes from "prop-types"
|
||||
|
||||
export default class StandaloneLayout extends React.Component {
|
||||
|
||||
static propTypes = {
|
||||
specActions: PropTypes.object.isRequired,
|
||||
getComponent: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
render() {
|
||||
const { getComponent } = this.props
|
||||
const EditorLayout = getComponent("EditorLayout", true)
|
||||
const Topbar = getComponent("Topbar", true)
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Topbar />
|
||||
<EditorLayout/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
@import "topbar-insert-forms.less";
|
||||
@import "topbar-modal.less";
|
||||
@import "topbar.less";
|
||||
@@ -0,0 +1,85 @@
|
||||
.map-form-left {
|
||||
float: left;
|
||||
width: 40%;
|
||||
}
|
||||
|
||||
.map-form-right {
|
||||
float: right;
|
||||
width: 55%;
|
||||
}
|
||||
|
||||
.d-inline-block {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.float-right {
|
||||
float: right;
|
||||
}
|
||||
|
||||
button.remove-item {
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
line-height: 0;
|
||||
padding: 1rem;
|
||||
margin: -1rem -1rem -1rem auto;
|
||||
}
|
||||
|
||||
.form-container {
|
||||
width: 750px;
|
||||
a {
|
||||
color: green;
|
||||
font-size: 1.3em;
|
||||
text-decoration: none;
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.close.remove-item {
|
||||
font-size: 2em;
|
||||
margin-top: -10px;
|
||||
float: right;
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.card-body {
|
||||
padding: 20px;
|
||||
border: 1px solid lightgrey;
|
||||
border-radius: 5px;
|
||||
margin: 10px 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.invalid-feedback {
|
||||
color: red;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
padding-bottom: 10px;
|
||||
.input-label {
|
||||
font-weight: 400;
|
||||
.input-label-title {
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
input[type=text] {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
input[type=text].border-danger{
|
||||
border: 1px solid red;
|
||||
}
|
||||
|
||||
select {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
textarea {
|
||||
border: 1px solid lightgrey;
|
||||
height: 200px;
|
||||
min-height: 100px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
.modal.topbar-modal {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 1000;
|
||||
max-height: 90vh;
|
||||
|
||||
.modal-dialog-sm {
|
||||
width: 400px;
|
||||
}
|
||||
|
||||
.modal-dialog {
|
||||
background-color: white;
|
||||
border: 1px solid lightgrey;
|
||||
border-radius: 5px;
|
||||
max-width: 800px;
|
||||
margin: auto;
|
||||
|
||||
.modal-content {
|
||||
.modal-body {
|
||||
max-height: 80vh;
|
||||
overflow-y: auto;
|
||||
padding: 20px;
|
||||
|
||||
.label {
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
padding: 20px;
|
||||
width: 100%;
|
||||
border-top: 1px solid lightgrey;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.modal-header-border {
|
||||
border-bottom: 1px solid lightgrey;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
width: 100%;
|
||||
padding: 10px 20px;
|
||||
|
||||
.modal-title {
|
||||
font-size: 1.8em;
|
||||
padding-bottom: 15px;
|
||||
}
|
||||
|
||||
.close {
|
||||
float: right;
|
||||
font-size: 1.8em;
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
|
||||
.topbar {
|
||||
background-color: #1b1b1b;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.topbar-wrapper {
|
||||
padding: 0.7em;
|
||||
display: flex;
|
||||
|
||||
& > * {
|
||||
margin-left: 1em;
|
||||
margin-right: 1em;
|
||||
align-self: center;
|
||||
color: white;
|
||||
font-size: 1.0em;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
& .menu-item {
|
||||
cursor: pointer;
|
||||
font-size:14px;
|
||||
|
||||
&::after {
|
||||
content: '▼';
|
||||
margin-left: 6px;
|
||||
font-size: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.topbar-logo__img {
|
||||
float: left;
|
||||
}
|
||||
|
||||
.topbar-logo__title {
|
||||
display: inline-block;
|
||||
color: #fff;
|
||||
font-size: 1.5em;
|
||||
font-weight: bold;
|
||||
margin: 0.1em 1.2em 0 0.5em;
|
||||
}
|
||||
|
||||
.dd-menu {
|
||||
&.long {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
max-width: 800px;
|
||||
.dd-menu-items {
|
||||
width: 700px;
|
||||
.dd-items-left {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-height: 500px;
|
||||
flex-wrap: wrap;
|
||||
margin: 1.7em 0 0!important;
|
||||
li {
|
||||
flex:22%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.long-menu-message {
|
||||
padding: 1.5em;
|
||||
color: #ccc;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.dd-menu-items {
|
||||
margin: 1.1em 0 0 0 !important;
|
||||
ol,ul {
|
||||
border-radius:0 0 4px 4px;
|
||||
|
||||
li {
|
||||
&:last-of-type {
|
||||
&:hover {
|
||||
border-radius:0 0 4px 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.modal {
|
||||
font-family: sans-serif;
|
||||
color: #3b4151;
|
||||
padding: 1em;
|
||||
position: relative;
|
||||
min-height: 12em;
|
||||
|
||||
div.container {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.right {
|
||||
margin: 1em;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
button {
|
||||
margin-left: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-message {
|
||||
margin: 1.75em 2em;
|
||||
font-size: 1.1em;
|
||||
|
||||
p {
|
||||
line-height: 1.3;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import React, { Component } from "react"
|
||||
import PropTypes from "prop-types"
|
||||
|
||||
class Dropdown extends Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
isExpanded: false
|
||||
}
|
||||
|
||||
this.onToggleClick = this.onToggleClick.bind(this)
|
||||
this.handleClickOutside = this.handleClickOutside.bind(this)
|
||||
this.setWrapperRef = this.setWrapperRef.bind(this)
|
||||
}
|
||||
|
||||
componentDidMount = () => {
|
||||
document.addEventListener("mousedown", this.handleClickOutside)
|
||||
}
|
||||
|
||||
componentWillUnmount = () => {
|
||||
document.removeEventListener("mousedown", this.handleClickOutside)
|
||||
}
|
||||
|
||||
onToggleClick = () => {
|
||||
this.setState(prevState => ({
|
||||
isExpanded: !prevState.isExpanded
|
||||
}))
|
||||
}
|
||||
|
||||
setWrapperRef = (node) => {
|
||||
this.wrapperRef = node
|
||||
}
|
||||
|
||||
handleClickOutside = (event) => {
|
||||
if (this.wrapperRef && !this.wrapperRef.contains(event.target)) {
|
||||
this.setState({
|
||||
isExpanded: false
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="dd-menu dd-menu-left" ref={this.setWrapperRef}>
|
||||
<span className="menu-item" role="button" aria-haspopup="true" aria-expanded={this.state.isExpanded} onClick={this.onToggleClick}>
|
||||
{this.props.displayName}
|
||||
</span>
|
||||
{this.state.isExpanded &&
|
||||
<div className="dd-menu-items" aria-labelledby="Dropdown" onClick={this.onToggleClick} role="menu" tabIndex={0}>
|
||||
<ul className="dd-items-left">
|
||||
{this.props.children}
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Dropdown.propTypes = {
|
||||
displayName: PropTypes.string.isRequired,
|
||||
children: PropTypes.oneOfType([
|
||||
PropTypes.array,
|
||||
PropTypes.element
|
||||
])
|
||||
}
|
||||
|
||||
export default Dropdown
|
||||
@@ -0,0 +1,19 @@
|
||||
import React from "react"
|
||||
import PropTypes from "prop-types"
|
||||
|
||||
const DropdownItem = (props) => (
|
||||
<div>
|
||||
<li className="dropdown-item">
|
||||
<button onClick={props.onClick}> {props.name} </button>
|
||||
</li>
|
||||
{props.endsSection && <div className="dropdown-divider" />}
|
||||
</div>
|
||||
)
|
||||
|
||||
DropdownItem.propTypes = {
|
||||
onClick: PropTypes.func,
|
||||
name: PropTypes.string,
|
||||
endsSection: PropTypes.bool
|
||||
}
|
||||
|
||||
export default DropdownItem
|
||||
@@ -0,0 +1,94 @@
|
||||
import React, { Component } from "react"
|
||||
import PropTypes from "prop-types"
|
||||
import YAML from "js-yaml"
|
||||
import { checkForErrors } from "../helpers/validation-helpers"
|
||||
|
||||
class AddForm extends Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
formErrors: false,
|
||||
formData: this.props.existingData ?
|
||||
this.props.getFormData( (newForm, path) => this.updateForm(newForm, path), [], this.props.existingData) :
|
||||
this.props.getFormData( (newForm, path) => this.updateForm(newForm, path), [] )
|
||||
}
|
||||
|
||||
this.updateForm = this.updateForm.bind(this)
|
||||
this.submit = this.submit.bind(this)
|
||||
}
|
||||
|
||||
submit = () => {
|
||||
const formData = this.state.formData
|
||||
|
||||
// Prevent form submission if the data was invalid.
|
||||
if (checkForErrors(formData)[1]) {
|
||||
this.setState(prevState => ({
|
||||
formErrors: true,
|
||||
formData: checkForErrors(prevState.formData)[0]
|
||||
}))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
this.setState({
|
||||
formErrors: false
|
||||
})
|
||||
|
||||
// Update the Swagger UI state.
|
||||
this.props.updateSpecJson(formData)
|
||||
|
||||
// Update the spec string in the Swagger UI state with the new json.
|
||||
const currentJson = this.props.specSelectors.specJson()
|
||||
this.props.specActions.updateSpec(YAML.safeDump(currentJson.toJS()), "insert")
|
||||
|
||||
// Perform any parent component actions for the form.
|
||||
this.props.submit()
|
||||
}
|
||||
|
||||
updateForm = (newFormData, path) => {
|
||||
this.setState(prevState => ({
|
||||
formData: prevState.formData.setIn(path, newFormData)
|
||||
}))
|
||||
}
|
||||
|
||||
render() {
|
||||
const { getComponent } = this.props
|
||||
const InsertForm = getComponent("InsertForm")
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="modal-body">
|
||||
<div className="form-container">
|
||||
<InsertForm formData={this.state.formData} getComponent={getComponent} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="modal-footer">
|
||||
{ this.state.formErrors && <div className="invalid-feedback">Please fix errors before submitting.</div>}
|
||||
<button className="btn btn-default" onClick={this.submit}>{this.props.submitButtonText}</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
AddForm.propTypes = {
|
||||
specActions: PropTypes.shape({
|
||||
updateSpec: PropTypes.func.isRequired
|
||||
}),
|
||||
specSelectors: PropTypes.shape({
|
||||
specStr: PropTypes.func.isRequired,
|
||||
specJson: PropTypes.func.isRequired
|
||||
}),
|
||||
submit: PropTypes.func.isRequired,
|
||||
submitButtonText: PropTypes.string.isRequired,
|
||||
getFormData: PropTypes.func.isRequired,
|
||||
updateSpecJson: PropTypes.func.isRequired,
|
||||
existingData: PropTypes.oneOfType([
|
||||
PropTypes.object,
|
||||
PropTypes.array
|
||||
]),
|
||||
getComponent: PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
export default AddForm
|
||||
@@ -0,0 +1,29 @@
|
||||
import React from "react"
|
||||
import PropTypes from "prop-types"
|
||||
|
||||
const FormChild = (props) => {
|
||||
const { getComponent } = props
|
||||
const FormInputWrapper = getComponent("FormInputWrapper")
|
||||
|
||||
return (
|
||||
<div key={props.name} className="card-body">
|
||||
<FormInputWrapper name={props.name} description={props.description} isRequired={props.isRequired}>
|
||||
{ !props.isRequired && <a onClick={props.flipRequired}> Add {props.name} </a> }
|
||||
{ props.isRequired && props.optional && <a onClick={props.flipRequired}> Remove {props.name} </a> }
|
||||
{ props.isRequired && props.childForm }
|
||||
</FormInputWrapper>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
FormChild.propTypes = {
|
||||
name: PropTypes.string,
|
||||
description: PropTypes.string,
|
||||
isRequired: PropTypes.bool,
|
||||
childForm: PropTypes.any.isRequired,
|
||||
flipRequired: PropTypes.func.isRequired,
|
||||
optional: PropTypes.bool,
|
||||
getComponent: PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
export default FormChild
|
||||
@@ -0,0 +1,146 @@
|
||||
import React, { Component } from "react"
|
||||
import PropTypes from "prop-types"
|
||||
import classNames from "classnames"
|
||||
|
||||
class FormDropdown extends Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
addedOptions: [],
|
||||
toBeAdded: "",
|
||||
showAddOption: false,
|
||||
isValidAddition: true
|
||||
}
|
||||
|
||||
this.updateToBeAdded = this.updateToBeAdded.bind(this)
|
||||
this.showAddField = this.showAddField.bind(this)
|
||||
this.onEnterKeyPress = this.onEnterKeyPress.bind(this)
|
||||
this.submitAdded = this.submitAdded.bind(this)
|
||||
this.onChangeWrapper = this.onChangeWrapper.bind(this)
|
||||
}
|
||||
|
||||
onEnterKeyPress = (event) => {
|
||||
if (event.key === "Enter") {
|
||||
this.submitAdded()
|
||||
}
|
||||
}
|
||||
|
||||
submitAdded = () => {
|
||||
if (this.props.isValidAddition(this.state.toBeAdded)) {
|
||||
this.setState((prevState) => {
|
||||
prevState.addedOptions.push(prevState.toBeAdded)
|
||||
return {
|
||||
addedOptions: prevState.addedOptions,
|
||||
toBeAdded: "",
|
||||
showAddOption: false
|
||||
}
|
||||
})
|
||||
} else {
|
||||
this.setState({
|
||||
isValidAddition: false
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
updateToBeAdded = (event) => {
|
||||
this.setState({
|
||||
toBeAdded: event.target.value,
|
||||
isValidAddition: this.props.isValidAddition(event.target.value)
|
||||
})
|
||||
|
||||
this.props.onChange(event)
|
||||
}
|
||||
|
||||
showAddField = () => {
|
||||
this.setState({
|
||||
showAddOption: true
|
||||
})
|
||||
|
||||
if (this.state.toBeAdded) {
|
||||
this.submitAdded()
|
||||
}
|
||||
}
|
||||
|
||||
onChangeWrapper = (event) => {
|
||||
if (event.target.value === "Please Select" || event.target.value === this.props.placeholderText) {
|
||||
const updated = event
|
||||
updated.target.value = null
|
||||
this.props.onChange(updated)
|
||||
}
|
||||
|
||||
this.props.onChange(event)
|
||||
}
|
||||
|
||||
render() {
|
||||
let addedOption = <span />
|
||||
const addButton = <a role="button" className="d-inline-block float-right" onClick={this.showAddField} onKeyDown={this.onEnterKeyPress} tabIndex={0}>Add</a>
|
||||
if (this.props.isValidAddition) {
|
||||
if (this.state.showAddOption) {
|
||||
addedOption = (
|
||||
<div>
|
||||
<input
|
||||
className="form-control"
|
||||
type="text"
|
||||
onChange={this.updateToBeAdded}
|
||||
value={this.state.toBeAdded}
|
||||
placeholder="Add Option"
|
||||
onKeyDown={this.addField}
|
||||
/>
|
||||
{addButton}
|
||||
{!this.state.isValidAddition &&
|
||||
this.props.isValidAdditionMessage &&
|
||||
<div className="invalid-feedback">
|
||||
{this.props.isValidAdditionMessage}
|
||||
</div>
|
||||
}
|
||||
</div>)
|
||||
} else {
|
||||
addedOption = addButton
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{!this.state.showAddOption &&
|
||||
<select
|
||||
value={this.props.selected || this.props.placeholderText || "Please Select"}
|
||||
onChange={this.onChangeWrapper}
|
||||
className={classNames("custom-select", {"border-danger": !this.props.isValid})}
|
||||
>
|
||||
<option value={this.props.placeholderText || "Please Select"}>
|
||||
{this.props.placeholderText || "Please Select"}
|
||||
</option>
|
||||
{(this.props.options || []).map((option, i) =>
|
||||
<option key={option + i} value={option}>{option}</option>)}
|
||||
{this.state.addedOptions.length &&
|
||||
this.state.addedOptions.map((option, i) =>
|
||||
<option key={option + i} value={option}>{option}</option>)
|
||||
}
|
||||
</select>
|
||||
}
|
||||
{addedOption}
|
||||
{!this.props.isValid &&
|
||||
<div className="invalid-feedback d-block">
|
||||
{this.props.validationMessage}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
FormDropdown.propTypes = {
|
||||
isValid: PropTypes.bool.isRequired,
|
||||
placeholderText: PropTypes.string,
|
||||
validationMessage: PropTypes.string,
|
||||
options: PropTypes.oneOfType([
|
||||
PropTypes.array,
|
||||
PropTypes.object
|
||||
]),
|
||||
onChange: PropTypes.func.isRequired,
|
||||
selected: PropTypes.string,
|
||||
isValidAddition: PropTypes.func,
|
||||
isValidAdditionMessage: PropTypes.string
|
||||
}
|
||||
|
||||
export default FormDropdown
|
||||
@@ -0,0 +1,50 @@
|
||||
import React, { Component } from "react"
|
||||
import PropTypes from "prop-types"
|
||||
|
||||
class FormInput extends Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.isNotRequiredAndEmpty = this.isNotRequiredAndEmpty.bind(this)
|
||||
}
|
||||
|
||||
isNotRequiredAndEmpty = () => !this.props.inputValue && !this.props.isRequired
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
{this.props.bigTextBox ?
|
||||
<textarea
|
||||
type="text"
|
||||
value={this.props.inputValue}
|
||||
className={`form-control ${this.props.isValid || this.isNotRequiredAndEmpty() ? "" : "border border-danger"}`}
|
||||
onChange={this.props.onChange}
|
||||
placeholder={this.props.placeholderText}
|
||||
/>:
|
||||
<input
|
||||
type="text"
|
||||
value={this.props.inputValue}
|
||||
className={`form-control ${this.props.isValid || this.isNotRequiredAndEmpty() ? "" : "border border-danger"}`}
|
||||
onChange={this.props.onChange}
|
||||
placeholder={this.props.placeholderText}
|
||||
/>}
|
||||
{!this.props.isValid && !this.isNotRequiredAndEmpty() && this.props.validationMessage &&
|
||||
<div className="invalid-feedback">
|
||||
{this.props.validationMessage}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
FormInput.propTypes = {
|
||||
isValid: PropTypes.bool,
|
||||
placeholderText: PropTypes.string,
|
||||
validationMessage: PropTypes.string,
|
||||
inputValue: PropTypes.string.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
isRequired: PropTypes.bool,
|
||||
bigTextBox: PropTypes.bool
|
||||
}
|
||||
|
||||
export default FormInput
|
||||
@@ -0,0 +1,28 @@
|
||||
import React from "react"
|
||||
import PropTypes from "prop-types"
|
||||
|
||||
const FormInputWrapper = props => (
|
||||
<div className="form-group" key={props.description}>
|
||||
<label className="input-label">
|
||||
<span className="input-label-title">
|
||||
{props.name} {props.isRequired && <span>*</span>}
|
||||
</span>
|
||||
<div>
|
||||
{props.description}
|
||||
</div>
|
||||
</label>
|
||||
{props.children}
|
||||
</div>
|
||||
)
|
||||
|
||||
FormInputWrapper.propTypes = {
|
||||
name: PropTypes.string,
|
||||
description: PropTypes.string,
|
||||
isRequired: PropTypes.bool,
|
||||
children: PropTypes.oneOfType([
|
||||
PropTypes.array,
|
||||
PropTypes.element
|
||||
])
|
||||
}
|
||||
|
||||
export default FormInputWrapper
|
||||
@@ -0,0 +1,44 @@
|
||||
import React from "react"
|
||||
import PropTypes from "prop-types"
|
||||
|
||||
const FormMap = (props) => {
|
||||
const { getComponent } = props
|
||||
const FormInput = getComponent("FormInput")
|
||||
const FormInputWrapper = getComponent("FormInputWrapper")
|
||||
|
||||
return (
|
||||
<div key={props.name}>
|
||||
<div className="map-form-left float-left">
|
||||
<FormInputWrapper name={props.name} description={props.description} isRequired={props.isRequired}>
|
||||
<FormInput
|
||||
isValid={props.isValid}
|
||||
placeholderText={props.placeholderText}
|
||||
validationMessage={props.validationMessage}
|
||||
inputValue={props.keyValue}
|
||||
onChange={props.onChange}
|
||||
isRequired={props.isRequired}
|
||||
/>
|
||||
</FormInputWrapper>
|
||||
</div>
|
||||
|
||||
<div className="map-form-right float-right">
|
||||
{props.childForm}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
FormMap.propTypes = {
|
||||
name: PropTypes.string,
|
||||
isValid: PropTypes.bool.isRequired,
|
||||
description: PropTypes.string,
|
||||
validationMessage: PropTypes.string,
|
||||
placeholderText: PropTypes.string,
|
||||
keyValue: PropTypes.string.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
isRequired: PropTypes.bool,
|
||||
childForm: PropTypes.any.isRequired,
|
||||
getComponent: PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
export default FormMap
|
||||
@@ -0,0 +1,40 @@
|
||||
import React from "react"
|
||||
import PropTypes from "prop-types"
|
||||
import { OrderedMap, Map } from "immutable"
|
||||
import { checkForEmptyValue } from "../helpers/validation-helpers"
|
||||
|
||||
const InsertForm = ({ formData, getComponent }) => {
|
||||
const InsertFormInput = getComponent("InsertFormInput")
|
||||
const formRows = []
|
||||
let i = 0
|
||||
|
||||
formData.forEach((v) => {
|
||||
if (OrderedMap.isOrderedMap(v) || Map.isMap(v)) {
|
||||
// The form field has a prerequisite field that has been filled out.
|
||||
const dependsOnNonEmpty = v.has("dependsOn") && (!checkForEmptyValue(formData.getIn(v.get("dependsOn"))) || v.get("dependsOn") === "formData")
|
||||
|
||||
if (dependsOnNonEmpty && v.has("updateOptions") && v.has("options")) {
|
||||
// There is an action to perform when the prerequisite has been filled out to update
|
||||
// the options in the dropdown.
|
||||
const dependsOnValue = formData.getIn(v.get("dependsOn"))
|
||||
const updateOptions = v.get("updateOptions")
|
||||
|
||||
formRows.push(<InsertFormInput formData={v.set("options", updateOptions(dependsOnValue, formData))} index={i} getComponent={getComponent} />)
|
||||
} else if (!v.has("dependsOn") || (!(v.has("updateOptions") && v.has("options")) && dependsOnNonEmpty )) {
|
||||
// There is no prerequisite or the prerequisite has been filled out and there is no
|
||||
// additional action to take, so simply show the form field.
|
||||
formRows.push(<InsertFormInput formData={v} index={i} getComponent={getComponent}/>)
|
||||
}
|
||||
}
|
||||
i+=1
|
||||
})
|
||||
|
||||
return <div> {formRows} </div>
|
||||
}
|
||||
|
||||
InsertForm.propTypes = {
|
||||
formData: PropTypes.object.isRequired,
|
||||
getComponent: PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
export default InsertForm
|
||||
@@ -0,0 +1,105 @@
|
||||
import React from "react"
|
||||
import PropTypes from "prop-types"
|
||||
import { OrderedMap, Map, List } from "immutable"
|
||||
import { flipRequired, onChange, addFormItem } from "../helpers/form-data-helpers"
|
||||
|
||||
const InsertFormInput = ({ getComponent, index, formData }) => {
|
||||
const FormMap = getComponent("FormMap")
|
||||
const FormChild = getComponent("FormChild")
|
||||
const FormInputWrapper = getComponent("FormInputWrapper")
|
||||
const FormDropdown = getComponent("FormDropdown")
|
||||
const InsertForm = getComponent("InsertForm")
|
||||
const InsertFormList = getComponent("InsertFormList")
|
||||
const FormInput = getComponent("FormInput")
|
||||
|
||||
let input
|
||||
const value = formData.get("value")
|
||||
|
||||
// The form represents a (key, value) pair.
|
||||
if (formData.has("keyValue")) {
|
||||
return (
|
||||
<FormMap
|
||||
key={formData.get("name")}
|
||||
name={formData.get("name")}
|
||||
description={formData.get("description")}
|
||||
isRequired={formData.get("isRequired")}
|
||||
isValid={!formData.get("hasErrors")}
|
||||
placeholderText={formData.get("placeholder")}
|
||||
validationMessage={formData.get("validationMessage")}
|
||||
keyValue={formData.get("keyValue") || ""}
|
||||
onChange={event => onChange(event, formData)}
|
||||
childForm={<InsertForm formData={value} getComponent={getComponent} />}
|
||||
getComponent={getComponent}
|
||||
/>
|
||||
)
|
||||
} else if (OrderedMap.isOrderedMap(value) || Map.isMap(value)) {
|
||||
// The form has a child form.
|
||||
return (
|
||||
<FormChild
|
||||
key={formData.get("name")}
|
||||
name={formData.get("name")}
|
||||
flipRequired={() => flipRequired(formData)}
|
||||
description={formData.get("description")}
|
||||
isRequired={formData.get("isRequired")}
|
||||
optional={formData.get("optional")}
|
||||
childForm={<InsertForm formData={value} getComponent={getComponent}/>}
|
||||
getComponent={getComponent}
|
||||
/>
|
||||
)
|
||||
} else if (formData.has("options")) {
|
||||
// The input is a dropdown-type input.
|
||||
input = (
|
||||
<FormDropdown
|
||||
key={formData.get("name")}
|
||||
isValid={!formData.get("hasErrors")}
|
||||
placeholderText={formData.get("placeholder")}
|
||||
validationMessage={formData.get("validationMessage")}
|
||||
selected={value || formData.get("placeholder") || "Please Select"}
|
||||
onChange={event => onChange(event, formData)}
|
||||
isRequired={formData.get("isRequired")}
|
||||
options={formData.get("options")}
|
||||
isValidAddition={formData.get("isValid")}
|
||||
/>)
|
||||
} else if (List.isList(value)) {
|
||||
// The element is a list-control.
|
||||
const childForm = <InsertFormList formData={value} parent={formData} getComponent={getComponent}/>
|
||||
|
||||
input = (
|
||||
<div key={formData.get("name")}>
|
||||
{childForm}
|
||||
<a role="button" className="d-inline-block float-right" onClick={() => addFormItem(formData)}>Add {formData.get("name")}</a>
|
||||
</div>
|
||||
)
|
||||
} else { // The element is a basic input.
|
||||
input = (
|
||||
<FormInput
|
||||
key={formData.get("name")}
|
||||
isValid={!formData.get("hasErrors")}
|
||||
placeholderText={formData.get("placeholder")}
|
||||
validationMessage={formData.get("validationMessage")}
|
||||
inputValue={value || ""}
|
||||
onChange={event => onChange(event, formData)}
|
||||
isRequired={formData.get("isRequired")}
|
||||
bigTextBox={formData.get("bigTextBox")}
|
||||
/>)
|
||||
}
|
||||
|
||||
return (
|
||||
<FormInputWrapper
|
||||
key={`${formData.get("name")}-${index}` }
|
||||
name={formData.get("name")}
|
||||
description={formData.get("description")}
|
||||
isRequired={formData.get("isRequired")}
|
||||
>
|
||||
{ input }
|
||||
</FormInputWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
InsertFormInput.propTypes = {
|
||||
formData: PropTypes.object.isRequired,
|
||||
index: PropTypes.number.isRequired,
|
||||
getComponent: PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
export default InsertFormInput
|
||||
@@ -0,0 +1,45 @@
|
||||
import React from "react"
|
||||
import PropTypes from "prop-types"
|
||||
import { OrderedMap, Map, List } from "immutable"
|
||||
import { removeFormItem } from "../helpers/form-data-helpers"
|
||||
|
||||
const InsertFormList = ({ parent, formData, getComponent }) =>
|
||||
{
|
||||
const InsertForm = getComponent("InsertForm")
|
||||
const InsertFormInput = getComponent("InsertFormInput")
|
||||
|
||||
if (!List.isList(formData)) {
|
||||
return (<div> An error occurred while loading the form.</div>)
|
||||
}
|
||||
|
||||
const jsx = []
|
||||
|
||||
formData.forEach((item, index) => {
|
||||
const showClose = index === formData.count() - 1
|
||||
const close = (
|
||||
<span type="button" className="close remove-item pull-right" aria-label="remove" onClick={() => removeFormItem(parent)}>
|
||||
<span aria-hidden="true">×</span>
|
||||
</span>
|
||||
)
|
||||
|
||||
jsx.push((
|
||||
<div className="card-body" key={`index-${index}`}>
|
||||
{showClose && close}
|
||||
{((OrderedMap.isOrderedMap(item) || Map.isMap(item)) && item.has("value")) ?
|
||||
<InsertFormInput formData={item} index={index} getComponent={getComponent} /> :
|
||||
<InsertForm formData={item} getComponent={getComponent} />
|
||||
}
|
||||
</div>
|
||||
))
|
||||
})
|
||||
|
||||
return <div> {jsx} </div>
|
||||
}
|
||||
|
||||
InsertFormList.propTypes = {
|
||||
formData: PropTypes.object.isRequired,
|
||||
getComponent: PropTypes.func.isRequired,
|
||||
parent: PropTypes.object
|
||||
}
|
||||
|
||||
export default InsertFormList
|
||||
@@ -0,0 +1,124 @@
|
||||
## Form Data Readme
|
||||
|
||||
A "Form Data" object is an immutable js object. It is used to describe a form that will be automatically generated from the object and to validate the inputs to that form.
|
||||
|
||||
The intent of a "Form Data" object is to allow easily-reusable forms that correspond to objects defined in the Open Api 3.0 specification. The helper methods for Form Objects are found in the helpers folder.
|
||||
|
||||
### UI Generation from forms
|
||||
The UI for a given form object is generated by the ```getForm``` method in form-data-helpers.js.
|
||||
|
||||
### Validation
|
||||
Validation for a given form is handled by the ```checkForErrors``` method in validation-helpers.js
|
||||
|
||||
### Files in the /form-objects folder
|
||||
These files describe a corresponding object in the Open Api specification using the object format. The somenameForm method in these files returns a form object describing an open api object. The somenameObject method takes in a somenameForm object and produces a javascript object that is a valid sub-piece of an open api document.
|
||||
|
||||
## Object format
|
||||
```
|
||||
fieldIdentifier: {
|
||||
isRequired: boolean,
|
||||
updateForm: required function,
|
||||
hasErrors: boolean,
|
||||
value: required form data object || List of forms || string,
|
||||
isValid: optional function
|
||||
validationMessage: optional string
|
||||
description: optional string,
|
||||
name: required string,
|
||||
keyValue: optional string,
|
||||
options: optional array of strings,
|
||||
dependsOn: optional array of strings,
|
||||
updateOptions: optional function(dependsOn value)
|
||||
}
|
||||
```
|
||||
|
||||
### Properties
|
||||
|
||||
#### isRequired
|
||||
Defaults to false if not specified.
|
||||
Whether or not the generated form field is required. If isRequired is true, then required field validation will be automatically included.
|
||||
|
||||
#### updateForm
|
||||
A function that updates the form.
|
||||
```
|
||||
(updatedObject, pathToObject) => {
|
||||
// UPDATE the form data object in the parent state
|
||||
}
|
||||
```
|
||||
#### hasErrors
|
||||
Defaults to false if not specified.
|
||||
Used for validation purposes. Whether or not the form object or any children form objects have validation errors. Updated by ```checkForErrors(formData)```.
|
||||
|
||||
### value
|
||||
The value of the form field (from user input) or a child form object. Form fields will be recursively generated for child form objects. If this value is a list, then a "list control" form will be generated.
|
||||
|
||||
#### isValid
|
||||
Optional function for validation of the value entered in the form field. Should return false if errors were found and true if the field is valid.
|
||||
```
|
||||
(value) => {
|
||||
// Return boolean indicating whether the value is valid.
|
||||
}
|
||||
```
|
||||
#### validationMessage
|
||||
Optional validation message that will be shown when the field contains an invalid input.
|
||||
|
||||
#### description
|
||||
Optional description of the form field that will be displayed under the form name.
|
||||
|
||||
#### name
|
||||
The name that will be displayed above the form field.
|
||||
|
||||
#### keyValue
|
||||
If keyValue is provided, then a form will be generated with a keyValue text input on the left and the "value" form input or child form on the right. This form is a "map" (key, value) pair, with keyValue as the user-entered key and value as the user-entered value.
|
||||
|
||||
#### options
|
||||
If options is provided, the generated form field will be a dropdown.
|
||||
|
||||
#### dependsOn & updateOptions
|
||||
*Field with these properties must represent a dropdown. (i.e. have property "options")*
|
||||
|
||||
These properties represent a way for a dropdown be conditionally rendered in the UI based on whether a prerequisite field containing necessary information for populating the dropdown options has been filled out.
|
||||
|
||||
##### dependsOn
|
||||
Path to the value the dropdown field depends on. If the value at path ```dependsOn``` is not defined, the dropdown UI for the field will be hidden.
|
||||
|
||||
##### updateOptions
|
||||
Function that will be called on the prerequisite value when the prerequisite value is filled out/selected by the user.
|
||||
```
|
||||
(value the field depends on) => {
|
||||
// return options for the field based on the value the field
|
||||
}
|
||||
```
|
||||
##### Example:
|
||||
"petType" is a dropdown to select a type of pet.
|
||||
"breed" is a dropdown to select a breed of that type of pet.
|
||||
|
||||
if "cats" is selected in the pet type drop then the "breed" dropdown will appear with the options 'main coon', 'persian', and 'bengal'.
|
||||
|
||||
```
|
||||
const getOptions = (petType) => {
|
||||
switch(petType):
|
||||
case 'dogs':
|
||||
return ['golden retreiver', 'pug', 'poodle']
|
||||
case 'cats':
|
||||
return ['maine coon', 'persian', 'bengal']
|
||||
default:
|
||||
return []
|
||||
}
|
||||
|
||||
const selectPetBreedForm = (updateForm) => fromJS({
|
||||
petType: {
|
||||
name: 'Pet Type'
|
||||
value: '',
|
||||
options: ['dogs', 'cats'],
|
||||
updateForm: e => updateForm(e, path.concat(['animalType']))
|
||||
},
|
||||
breed: {
|
||||
name: 'Breed'
|
||||
value: '',
|
||||
options: [],
|
||||
updateForm: e => updateForm(e, path.concat(['breed']))
|
||||
dependsOn: ['petType', 'value']
|
||||
updateOptions: getOptions
|
||||
}
|
||||
})
|
||||
```
|
||||
@@ -0,0 +1,48 @@
|
||||
import { fromJS } from "immutable"
|
||||
import { selectOperationForm, selectOperationObject } from "./select-operation"
|
||||
|
||||
const tagItem = (updateForm, path) =>
|
||||
fromJS({
|
||||
tag: {
|
||||
value: "",
|
||||
isRequired: true,
|
||||
name: "Tag",
|
||||
description: "REQUIRED. The name of the tag.",
|
||||
validationMessage: "Please enter a tag name. The field is required.",
|
||||
updateForm: newForm => updateForm(newForm, path.concat(["tag"]))
|
||||
}
|
||||
})
|
||||
|
||||
export const addOperationTagsForm = (updateForm, path, existing) =>
|
||||
fromJS({
|
||||
selectoperation: {
|
||||
name: "Select an operation to add tags to.",
|
||||
value: selectOperationForm(updateForm, path.concat(["selectoperation", "value"]), existing),
|
||||
isRequired: true,
|
||||
updateForm: newForm => updateForm(newForm, path.concat(["selectoperation"]))
|
||||
},
|
||||
tags: {
|
||||
value: [],
|
||||
dependsOn: ["selectoperation", "value", "operation", "value"],
|
||||
name: "Tags",
|
||||
description: "A list of tags for API documentation control. Tags can be used for logical grouping of operations by resources or any other qualifier.",
|
||||
updateForm: newForm => updateForm(newForm, path.concat(["tags"])),
|
||||
defaultItem: i => tagItem(updateForm, path.concat(["tags", "value", i]))
|
||||
}
|
||||
})
|
||||
|
||||
export const addOperationTagsObject = (formData) => {
|
||||
const parsedTags = []
|
||||
const tags = formData.getIn(["tags", "value"])
|
||||
|
||||
tags.forEach((tag) => {
|
||||
parsedTags.push(tag.getIn(["tag", "value"]))
|
||||
})
|
||||
|
||||
const selectedAndTags = {
|
||||
selectedOperation: selectOperationObject(formData.getIn(["selectoperation", "value"])),
|
||||
tags: parsedTags
|
||||
}
|
||||
|
||||
return selectedAndTags
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import { fromJS } from "immutable"
|
||||
import { validateUrl } from "../helpers/validation-helpers"
|
||||
|
||||
export const contactForm = (updateForm, path) =>
|
||||
fromJS({
|
||||
value: {
|
||||
name: {
|
||||
name: "Name",
|
||||
value: "",
|
||||
updateForm: newForm => updateForm(newForm, path.concat(["value", "name"]))
|
||||
},
|
||||
url: {
|
||||
name: "URL",
|
||||
value: "",
|
||||
updateForm: newForm => updateForm(newForm, path.concat(["value", "url"])),
|
||||
isValid: value => validateUrl(value),
|
||||
validationMessage: "Please enter a valid URL."
|
||||
},
|
||||
email: {
|
||||
name: "Email",
|
||||
value: "",
|
||||
updateForm: newForm => updateForm(newForm, path.concat(["value", "email"]))
|
||||
}
|
||||
},
|
||||
name: "Contact",
|
||||
description: "The contact information for the exposed API.",
|
||||
updateForm: newForm => updateForm(newForm, path)
|
||||
})
|
||||
|
||||
export const contactObject = (formData) => {
|
||||
const name = formData.getIn(["value", "name", "value"])
|
||||
const url = formData.getIn(["value", "url", "value"])
|
||||
const email = formData.getIn(["value", "email", "value"])
|
||||
const contact = {}
|
||||
|
||||
if (!name && !url && !email) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (email) {
|
||||
contact.email = email
|
||||
}
|
||||
|
||||
if (name) {
|
||||
contact.name = name
|
||||
}
|
||||
|
||||
if (url) {
|
||||
contact.url = url
|
||||
}
|
||||
|
||||
return contact
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { fromJS } from "immutable"
|
||||
import { selectResponseObject, selectResponseForm } from "./select-response"
|
||||
|
||||
export const exampleForm = (updateForm, path, existing) => (
|
||||
fromJS({
|
||||
selectresponse: {
|
||||
name: "Select Response",
|
||||
value: selectResponseForm(updateForm, path.concat(["selectresponse", "value"]), existing),
|
||||
isRequired: true,
|
||||
description: "Select the location in the document where you wish to add an example."
|
||||
},
|
||||
exampleName: {
|
||||
name: "Example Name",
|
||||
description: "The name of the sample response.",
|
||||
value: "",
|
||||
updateForm: event => updateForm(event, path.concat(["exampleName"])),
|
||||
isRequired: true,
|
||||
dependsOn: ["selectresponse", "value", "mediatype", "value"]
|
||||
},
|
||||
exampleValue: {
|
||||
name: "Example Value",
|
||||
value: "",
|
||||
bigTextBox: true,
|
||||
updateForm: event => updateForm(event, path.concat(["exampleValue"])),
|
||||
description: "The value of the sample response. This can be an arbitrary string, json, xml, etc.",
|
||||
isRequired: true,
|
||||
dependsOn: ["selectresponse", "value", "mediatype", "value"]
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
export const exampleObject = (formData) => {
|
||||
const responsePath = selectResponseObject(formData.getIn(["selectresponse", "value"]))
|
||||
const exampleName = formData.getIn(["exampleName", "value"])
|
||||
const exampleValue = formData.getIn(["exampleValue", "value"])
|
||||
|
||||
return {
|
||||
responsePath: [...responsePath, "examples"],
|
||||
exampleName,
|
||||
exampleValue
|
||||
}
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
import { fromJS } from "immutable"
|
||||
|
||||
export const externalDocumentationForm = (updateForm, path) =>
|
||||
fromJS({
|
||||
url: {
|
||||
value: "",
|
||||
isRequired: true,
|
||||
name: "URL",
|
||||
description: "REQUIRED. The URL for the target documentation. Value MUST be in the format of a URL.",
|
||||
updateForm: event => updateForm(event, path.concat(["url"])),
|
||||
validationMessage: "Please enter a valid URL."
|
||||
},
|
||||
description: {
|
||||
value: "",
|
||||
name: "Description",
|
||||
description: "A short description of the target documentation. CommonMark syntax MAY be used for rich text representation.",
|
||||
updateForm: event => updateForm(event, path.concat(["description"]))
|
||||
}
|
||||
})
|
||||
|
||||
export const externalDocumentationObject = (formData) => {
|
||||
const url = formData.getIn(["url", "value"])
|
||||
const description = formData.getIn(["description", "value"])
|
||||
|
||||
if (!url && !description) {
|
||||
return null
|
||||
}
|
||||
|
||||
const externalDocumentation = {}
|
||||
|
||||
if (url) {
|
||||
externalDocumentation.url = url
|
||||
}
|
||||
|
||||
if (description) {
|
||||
externalDocumentation.description = description
|
||||
}
|
||||
|
||||
return externalDocumentation
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import { fromJS } from "immutable"
|
||||
import { licenseForm, licenseObject } from "./license-object"
|
||||
import { contactForm, contactObject } from "./contact-object"
|
||||
|
||||
export const infoForm = (updateForm, path, existingValues) =>
|
||||
fromJS({
|
||||
title: {
|
||||
value: existingValues ? existingValues.get("title") : "",
|
||||
isRequired: true,
|
||||
name: "Title",
|
||||
description: "REQUIRED. The title of the application.",
|
||||
updateForm: newForm => updateForm(newForm, path.concat(["title"])),
|
||||
validationMessage: "Please enter a title. The field is required."
|
||||
},
|
||||
description: {
|
||||
value: existingValues ? existingValues.get("description") : "",
|
||||
name: "Description",
|
||||
description: "A short description of the application. CommonMark syntax MAY be used for rich text representation.",
|
||||
updateForm: newForm => updateForm(newForm, path.concat(["description"]))
|
||||
},
|
||||
version: {
|
||||
value: existingValues ? existingValues.get("version") : "",
|
||||
isRequired: true,
|
||||
name: "Version",
|
||||
description: "REQUIRED. The version of the OpenAPI document (which is distinct from the OpenAPI Specification version or the API implementation version).",
|
||||
updateForm: newForm => updateForm(newForm, path.concat(["version"])),
|
||||
validationMessage: "Please enter a version. The version field is required."
|
||||
},
|
||||
termsofservice: {
|
||||
value: existingValues ? existingValues.get("termsofservice") : "",
|
||||
name: "Terms of Service",
|
||||
description: "A URL to the Terms of Service for the API. MUST be in the format of a URL.",
|
||||
updateForm: newForm => updateForm(newForm, path.concat(["termsofservice"]))
|
||||
},
|
||||
license: licenseForm(updateForm, path.concat(["license"]), existingValues ? existingValues.get("license") : ""),
|
||||
contact: contactForm(updateForm, path.concat(["contact"]))
|
||||
})
|
||||
|
||||
export const infoObject = (formData) => {
|
||||
const newInfo = {
|
||||
title: formData.getIn(["title", "value"]),
|
||||
version: formData.getIn(["version", "value"])
|
||||
}
|
||||
|
||||
const description = formData.getIn(["description", "value"])
|
||||
const termsOfService = formData.getIn(["termsofservice", "value"])
|
||||
|
||||
if (description) {
|
||||
newInfo.description = description
|
||||
}
|
||||
|
||||
if (termsOfService) {
|
||||
newInfo.termsOfService = termsOfService
|
||||
}
|
||||
|
||||
const contact = contactObject(formData.getIn(["contact"]))
|
||||
if (contact) {
|
||||
newInfo.contact = contact
|
||||
}
|
||||
|
||||
const license = licenseObject(formData.getIn(["license"]))
|
||||
if (license) {
|
||||
newInfo.license = license
|
||||
}
|
||||
|
||||
return newInfo
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import { fromJS } from "immutable"
|
||||
import { validateUrl } from "../helpers/validation-helpers"
|
||||
|
||||
export const licenseForm = (updateForm, path, existingValues) =>
|
||||
fromJS({
|
||||
value: {
|
||||
name: {
|
||||
name: "Name",
|
||||
value: existingValues ? existingValues.get("name") : "",
|
||||
isRequired: true,
|
||||
updateForm: newForm => updateForm(newForm, path.concat(["value", "name"]))
|
||||
},
|
||||
url: {
|
||||
name: "URL",
|
||||
value: existingValues ? existingValues.get("url") : "",
|
||||
hasErrors: !validateUrl(existingValues ? existingValues.get("url") : ""),
|
||||
updateForm: newForm => updateForm(newForm, path.concat(["value", "url"])),
|
||||
isValid: value => validateUrl(value),
|
||||
validationMessage: "Please enter a valid URL."
|
||||
}
|
||||
},
|
||||
name: "License",
|
||||
description: "The license information for the exposed API.",
|
||||
updateForm: newForm => updateForm(newForm, path)
|
||||
})
|
||||
|
||||
export const licenseObject = (formData) => {
|
||||
const name = formData.getIn(["value", "name", "value"])
|
||||
const url = formData.getIn(["value", "url", "value"])
|
||||
const newLicense = {}
|
||||
|
||||
if (!name && !url) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (name) {
|
||||
newLicense.name = name
|
||||
}
|
||||
|
||||
if (url) {
|
||||
newLicense.url = url
|
||||
}
|
||||
|
||||
return newLicense
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
import { fromJS } from "immutable"
|
||||
|
||||
const tagItem = (updateForm, path) =>
|
||||
fromJS({
|
||||
tag: {
|
||||
value: "",
|
||||
isRequired: true,
|
||||
name: "Tag",
|
||||
description: "REQUIRED. The name of the tag.",
|
||||
validationMessage: "Please enter a tag name. The field is required.",
|
||||
updateForm: newForm => updateForm(newForm, path.concat(["tag"]))
|
||||
}
|
||||
})
|
||||
|
||||
export const operationForm = (updateForm, path, existingPaths) =>
|
||||
fromJS({
|
||||
path: {
|
||||
value: "",
|
||||
isRequired: true,
|
||||
name: "Path",
|
||||
description: "REQUIRED. The path to add the operation to.",
|
||||
updateForm: event => updateForm(event, path.concat(["path"])),
|
||||
validationMessage: "Please select a path. The field is required.",
|
||||
options: existingPaths || ["Please Select"],
|
||||
isValid: () => true
|
||||
},
|
||||
operation: {
|
||||
value: "",
|
||||
isRequired: true,
|
||||
name: "Operation",
|
||||
description: "REQUIRED. Select an operation.",
|
||||
updateForm: event => updateForm(event, path.concat(["operation"])),
|
||||
validationMessage: "Please select an operation. The field is required.",
|
||||
options: ["get", "put", "post", "delete", "options", "head", "patch", "trace"]
|
||||
},
|
||||
summary: {
|
||||
value: "",
|
||||
name: "Summary",
|
||||
description: "Add a short summary of what the operation does.",
|
||||
updateForm: event => updateForm(event, path.concat(["summary"])),
|
||||
validationMessage: "Please enter a version. The version field is required."
|
||||
},
|
||||
description: {
|
||||
value: "",
|
||||
name: "Description",
|
||||
description: "A verbose explanation of the operation behavior. CommonMark syntax MAY be used for rich text representation.",
|
||||
hasErrors: false,
|
||||
updateForm: event => updateForm(event, path.concat(["description"]))
|
||||
},
|
||||
operationid:{
|
||||
value: "",
|
||||
name: "Operation ID",
|
||||
description: "Unique string used to identify the operation. The id MUST be unique among all operations described in the API. Tools and libraries MAY use the operationId to uniquely identify an operation, therefore, it is RECOMMENDED to follow common programming naming conventions.",
|
||||
updateForm: event => updateForm(event, path.concat(["operationid"]))
|
||||
},
|
||||
tags: {
|
||||
value: [],
|
||||
name: "Tags",
|
||||
description: "A list of tags for API documentation control. Tags can be used for logical grouping of operations by resources or any other qualifier.",
|
||||
updateForm: newForm => updateForm(newForm, path.concat(["tags"])),
|
||||
defaultItem: i => tagItem(updateForm, path.concat(["tags", "value", i]))
|
||||
}
|
||||
})
|
||||
|
||||
export const operationObject = (formData) => {
|
||||
const parsedTags = []
|
||||
const tags = formData.getIn(["tags", "value"])
|
||||
|
||||
tags.forEach((tag) => {
|
||||
parsedTags.push(tag.getIn(["tag", "value"]))
|
||||
})
|
||||
|
||||
const newOp = {
|
||||
summary: formData.getIn(["summary", "value"]),
|
||||
description: formData.getIn(["description", "value"]),
|
||||
operationId: formData.getIn(["operationid", "value"]),
|
||||
responses: {
|
||||
default: {
|
||||
description: "Default error sample response"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (parsedTags.length) {
|
||||
newOp.tags = parsedTags
|
||||
}
|
||||
|
||||
if (!formData.getIn(["path", "value"])) {
|
||||
return
|
||||
}
|
||||
|
||||
return newOp
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import { fromJS } from "immutable"
|
||||
|
||||
export const pathForm = (updateForm, path) =>
|
||||
fromJS({
|
||||
path: {
|
||||
value: "",
|
||||
isRequired: true,
|
||||
name: "Path",
|
||||
description: "REQUIRED. The path to add.",
|
||||
updateForm: event => updateForm(event, path.concat(["path"])),
|
||||
validationMessage: "Please enter a path starting with a '/'. The field is required.",
|
||||
isValid: (value) => value.startsWith("/")
|
||||
},
|
||||
summary: {
|
||||
value: "",
|
||||
name: "Summary",
|
||||
description: "Enter a summary of the path.",
|
||||
updateForm: event => updateForm(event, path.concat(["summary"])),
|
||||
validationMessage: "Please select an operation. The field is required."
|
||||
},
|
||||
description: {
|
||||
value: "",
|
||||
name: "Description",
|
||||
description: "An optional, string description, intended to apply to all operations in this path. CommonMark syntax MAY be used for rich text representation.",
|
||||
updateForm: event => updateForm(event, path.concat(["description"]))
|
||||
}
|
||||
})
|
||||
|
||||
export const pathObject = (formData) => {
|
||||
const pathSummary = formData.getIn(["summary", "value"])
|
||||
const pathDescription = formData.getIn(["description", "value"])
|
||||
const newPath = { key: formData.getIn(["path", "value"]), value: {} }
|
||||
|
||||
if (pathSummary) {
|
||||
newPath["value"]["summary"] = pathSummary
|
||||
}
|
||||
|
||||
if (pathDescription) {
|
||||
newPath["value"]["description"] = pathDescription
|
||||
}
|
||||
|
||||
return newPath
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { fromJS } from "immutable"
|
||||
|
||||
export const selectOperationForm = (updateForm, path, existing) => (
|
||||
fromJS({
|
||||
path: {
|
||||
value: "",
|
||||
isRequired: true,
|
||||
name: "Path",
|
||||
description: "REQUIRED. The path the operation is under.",
|
||||
updateForm: event => updateForm(event, path.concat(["path"])),
|
||||
validationMessage: "Please select a path. The field is required.",
|
||||
options: existing ? existing.getPaths() : [],
|
||||
isValid: () => true
|
||||
},
|
||||
operation: {
|
||||
value: "",
|
||||
isRequired: true,
|
||||
name: "Operation",
|
||||
description: "REQUIRED. Select an operation.",
|
||||
updateForm: event => updateForm(event, path.concat(["operation"])),
|
||||
validationMessage: "Please select an operation. The field is required.",
|
||||
options: [],
|
||||
dependsOn: ["path", "value"],
|
||||
updateOptions: existing ? existing.getOperations : () => []
|
||||
}
|
||||
}))
|
||||
|
||||
export const selectOperationObject = (formData) => {
|
||||
const path = ["paths"]
|
||||
path.push(formData.getIn(["path", "value"]))
|
||||
path.push(formData.getIn(["operation", "value"]))
|
||||
|
||||
return path
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import { fromJS } from "immutable"
|
||||
import { selectOperationObject, selectOperationForm } from "./select-operation"
|
||||
|
||||
|
||||
const selectResponse = (updateForm, path, existing) => fromJS({
|
||||
response: {
|
||||
value: "",
|
||||
isRequired: true,
|
||||
name: "Response",
|
||||
description: "REQUIRED. The response to add the example to.",
|
||||
updateForm: event => updateForm(event, path.concat(["response"])),
|
||||
validationMessage: "Please select a response to add the example to. The field is required.",
|
||||
options: ["Please Select Or Add Response"],
|
||||
dependsOn: ["operation", "value"],
|
||||
updateOptions: existing ? existing.getResponses: () => [],
|
||||
isValid: () => true
|
||||
},
|
||||
mediatype: {
|
||||
value: "",
|
||||
isRequired: true,
|
||||
name: "Media Type",
|
||||
description: "REQUIRED. The media type of the response. For example, text/plain or application/json.",
|
||||
options: ["Please Select Or Add Media Type"],
|
||||
dependsOn: ["response", "value"],
|
||||
updateForm: event => updateForm(event, path.concat(["mediatype"])),
|
||||
updateOptions: existing ? existing.getMediaTypes : () => [],
|
||||
isValid: () => true,
|
||||
validationMessage: "Please select or add a media type for the example. The field is required."
|
||||
}
|
||||
})
|
||||
|
||||
export const selectResponseForm = (updateForm, path, existing) =>
|
||||
selectOperationForm(updateForm, path, existing)
|
||||
.merge(selectResponse(updateForm, path, existing))
|
||||
|
||||
|
||||
export const selectResponseObject= (formData) => {
|
||||
const path = selectOperationObject(formData)
|
||||
path.push("responses")
|
||||
path.push(formData.getIn(["response", "value"]))
|
||||
path.push("content")
|
||||
path.push(formData.getIn(["mediatype", "value"]))
|
||||
|
||||
return path
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
import { fromJS } from "immutable"
|
||||
|
||||
const enumFormItem = (j, updateForm, path) => fromJS({
|
||||
name: "Enum Value",
|
||||
description: "A value in the enumeration of possible variable values.",
|
||||
isRequired: false,
|
||||
hasErrors: false,
|
||||
value: "",
|
||||
updateForm: newForm => updateForm(newForm, path.concat(["value", j]))
|
||||
})
|
||||
|
||||
const serverVariableFormItem = (i, updateForm, path) => fromJS({
|
||||
isRequired: true,
|
||||
name: "Variable Name",
|
||||
keyValue: "",
|
||||
description: "The name of the server variable.",
|
||||
value: {
|
||||
default: {
|
||||
value: "",
|
||||
isRequired: true,
|
||||
name: "Default",
|
||||
description: "REQUIRED. The default value to use for substitution, and to send, if an alternate value is not supplied. Unlike the Schema Object's default, this value MUST be provided by the consumer.",
|
||||
updateForm: newForm => updateForm(newForm, path.concat(["value", i, "value", "default"]))
|
||||
},
|
||||
enum: {
|
||||
value: [enumFormItem(0, updateForm, path.concat(["value", i, "value", "enum"]))],
|
||||
name: "Enum",
|
||||
defaultItem: j => enumFormItem(j, updateForm, path.concat(["value", i, "value", "enum"])),
|
||||
description: "An enumeration of string values to be used if the substitution options are from a limited set.",
|
||||
updateForm: newForm => updateForm(newForm, path.concat(["value", i, "value", "enum"]))
|
||||
},
|
||||
vardescription: {
|
||||
value: "",
|
||||
name: "Description",
|
||||
description: "A short description of the tag. CommonMark syntax MAY be used for rich text representation.",
|
||||
updateForm: newForm => updateForm(newForm, path.concat(["value", i, "value", "vardescription"]))
|
||||
}
|
||||
},
|
||||
updateForm: newForm => updateForm(newForm, path.concat(["value", i]))
|
||||
})
|
||||
|
||||
export const serverVariableForm = (updateForm, path) =>
|
||||
fromJS({
|
||||
value: [],
|
||||
name: "Server Variables",
|
||||
description: "A map between a variable name and its value. The value is used for substitution in the server's URL template.",
|
||||
updateForm: newForm => updateForm(newForm, path),
|
||||
defaultItem: i => serverVariableFormItem(i, updateForm, path)
|
||||
})
|
||||
|
||||
export const serverVariableObject = (formData) => {
|
||||
const variables = formData.get("value")
|
||||
const newVariables = {}
|
||||
|
||||
variables.forEach((variable) => {
|
||||
const varName = variable.getIn(["keyValue"])
|
||||
const varValue = variable.getIn(["value"])
|
||||
|
||||
const enumVal = varValue.getIn(["enum", "value"])
|
||||
const enumValues = []
|
||||
|
||||
if (enumVal) {
|
||||
enumVal.forEach((option) => {
|
||||
enumValues.push(option.get("value"))
|
||||
})
|
||||
}
|
||||
|
||||
const newVariable = {
|
||||
default: varValue.getIn(["default", "value"]),
|
||||
enum: enumValues,
|
||||
description: varValue.getIn(["vardescription", "value"])
|
||||
}
|
||||
|
||||
newVariables[varName] = newVariable
|
||||
})
|
||||
|
||||
return newVariables
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import { fromJS } from "immutable"
|
||||
import { serverVariableForm, serverVariableObject } from "./server-variable-object"
|
||||
|
||||
const serverFormItem = (i, updateForm, path) => fromJS({
|
||||
url: {
|
||||
value: "",
|
||||
isRequired: true,
|
||||
name: "URL",
|
||||
description: "REQUIRED. A URL to the target host. This URL supports Server Variables and MAY be relative, to indicate that the host location is relative to the location where the OpenAPI document is being served. Variable substitutions will be made when a variable is named in {brackets}.",
|
||||
validationMessage: "Please enter a URL. The field is required.",
|
||||
updateForm: newForm => updateForm(newForm, path.concat(["servers", "value", i, "url"]))
|
||||
},
|
||||
description: {
|
||||
value: "",
|
||||
name: "Description",
|
||||
description: "An optional string describing the host designated by the URL. CommonMark syntax MAY be used for rich text representation.",
|
||||
updateForm: newForm => updateForm(newForm, path.concat(["servers", "value", i, "description"]))
|
||||
},
|
||||
variables: serverVariableForm(updateForm, path.concat(["servers", "value", i, "variables"]))
|
||||
})
|
||||
|
||||
export const serversForm = (updateForm, path) =>
|
||||
fromJS({
|
||||
servers: {
|
||||
value: [serverFormItem(0, updateForm, path)],
|
||||
name: "Server",
|
||||
description: "An object representing a Server.",
|
||||
updateForm: newForm => updateForm(newForm, path.concat(["servers"])),
|
||||
defaultItem: i => serverFormItem(i, updateForm, path)
|
||||
}
|
||||
})
|
||||
|
||||
export const serversObject = (formData) => {
|
||||
const servers = formData.getIn(["servers", "value"])
|
||||
const newServers = []
|
||||
|
||||
servers.forEach((server) => {
|
||||
const newServer = {}
|
||||
const variables = serverVariableObject(server.get("variables"))
|
||||
const description = server.getIn(["description", "value"])
|
||||
const url = server.getIn(["url", "value"])
|
||||
|
||||
if (url) {
|
||||
newServer.url = url
|
||||
}
|
||||
|
||||
if (variables) {
|
||||
newServer.variables = variables
|
||||
}
|
||||
|
||||
if (description) {
|
||||
newServer.description = description
|
||||
}
|
||||
|
||||
newServers.push(newServer)
|
||||
})
|
||||
|
||||
return newServers
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import { fromJS } from "immutable"
|
||||
import { externalDocumentationObject, externalDocumentationForm } from "./external-documentation-object"
|
||||
|
||||
export const tagForm = (updateForm, path) =>
|
||||
fromJS({
|
||||
name: {
|
||||
value: "",
|
||||
isRequired: true,
|
||||
name: "Name",
|
||||
description: "REQUIRED. The name of the tag.",
|
||||
validationMessage: "Please enter a tag name. The name field is required.",
|
||||
updateForm: newForm => updateForm(newForm, path.concat(["name"]))
|
||||
},
|
||||
description: {
|
||||
value: "",
|
||||
name: "Description",
|
||||
description: "A short description of the tag. CommonMark syntax MAY be used for rich text representation.",
|
||||
updateForm: newForm => updateForm(newForm, path.concat(["description"]))
|
||||
},
|
||||
externalDocs: {
|
||||
value: externalDocumentationForm(updateForm, path.concat(["externalDocs", "value"])),
|
||||
name: "External Documentation",
|
||||
updateForm: newForm => updateForm(newForm, path.concat(["externalDocs"]))
|
||||
}
|
||||
})
|
||||
|
||||
export const tagObject = (formData) => {
|
||||
const name = formData.getIn(["name", "value"])
|
||||
const description = formData.getIn(["description", "value"])
|
||||
const externalDocs = formData.getIn(["externalDocs", "value"])
|
||||
|
||||
const externalDocsObject = externalDocumentationObject(externalDocs)
|
||||
const tagObject = {}
|
||||
|
||||
if (!name && !description && !externalDocsObject) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (name) {
|
||||
tagObject.name = name
|
||||
}
|
||||
|
||||
if (description) {
|
||||
tagObject.description = description
|
||||
}
|
||||
|
||||
if (externalDocsObject) {
|
||||
tagObject.externalDocs = externalDocsObject
|
||||
}
|
||||
|
||||
return tagObject
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { fromJS } from "immutable"
|
||||
import { tagObject, tagForm } from "./tag-object"
|
||||
|
||||
export const tagsForm = (updateForm, path) =>
|
||||
fromJS({
|
||||
tags: {
|
||||
value: [tagForm(updateForm, path.concat(["tags", "value", 0]))],
|
||||
name: "Tag Declarations",
|
||||
description: "A list of tags used by the specification with additional metadata. The order of the tags can be used to reflect on their order by the parsing tools. Not all tags that are used by the Operation Object must be declared. The tags that are not declared MAY be organized randomly or based on the tools' logic. Each tag name in the list MUST be unique.",
|
||||
updateForm: newForm => updateForm(newForm, path.concat(["tags"])),
|
||||
defaultItem: i => tagForm(updateForm, path.concat(["tags", "value", i]))
|
||||
}
|
||||
})
|
||||
|
||||
export const tagsObject = (formData) => {
|
||||
const tags = formData.getIn(["tags", "value"])
|
||||
const tagsObject = []
|
||||
|
||||
tags.forEach((tag) => {
|
||||
const newTag = tagObject(tag)
|
||||
tagsObject.push(newTag)
|
||||
})
|
||||
|
||||
return tagsObject
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import { OrderedMap, Map, List } from "immutable"
|
||||
import { checkForEmptyValue } from "./validation-helpers"
|
||||
|
||||
// Updates a form input given an onChange event,
|
||||
// the location of the form input data in the form data object, and a function
|
||||
// 'updateForm' that will update the form data.
|
||||
export const onChange = (event, formData ) => {
|
||||
let updatedField
|
||||
const updateForm = formData.get("updateForm")
|
||||
const isRequired = formData.get("isRequired")
|
||||
|
||||
if (!event) {
|
||||
return formData
|
||||
}
|
||||
|
||||
const field = formData.has("keyValue") ? "keyValue" : "value"
|
||||
const fieldValue = formData.get(field)
|
||||
const value = event.target.value
|
||||
|
||||
if (List.isList(fieldValue)) {
|
||||
updatedField = formData.set(field, fieldValue.push(value))
|
||||
} else {
|
||||
updatedField = formData.set(field, value)
|
||||
}
|
||||
|
||||
const isEmpty = checkForEmptyValue(value)
|
||||
|
||||
if (isRequired && isEmpty) {
|
||||
return updateForm(updatedField.set("hasErrors", true))
|
||||
}
|
||||
if (!isRequired && isEmpty) {
|
||||
return updateForm(updatedField.set("hasErrors", false))
|
||||
}
|
||||
|
||||
const validationMethod = formData.get("isValid")
|
||||
|
||||
return updateForm(updatedField.set("hasErrors", validationMethod ? !validationMethod(value) : false))
|
||||
}
|
||||
|
||||
// Sets a formData isRequired attribute to !isRequired. Sets
|
||||
// a flag property "optional" to track the change that occurred if
|
||||
// the item was not already required. This allows the add/remove functionality for optional
|
||||
// child forms.
|
||||
export const flipRequired = (formData) => {
|
||||
if (OrderedMap.isOrderedMap(formData.get("value")) || Map.isMap(formData.get("value"))) {
|
||||
const isRequired = formData.get("isRequired")
|
||||
const updateForm = formData.get("updateForm")
|
||||
let updated = formData.set("isRequired", !isRequired)
|
||||
|
||||
if (!isRequired) {
|
||||
updated = updated.set("optional", true)
|
||||
}
|
||||
|
||||
updateForm(updated)
|
||||
}
|
||||
}
|
||||
|
||||
// For use with forms that are List Controls.
|
||||
// Updates the form data object with an additional list item.
|
||||
export const addFormItem = (formData) => {
|
||||
const list = formData.get("value")
|
||||
const updateForm = formData.get("updateForm")
|
||||
if (List.isList(list)) {
|
||||
const defaultItem = formData.get("defaultItem")
|
||||
const updated = formData.set("value", list.push(defaultItem(list.size)))
|
||||
updateForm(updated)
|
||||
}
|
||||
}
|
||||
|
||||
// For use with forms that are List Controls.
|
||||
// Updates the form data object with the final list item removed.
|
||||
export const removeFormItem = (formData) => {
|
||||
const list = formData.get("value")
|
||||
const updateForm = formData.get("updateForm")
|
||||
|
||||
if (List.isList(list)) {
|
||||
updateForm(formData.set("value", list.pop()))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
import { OrderedMap, List, Map } from "immutable"
|
||||
|
||||
export const checkForEmptyValue = (value) => {
|
||||
return !value || value === null || value === "" || !/\S/.test(value) || value === [] ||
|
||||
(List.isList(value) && value.count() === 0) ||
|
||||
(Map.isMap(value) && !value.size) ||
|
||||
(OrderedMap.isOrderedMap(value) && !value.size )
|
||||
}
|
||||
|
||||
// Returns [ updatedFormData, hasErrors ]. The updatedFormData
|
||||
// will include updated 'hasErrors' attributes.
|
||||
// Should be called before form submission to ensure that the user
|
||||
// has entered valid values.
|
||||
export const checkForErrors = (formData) => {
|
||||
let errors = false
|
||||
|
||||
// Invalid object was provided.
|
||||
if (!OrderedMap.isOrderedMap(formData) && !Map.isMap(formData)) {
|
||||
return [formData, true]
|
||||
}
|
||||
|
||||
// The form is a single formData item representing a form with a single field.
|
||||
if (formData.has("value") || formData.has("keyValue")) {
|
||||
const validationMethod = formData.get("isValid")
|
||||
const value = formData.has("keyValue") ? "keyValue" : "value"
|
||||
const validationResult = (validationMethod ? !validationMethod(value) : false)
|
||||
|
||||
errors = errors || validationResult
|
||||
return [formData.set("hasErrors", (validationMethod ? !validationResult : false)), errors]
|
||||
}
|
||||
|
||||
// The form is an OrderedMap of formData items representing a form with multiple fields.
|
||||
// Iterate through them to ensure they are all valid.
|
||||
const updatedFormData = formData.map((formItem) => {
|
||||
const value = formItem.get("value")
|
||||
const isRequired = formItem.get("isRequired")
|
||||
|
||||
// The form field represents a mapping (keyValue => value).
|
||||
if (formItem.has("keyValue")) {
|
||||
// Perform validation on the child/value.
|
||||
const childValidation = checkForErrors(value)
|
||||
|
||||
// Perform validation on the key/parent.
|
||||
const keyValidation = checkForErrors(formItem
|
||||
.set("value", formItem.get("keyValue"))
|
||||
.delete("keyValue"))
|
||||
|
||||
errors = errors || childValidation[1] || keyValidation[1]
|
||||
return formItem
|
||||
.set("hasErrors", errors)
|
||||
.set("value", childValidation[0])
|
||||
.set("keyValue", keyValidation[0])
|
||||
}
|
||||
|
||||
const isEmpty = checkForEmptyValue(value)
|
||||
|
||||
// The form field is required but nothing has been entered.
|
||||
if (isRequired && isEmpty) {
|
||||
errors = true
|
||||
return formItem.set("hasErrors", true)
|
||||
}
|
||||
|
||||
// The form field is not required and nothing has been entered.
|
||||
if (!isRequired && isEmpty) {
|
||||
return formItem.set("hasErrors", false)
|
||||
}
|
||||
|
||||
// Something has been entered in the form field that we need to validate.
|
||||
if (!isEmpty) {
|
||||
const validationMethod = formItem.get("isValid")
|
||||
|
||||
// If the value is an ordered map, recurse to determine
|
||||
// whether any child form data is valid.
|
||||
if ((OrderedMap.isOrderedMap(value) || Map.isMap(value)) && isRequired) {
|
||||
const itemHasErrors = checkForErrors(value)
|
||||
errors = errors || itemHasErrors[1]
|
||||
const newvalue = formItem.set("value", itemHasErrors[0])
|
||||
return newvalue.set("hasErrors", itemHasErrors[1])
|
||||
} else if ((OrderedMap.isOrderedMap(value) || Map.isMap(value)) && !isRequired) {
|
||||
return formItem.set("hasErrors", false)
|
||||
}
|
||||
|
||||
// If the value is a list, recurse on each item to determine
|
||||
// whether any child form data is valid.
|
||||
if (List.isList(value)) {
|
||||
value.map((item) => {
|
||||
const itemHasErrors = checkForErrors(item)
|
||||
errors = errors || itemHasErrors[1]
|
||||
const newitem = item.set("value", itemHasErrors[0])
|
||||
return newitem.set("hasErrors", itemHasErrors[1])
|
||||
})
|
||||
}
|
||||
|
||||
// The form field is a single form field.
|
||||
const validationResult = (validationMethod ? !validationMethod(value) : false)
|
||||
errors = errors || validationResult
|
||||
return formItem.set("hasErrors", validationResult)
|
||||
}
|
||||
|
||||
return formItem
|
||||
})
|
||||
|
||||
return [updatedFormData, errors]
|
||||
}
|
||||
|
||||
export const validateUrl = (url) => {
|
||||
return /^(?:(?:(?:https?|ftp):)?\/\/)(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)*(?:\.(?:[a-z\u00a1-\uffff]{2,})))(?::\d{2,5})?(?:[/?#]\S*)?$/i.test(url)
|
||||
}
|
||||
|
||||
export const validateAlphaNum = (string) => {
|
||||
return /^[a-z0-9]+$/i.test(string)
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import { OrderedMap, List } from "immutable"
|
||||
|
||||
import TopbarInsert from "./topbar-insert"
|
||||
import TopbarModal from "./modal/Modal"
|
||||
|
||||
import InsertDropdown from "./dropdown/Dropdown"
|
||||
import InsertDropdownItem from "./dropdown/DropdownItem"
|
||||
import AddForm from "./forms/components/AddForm"
|
||||
import FormChild from "./forms/components/FormChild"
|
||||
import FormDropdown from "./forms/components/FormDropdown"
|
||||
import FormInput from "./forms/components/FormInput"
|
||||
import FormInputWrapper from "./forms/components/FormInputWrapper"
|
||||
import FormMap from "./forms/components/FormMap"
|
||||
import InsertForm from "./forms/components/InsertForm"
|
||||
import InsertFormInput from "./forms/components/InsertFormInput"
|
||||
import InsertFormList from "./forms/components/InsertFormList"
|
||||
|
||||
export default function () {
|
||||
const ADD_TO_SPEC = "add_to_spec"
|
||||
|
||||
return {
|
||||
components: {
|
||||
TopbarInsert,
|
||||
TopbarModal,
|
||||
InsertDropdown,
|
||||
InsertDropdownItem,
|
||||
AddForm,
|
||||
FormChild,
|
||||
FormDropdown,
|
||||
FormInput,
|
||||
FormInputWrapper,
|
||||
FormMap,
|
||||
InsertForm,
|
||||
InsertFormInput,
|
||||
InsertFormList
|
||||
},
|
||||
statePlugins: {
|
||||
spec: {
|
||||
actions: {
|
||||
addToSpec: (path, item, key) => ({
|
||||
type: ADD_TO_SPEC,
|
||||
payload: { path, item, key }
|
||||
})
|
||||
},
|
||||
reducers: {
|
||||
[ADD_TO_SPEC]: (state, { payload }) => {
|
||||
const { path, item, key } = payload
|
||||
let currentItem = state.getIn(["json", ...path])
|
||||
|
||||
if (!currentItem) {
|
||||
currentItem = key ? new OrderedMap({ [key]: item }) : new List()
|
||||
}
|
||||
|
||||
currentItem = key ? currentItem.set(key, item) : currentItem.concat(item)
|
||||
|
||||
return state.setIn(["json", ...path], currentItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import React from "react"
|
||||
import PropTypes from "prop-types"
|
||||
import classNames from "classnames"
|
||||
|
||||
const Modal = (props) => (
|
||||
<div className="swagger-ui modal topbar-modal" role="dialog">
|
||||
<div className={classNames("modal-dialog", props.styleName)} role="document">
|
||||
<div className="modal-content">
|
||||
<div className={classNames("modal-header", {"modal-header-border" : props.title})} >
|
||||
<span className="modal-title">{props.title}</span>
|
||||
{!props.hideCloseButton && <a type="button" className="close" aria-label="Close" onClick={props.onCloseClick}>
|
||||
<span aria-hidden="true">×</span>
|
||||
</a> }
|
||||
</div>
|
||||
{props.children}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
Modal.propTypes = {
|
||||
title: PropTypes.string,
|
||||
styleName: PropTypes.string,
|
||||
onCloseClick: PropTypes.func,
|
||||
hideCloseButton: PropTypes.bool,
|
||||
children: PropTypes.oneOfType([
|
||||
PropTypes.array,
|
||||
PropTypes.element
|
||||
])
|
||||
}
|
||||
|
||||
export default Modal
|
||||
@@ -0,0 +1,314 @@
|
||||
import React, { Component } from "react"
|
||||
import PropTypes from "prop-types"
|
||||
|
||||
import { pathForm, pathObject } from "./forms/form-objects/path-object"
|
||||
import { operationForm, operationObject } from "./forms/form-objects/operation-object"
|
||||
import { infoForm, infoObject } from "./forms/form-objects/info-object"
|
||||
import { tagsForm, tagsObject } from "./forms/form-objects/tags-object"
|
||||
import { serversForm, serversObject } from "./forms/form-objects/servers-object"
|
||||
import { externalDocumentationForm, externalDocumentationObject } from "./forms/form-objects/external-documentation-object"
|
||||
import { addOperationTagsForm, addOperationTagsObject } from "./forms/form-objects/add-operation-tags"
|
||||
import { exampleForm, exampleObject } from "./forms/form-objects/example-value-object"
|
||||
import { selectOperationObject } from "./forms/form-objects/select-operation"
|
||||
|
||||
export default class TopbarInsert extends Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
showAddPathModal: false,
|
||||
showAddOperationModal: false,
|
||||
showAddInfoModal: false,
|
||||
showAddExternalDocsModal: false,
|
||||
showAddTagsModal: false,
|
||||
showAddServersModal: false,
|
||||
showAddOperationTagsModal: false
|
||||
}
|
||||
|
||||
this.openModalClick = this.openModalClick.bind(this)
|
||||
this.closeModalClick = this.closeModalClick.bind(this)
|
||||
this.updatePath = this.updatePath.bind(this)
|
||||
this.updateExternalDocs = this.updateExternalDocs.bind(this)
|
||||
this.updateInfo = this.updateInfo.bind(this)
|
||||
this.getPaths = this.getPaths.bind(this)
|
||||
this.updateOperation = this.updateOperation.bind(this)
|
||||
this.updateServers = this.updateServers.bind(this)
|
||||
this.updateTags = this.updateTags.bind(this)
|
||||
this.addOperationTags = this.addOperationTags.bind(this)
|
||||
this.getResponses = this.getResponses.bind(this)
|
||||
this.getMediaTypes = this.getMediaTypes.bind(this)
|
||||
this.addExampleResponse = this.addExampleResponse.bind(this)
|
||||
}
|
||||
|
||||
openModalClick = showModalProperty => () => {
|
||||
this.setState({
|
||||
[showModalProperty]: true
|
||||
})
|
||||
}
|
||||
|
||||
closeModalClick = showModalProperty => () => {
|
||||
this.setState({
|
||||
[showModalProperty]: false
|
||||
})
|
||||
}
|
||||
|
||||
updatePath = (formData) => {
|
||||
const pathFormObject = pathObject(formData)
|
||||
this.props.specActions.addToSpec(["paths"], pathFormObject.value, pathFormObject.key)
|
||||
}
|
||||
|
||||
updateExternalDocs = (formData) => {
|
||||
this.props.specActions.addToSpec([], externalDocumentationObject(formData), "externalDocs")
|
||||
}
|
||||
|
||||
updateInfo = (formData) => {
|
||||
this.props.specActions.addToSpec([], infoObject(formData), "info")
|
||||
}
|
||||
|
||||
getPaths = () => this.props.specSelectors.paths() ? Object.keys(this.props.specSelectors.paths().toJS()) : null
|
||||
|
||||
addOperationTags(formData) {
|
||||
const operationTagsObject = addOperationTagsObject(formData)
|
||||
operationTagsObject.selectedOperation.push("tags")
|
||||
this.props.specActions.addToSpec(operationTagsObject.selectedOperation, operationTagsObject.tags, null)
|
||||
}
|
||||
|
||||
getOperations = (path) =>
|
||||
path ? this.props.specSelectors.operations().toJS()
|
||||
.filter(item => item.path === path)
|
||||
.map(item => item.method) :
|
||||
null
|
||||
|
||||
updateOperation = (formData) => {
|
||||
const path = formData.getIn(["path", "value"])
|
||||
|
||||
if (!this.getPaths().includes(path)) {
|
||||
// Update json in the Swagger UI state with the new path.
|
||||
this.props.specActions.addToSpec(["paths"], null, path)
|
||||
}
|
||||
|
||||
this.props.specActions.addToSpec(["paths", path], operationObject(formData), formData.getIn(["operation", "value"]))
|
||||
}
|
||||
|
||||
updateServers = (formData) => {
|
||||
this.props.specActions.addToSpec(["servers"], serversObject(formData), null)
|
||||
}
|
||||
|
||||
updateTags = (formData) => {
|
||||
this.props.specActions.addToSpec(["tags"], tagsObject(formData), null)
|
||||
}
|
||||
|
||||
getResponses = (method, formData) => {
|
||||
// Operation = 'depends on' value
|
||||
// formData = all the data so far, process to get the response as well
|
||||
const operationPath = [...selectOperationObject(formData), "responses"]
|
||||
const responses = this.props.specSelectors.specJson().getIn(operationPath)
|
||||
|
||||
if (!responses) {
|
||||
return []
|
||||
}
|
||||
|
||||
return Object.keys(responses.toJS())
|
||||
}
|
||||
|
||||
getMediaTypes = (response, formData) => {
|
||||
const defaultOptions = [
|
||||
"application/json",
|
||||
"text/plain; charset=utf-8",
|
||||
"application/xml" ]
|
||||
|
||||
if (!formData) {
|
||||
return defaultOptions
|
||||
}
|
||||
|
||||
// Operation = 'depends on' value
|
||||
// formData = all the data so far, process to get the response as well
|
||||
const operationPath = [...selectOperationObject(formData), "responses"]
|
||||
const responses = this.props.specSelectors.specJson().getIn(operationPath)
|
||||
|
||||
if (responses) {
|
||||
const response = responses.get(formData.getIn(["response", "value"]))
|
||||
|
||||
if (response && response.has("content")) {
|
||||
const existing = Object.keys(response.get("content").toJS())
|
||||
const combined = defaultOptions.concat(existing)
|
||||
|
||||
// Remove duplicates.
|
||||
return combined.filter((item, pos) => combined.indexOf(item) == pos)
|
||||
}
|
||||
}
|
||||
|
||||
return defaultOptions
|
||||
}
|
||||
|
||||
addExampleResponse = (formData) => {
|
||||
const formObject = exampleObject(formData)
|
||||
this.props.specActions.addToSpec(formObject.responsePath, { value: formObject.exampleValue }, formObject.exampleName)
|
||||
}
|
||||
|
||||
render() {
|
||||
let { specSelectors, getComponent } = this.props
|
||||
|
||||
const Modal = getComponent("TopbarModal")
|
||||
const Dropdown = getComponent("InsertDropdown")
|
||||
const DropdownItem = getComponent("InsertDropdownItem")
|
||||
const AddForm = getComponent("AddForm")
|
||||
|
||||
if (!specSelectors.isOAS3()) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{this.state.showAddPathModal &&
|
||||
<Modal
|
||||
title="Add Path"
|
||||
onCloseClick={this.closeModalClick("showAddPathModal")}
|
||||
>
|
||||
<AddForm
|
||||
{...this.props}
|
||||
submit={this.closeModalClick("showAddPathModal")}
|
||||
submitButtonText="Add Path"
|
||||
getFormData={pathForm}
|
||||
updateSpecJson={this.updatePath}
|
||||
/>
|
||||
</Modal>
|
||||
}
|
||||
{ this.state.showAddOperationModal &&
|
||||
<Modal
|
||||
title="Add Operation to Document"
|
||||
onCloseClick={this.closeModalClick("showAddOperationModal")}
|
||||
>
|
||||
<AddForm
|
||||
{...this.props}
|
||||
submit={this.closeModalClick("showAddOperationModal")}
|
||||
submitButtonText="Add Operation"
|
||||
getFormData={operationForm}
|
||||
updateSpecJson={this.updateOperation}
|
||||
existingData={this.getPaths()}
|
||||
/>
|
||||
</Modal>
|
||||
}
|
||||
{ this.state.showAddInfoModal &&
|
||||
<Modal
|
||||
title="Add Info to Document"
|
||||
onCloseClick={this.closeModalClick("showAddInfoModal")}
|
||||
>
|
||||
<AddForm
|
||||
{...this.props}
|
||||
submit={this.closeModalClick("showAddInfoModal")}
|
||||
submitButtonText="Add Info"
|
||||
getFormData={infoForm}
|
||||
updateSpecJson={this.updateInfo}
|
||||
existingData={this.props.specSelectors.info()}
|
||||
/>
|
||||
</Modal>
|
||||
}
|
||||
{ this.state.showAddExternalDocsModal &&
|
||||
<Modal
|
||||
title="Add External Documentation"
|
||||
onCloseClick={this.closeModalClick("showAddExternalDocsModal")}
|
||||
>
|
||||
<AddForm
|
||||
{...this.props}
|
||||
submit={this.closeModalClick("showAddExternalDocsModal")}
|
||||
submitButtonText="Add External Documentation"
|
||||
getFormData={externalDocumentationForm}
|
||||
updateSpecJson={this.updateExternalDocs}
|
||||
/>
|
||||
</Modal>
|
||||
}
|
||||
{ this.state.showAddTagsModal &&
|
||||
<Modal
|
||||
title="Add Tag Declarations"
|
||||
onCloseClick={this.closeModalClick("showAddTagsModal")}
|
||||
>
|
||||
<AddForm
|
||||
{...this.props}
|
||||
submit={this.closeModalClick("showAddTagsModal")}
|
||||
submitButtonText="Add Tag Declarations"
|
||||
getFormData={tagsForm}
|
||||
updateSpecJson={this.updateTags}
|
||||
/>
|
||||
</Modal>
|
||||
}
|
||||
{ this.state.showAddServersModal &&
|
||||
<Modal
|
||||
title="Add Servers"
|
||||
onCloseClick={this.closeModalClick("showAddServersModal")}
|
||||
>
|
||||
<AddForm
|
||||
{...this.props}
|
||||
submit={this.closeModalClick("showAddServersModal")}
|
||||
submitButtonText="Add Servers"
|
||||
getFormData={serversForm}
|
||||
updateSpecJson={this.updateServers}
|
||||
/>
|
||||
</Modal>
|
||||
}
|
||||
{ this.state.showAddOperationTagsModal &&
|
||||
<Modal
|
||||
title="Add Tags To Operation"
|
||||
onCloseClick={this.closeModalClick("showAddOperationTagsModal")}
|
||||
isShown
|
||||
isLarge
|
||||
>
|
||||
<AddForm
|
||||
{...this.props}
|
||||
submit={this.closeModalClick("showAddOperationTagsModal")}
|
||||
getFormData={addOperationTagsForm}
|
||||
existingData={{ getPaths: this.getPaths, getOperations: this.getOperations }}
|
||||
submitButtonText="Add Tags To Operation"
|
||||
updateSpecJson={this.addOperationTags}
|
||||
/>
|
||||
</Modal>
|
||||
}
|
||||
{ this.state.showAddExampleModal &&
|
||||
<Modal
|
||||
title="Add Example Response"
|
||||
description="An example response sent from the API."
|
||||
onCloseClick={this.closeModalClick("showAddExampleModal")}
|
||||
isShownisLarge
|
||||
>
|
||||
<AddForm
|
||||
{...this.props}
|
||||
submit={this.closeModalClick("showAddExampleModal")}
|
||||
getFormData={exampleForm}
|
||||
existingData={{ getPaths: this.getPaths, getOperations: this.getOperations, getResponses: this.getResponses, getMediaTypes: this.getMediaTypes }}
|
||||
submitButtonText="Add Example Response"
|
||||
updateSpecJson={this.addExampleResponse}
|
||||
/>
|
||||
</Modal>
|
||||
}
|
||||
|
||||
<Dropdown displayName="Insert" >
|
||||
<DropdownItem onClick={this.openModalClick("showAddPathModal")} name="Add Path Item"/>
|
||||
<DropdownItem onClick={this.openModalClick("showAddOperationModal")} name="Add Operation" />
|
||||
<DropdownItem onClick={this.openModalClick("showAddInfoModal")} name="Add Info" />
|
||||
<DropdownItem onClick={this.openModalClick("showAddExternalDocsModal")} name="Add External Documentation" />
|
||||
<DropdownItem onClick={this.openModalClick("showAddTagsModal")} name="Add Tag Declarations" />
|
||||
<DropdownItem onClick={this.openModalClick("showAddOperationTagsModal")} name="Add Tags To Operation" />
|
||||
<DropdownItem onClick={this.openModalClick("showAddServersModal")} name="Add Servers" />
|
||||
<DropdownItem onClick={this.openModalClick("showAddExampleModal")} name="Add Example Response" />
|
||||
</Dropdown>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
TopbarInsert.propTypes = {
|
||||
getComponent: PropTypes.func.isRequired,
|
||||
specSelectors: PropTypes.shape({
|
||||
specJson: PropTypes.func.isRequired,
|
||||
info: PropTypes.func.isRequired,
|
||||
paths: PropTypes.func.isRequired,
|
||||
isOAS3: PropTypes.func.isRequired,
|
||||
operations: PropTypes.func.isRequired
|
||||
}),
|
||||
errSelectors: PropTypes.shape({
|
||||
allErrors: PropTypes.func.isRequired
|
||||
}),
|
||||
specActions: PropTypes.shape({
|
||||
addToSpec: PropTypes.func.isRequired
|
||||
})
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
import React, { Component } from "react"
|
||||
import PropTypes from "prop-types"
|
||||
|
||||
export default class ConvertDefinitionMenuItem extends Component {
|
||||
render() {
|
||||
const { isSwagger2, } = this.props
|
||||
|
||||
if(!isSwagger2) {
|
||||
return null
|
||||
}
|
||||
|
||||
return <li>
|
||||
<button type="button" onClick={this.props.onClick}>Convert to OpenAPI 3</button>
|
||||
</li>
|
||||
}
|
||||
}
|
||||
|
||||
ConvertDefinitionMenuItem.propTypes = {
|
||||
isSwagger2: PropTypes.bool.isRequired,
|
||||
onClick: PropTypes.func.isRequired,
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
import React, { Component } from "react"
|
||||
import PropTypes from "prop-types"
|
||||
|
||||
export default class ConvertModal extends Component {
|
||||
constructor() {
|
||||
super()
|
||||
this.state = {
|
||||
error: null,
|
||||
status: "new"
|
||||
}
|
||||
}
|
||||
convertDefinition = async () => {
|
||||
this.setState({ status: "converting" })
|
||||
|
||||
try {
|
||||
const conversionResult = await this.performConversion()
|
||||
this.setState({
|
||||
status: "success",
|
||||
})
|
||||
this.props.updateEditorContent(conversionResult)
|
||||
} catch (e) {
|
||||
this.setState({
|
||||
error: e,
|
||||
status: "errored",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
performConversion = async () => {
|
||||
const res = await fetch("//converter.swagger.io/api/convert", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/yaml",
|
||||
"Accept": "application/yaml",
|
||||
},
|
||||
body: this.props.editorContent
|
||||
})
|
||||
|
||||
const body = await res.text()
|
||||
|
||||
if(!res.ok) {
|
||||
throw new Error(body)
|
||||
}
|
||||
|
||||
return body
|
||||
}
|
||||
|
||||
render() {
|
||||
const { onClose, getComponent, converterUrl } = this.props
|
||||
|
||||
if(this.state.status === "new") {
|
||||
return <ConvertModalStepNew
|
||||
onClose={onClose}
|
||||
onContinue={() => this.convertDefinition()}
|
||||
getComponent={getComponent}
|
||||
converterUrl={converterUrl}
|
||||
/>
|
||||
}
|
||||
|
||||
if (this.state.status === "converting") {
|
||||
return <ConvertModalStepConverting
|
||||
getComponent={getComponent}
|
||||
/>
|
||||
}
|
||||
|
||||
if (this.state.status === "success") {
|
||||
return <ConvertModalStepSuccess
|
||||
onClose={onClose}
|
||||
getComponent={getComponent}
|
||||
/>
|
||||
}
|
||||
|
||||
if (this.state.status === "errored") {
|
||||
return <ConvertModalStepErrored
|
||||
onClose={onClose}
|
||||
error={this.state.error}
|
||||
getComponent={getComponent}
|
||||
/>
|
||||
}
|
||||
|
||||
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
ConvertModal.propTypes = {
|
||||
editorContent: PropTypes.string.isRequired,
|
||||
converterUrl: PropTypes.string.isRequired,
|
||||
getComponent: PropTypes.func.isRequired,
|
||||
updateEditorContent: PropTypes.func.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
const ConvertModalStepNew = ({ getComponent, onClose, onContinue, converterUrl }) => {
|
||||
const Modal = getComponent("TopbarModal")
|
||||
|
||||
return <Modal className="modal" styleName="modal-dialog-sm" onCloseClick={onClose}>
|
||||
<div className="container modal-message">
|
||||
<h2>Convert to OpenAPI 3</h2>
|
||||
<p>
|
||||
This feature uses the Swagger Converter API to convert your Swagger 2.0
|
||||
definition to OpenAPI 3.
|
||||
</p>
|
||||
<p>
|
||||
Swagger Editor's contents will be sent to <b><code>{converterUrl}</code></b> and overwritten
|
||||
by the conversion result.
|
||||
</p>
|
||||
</div>
|
||||
<div className="right">
|
||||
<button className="btn cancel" onClick={onClose}>Cancel</button>
|
||||
<button className="btn" onClick={onContinue}>Convert</button>
|
||||
</div>
|
||||
</Modal>
|
||||
}
|
||||
|
||||
ConvertModalStepNew.propTypes = {
|
||||
getComponent: PropTypes.func.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
onContinue: PropTypes.func.isRequired,
|
||||
converterUrl: PropTypes.string.isRequired,
|
||||
}
|
||||
|
||||
const ConvertModalStepConverting = ({ getComponent }) => {
|
||||
const Modal = getComponent("TopbarModal")
|
||||
|
||||
return <Modal className="modal" styleName="modal-dialog-sm" hideCloseButton={true}>
|
||||
<div className="container modal-message">
|
||||
<h2>Converting...</h2>
|
||||
<p>
|
||||
Please wait.
|
||||
</p>
|
||||
</div>
|
||||
</Modal>
|
||||
}
|
||||
|
||||
ConvertModalStepConverting.propTypes = {
|
||||
getComponent: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
const ConvertModalStepSuccess = ({ getComponent, onClose }) => {
|
||||
const Modal = getComponent("TopbarModal")
|
||||
|
||||
return <Modal className="modal" styleName="modal-dialog-sm" onCloseClick={onClose}>
|
||||
<div className="container modal-message">
|
||||
<h2>Conversion complete</h2>
|
||||
<p>
|
||||
Your definition was successfully converted to OpenAPI 3!
|
||||
</p>
|
||||
</div>
|
||||
<div className="right">
|
||||
<button className="btn" onClick={onClose}>Close</button>
|
||||
</div>
|
||||
</Modal>
|
||||
}
|
||||
|
||||
|
||||
ConvertModalStepSuccess.propTypes = {
|
||||
getComponent: PropTypes.func.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
const ConvertModalStepErrored = ({ getComponent, onClose, error }) => {
|
||||
const Modal = getComponent("TopbarModal")
|
||||
|
||||
return <Modal className="modal" styleName="modal-dialog-sm" onCloseClick={onClose}>
|
||||
<div className="container modal-message">
|
||||
<h2>Conversion failed</h2>
|
||||
<p>
|
||||
The converter service was unable to convert your definition.
|
||||
</p>
|
||||
<p>
|
||||
Here's what the service told us:
|
||||
</p>
|
||||
<code>
|
||||
{error.toString()}
|
||||
</code>
|
||||
</div>
|
||||
<div className="right">
|
||||
<button className="btn" onClick={onClose}>Close</button>
|
||||
</div>
|
||||
</Modal>
|
||||
}
|
||||
|
||||
ConvertModalStepErrored.propTypes = {
|
||||
getComponent: PropTypes.func.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
error: PropTypes.any.isRequired,
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
/* eslint-disable react/prop-types */
|
||||
|
||||
import React from "react"
|
||||
|
||||
import ConvertDefinitionMenuItem from "./components/convert-definition-menu-item"
|
||||
import ConvertModal from "./components/convert-modal"
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ConvertDefinitionMenuItem,
|
||||
ConvertModal,
|
||||
},
|
||||
wrapComponents: {
|
||||
Topbar: (Ori) => props => {
|
||||
const ConvertModal = props.getComponent("ConvertModal")
|
||||
return <div>
|
||||
<Ori {...props} />
|
||||
{props.topbarSelectors.showModal("convert") && <ConvertModal
|
||||
getComponent={props.getComponent}
|
||||
editorContent={props.specSelectors.specStr()}
|
||||
converterUrl={props.getConfigs().swagger2ConverterUrl}
|
||||
updateEditorContent={content => props.specActions.updateSpec(content, "insert")}
|
||||
onClose={() => props.topbarActions.hideModal("convert")}
|
||||
/>}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import React, { Component } from "react"
|
||||
import PropTypes from "prop-types"
|
||||
|
||||
import fileDialog from "file-dialog"
|
||||
import YAML from "js-yaml"
|
||||
import isJsonObject from "is-json"
|
||||
|
||||
export default class ImportFileMenuItem extends Component {
|
||||
onClick = async () => {
|
||||
const { onDocumentLoad } = this.props
|
||||
const fileList = await fileDialog()
|
||||
|
||||
const fileReader = new FileReader()
|
||||
|
||||
fileReader.onload = fileLoadedEvent => {
|
||||
let content = fileLoadedEvent.target.result
|
||||
|
||||
try {
|
||||
const preparedContent = isJsonObject(content) ? YAML.safeDump(YAML.safeLoad(content)) : content
|
||||
|
||||
if (typeof onDocumentLoad === "function") {
|
||||
onDocumentLoad(preparedContent)
|
||||
}
|
||||
} catch(e) {
|
||||
alert(`Oof! There was an error loading your document:\n\n${e.message || e}`)
|
||||
}
|
||||
}
|
||||
|
||||
fileReader.readAsText(fileList.item(0), "UTF-8")
|
||||
}
|
||||
render() {
|
||||
return <li>
|
||||
<button type="button" onClick={this.onClick}>Import file</button>
|
||||
</li>
|
||||
}
|
||||
}
|
||||
|
||||
ImportFileMenuItem.propTypes = {
|
||||
onDocumentLoad: PropTypes.func.isRequired,
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import ImportFileMenuItem from "./components/ImportFileMenuItem"
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ImportFileMenuItem
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
// Adapted from https://github.com/mlaursen/react-dd-menu/blob/master/src/js/DropdownMenu.js
|
||||
|
||||
import React, { PureComponent } from "react"
|
||||
import PropTypes from "prop-types"
|
||||
import ReactDOM from "react-dom"
|
||||
import CSSTransitionGroup from "react-transition-group/CSSTransitionGroup"
|
||||
import classnames from "classnames"
|
||||
|
||||
const TAB = 9
|
||||
const SPACEBAR = 32
|
||||
const ALIGNMENTS = ["center", "right", "left"]
|
||||
const MENU_SIZES = ["sm", "md", "lg", "xl"]
|
||||
|
||||
|
||||
export default class DropdownMenu extends PureComponent {
|
||||
static propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
close: PropTypes.func.isRequired,
|
||||
toggle: PropTypes.node.isRequired,
|
||||
children: PropTypes.node,
|
||||
inverse: PropTypes.bool,
|
||||
align: PropTypes.oneOf(ALIGNMENTS),
|
||||
animAlign: PropTypes.oneOf(ALIGNMENTS),
|
||||
textAlign: PropTypes.oneOf(ALIGNMENTS),
|
||||
menuAlign: PropTypes.oneOf(ALIGNMENTS),
|
||||
className: PropTypes.string,
|
||||
size: PropTypes.oneOf(MENU_SIZES),
|
||||
upwards: PropTypes.bool,
|
||||
animate: PropTypes.bool,
|
||||
enterTimeout: PropTypes.number,
|
||||
leaveTimeout: PropTypes.number,
|
||||
closeOnInsideClick: PropTypes.bool,
|
||||
closeOnOutsideClick: PropTypes.bool,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
inverse: false,
|
||||
align: "center",
|
||||
animAlign: null,
|
||||
textAlign: null,
|
||||
menuAlign: null,
|
||||
className: null,
|
||||
size: null,
|
||||
upwards: false,
|
||||
animate: true,
|
||||
enterTimeout: 150,
|
||||
leaveTimeout: 150,
|
||||
closeOnInsideClick: true,
|
||||
closeOnOutsideClick: true,
|
||||
};
|
||||
|
||||
static MENU_SIZES = MENU_SIZES;
|
||||
static ALIGNMENTS = ALIGNMENTS;
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if(this.props.isOpen === prevProps.isOpen) {
|
||||
return
|
||||
}
|
||||
|
||||
const menuItems = ReactDOM.findDOMNode(this).querySelector(".dd-menu > .dd-menu-items")
|
||||
if(this.props.isOpen && !prevProps.isOpen) {
|
||||
this.lastWindowClickEvent = this.handleClickOutside
|
||||
document.addEventListener("click", this.lastWindowClickEvent)
|
||||
if(this.props.closeOnInsideClick) {
|
||||
menuItems.addEventListener("click", this.props.close)
|
||||
}
|
||||
menuItems.addEventListener("onkeydown", this.close)
|
||||
} else if(!this.props.isOpen && prevProps.isOpen) {
|
||||
document.removeEventListener("click", this.lastWindowClickEvent)
|
||||
if(prevProps.closeOnInsideClick) {
|
||||
menuItems.removeEventListener("click", this.props.close)
|
||||
}
|
||||
menuItems.removeEventListener("onkeydown", this.close)
|
||||
|
||||
this.lastWindowClickEvent = null
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if(this.lastWindowClickEvent) {
|
||||
document.removeEventListener("click", this.lastWindowClickEvent)
|
||||
}
|
||||
}
|
||||
|
||||
close = (e) => {
|
||||
const key = e.which || e.keyCode
|
||||
if(key === SPACEBAR) {
|
||||
this.props.close()
|
||||
e.preventDefault()
|
||||
}
|
||||
};
|
||||
|
||||
handleClickOutside = (e) => {
|
||||
if(!this.props.closeOnOutsideClick) {
|
||||
return
|
||||
}
|
||||
|
||||
const node = ReactDOM.findDOMNode(this)
|
||||
let target = e.target
|
||||
|
||||
while(target.parentNode) {
|
||||
if(target === node) {
|
||||
return
|
||||
}
|
||||
|
||||
target = target.parentNode
|
||||
}
|
||||
|
||||
this.props.close(e)
|
||||
};
|
||||
|
||||
handleKeyDown = (e) => {
|
||||
const key = e.which || e.keyCode
|
||||
if(key !== TAB) {
|
||||
return
|
||||
}
|
||||
|
||||
const items = ReactDOM.findDOMNode(this).querySelectorAll("button,a")
|
||||
const id = e.shiftKey ? 1 : items.length - 1
|
||||
|
||||
if(e.target === items[id]) {
|
||||
this.props.close(e)
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
render() {
|
||||
const { menuAlign, align, inverse, size, className } = this.props
|
||||
|
||||
const menuClassName = classnames(
|
||||
"dd-menu",
|
||||
`dd-menu-${menuAlign || align}`,
|
||||
{ "dd-menu-inverse": inverse },
|
||||
className,
|
||||
size ? ("dd-menu-" + size) : null
|
||||
)
|
||||
|
||||
const { textAlign, upwards, animAlign, animate, enterTimeout, leaveTimeout } = this.props
|
||||
|
||||
const listClassName = "dd-items-" + (textAlign || align)
|
||||
const transitionProps = {
|
||||
transitionName: "grow-from-" + (upwards ? "up-" : "") + (animAlign || align),
|
||||
component: "div",
|
||||
className: classnames("dd-menu-items", { "dd-items-upwards": upwards }),
|
||||
onKeyDown: this.handleKeyDown,
|
||||
transitionEnter: animate,
|
||||
transitionLeave: animate,
|
||||
transitionEnterTimeout: enterTimeout,
|
||||
transitionLeaveTimeout: leaveTimeout,
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={menuClassName}>
|
||||
{this.props.toggle}
|
||||
<CSSTransitionGroup {...transitionProps}>
|
||||
{this.props.isOpen &&
|
||||
<ul key="items" className={listClassName}>{this.props.children}</ul>
|
||||
}
|
||||
</CSSTransitionGroup>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import Topbar from "./topbar.jsx"
|
||||
|
||||
export default function () {
|
||||
return {
|
||||
statePlugins: {
|
||||
topbar: {
|
||||
actions: {
|
||||
showModal(name) {
|
||||
return {
|
||||
type: "TOPBAR_SHOW_MODAL",
|
||||
target: name
|
||||
}
|
||||
},
|
||||
hideModal(name) {
|
||||
return {
|
||||
type: "TOPBAR_HIDE_MODAL",
|
||||
target: name
|
||||
}
|
||||
}
|
||||
},
|
||||
reducers: {
|
||||
TOPBAR_SHOW_MODAL: (state, action) => state.setIn(["shownModals", action.target], true),
|
||||
TOPBAR_HIDE_MODAL: (state, action) => state.setIn(["shownModals", action.target], false),
|
||||
},
|
||||
selectors: {
|
||||
showModal: (state, name) => state.getIn(["shownModals", name], false)
|
||||
}
|
||||
}
|
||||
},
|
||||
components: {
|
||||
Topbar
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 536 116">
|
||||
<defs>
|
||||
<style>
|
||||
.cls-1 {
|
||||
clip-path: url(#clip-SWE_TM-logo-on-dark);
|
||||
}
|
||||
|
||||
.cls-2 {
|
||||
fill: #fff;
|
||||
}
|
||||
|
||||
.cls-3 {
|
||||
fill: #85ea2d;
|
||||
}
|
||||
|
||||
.cls-4 {
|
||||
fill: #173647;
|
||||
}
|
||||
</style>
|
||||
<clipPath id="clip-SWE_TM-logo-on-dark">
|
||||
<rect width="536" height="116"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
<g id="SWE_TM-logo-on-dark" class="cls-1">
|
||||
<g id="SWE_In-Product">
|
||||
<path id="Path_3002" data-name="Path 3002" class="cls-2" d="M528.911,70.857h-.7V67.176h-1.26v-.6h3.219v.6h-1.26Z"/>
|
||||
<path id="Path_3003" data-name="Path 3003" class="cls-2" d="M532.979,70.857l-1.242-3.576h-.023q.049.8.05,1.494v2.082h-.636V66.574h.987l1.19,3.408h.017l1.225-3.408h.99v4.283h-.674V68.74q0-.319.016-.832t.028-.621h-.023l-1.286,3.57Z"/>
|
||||
<path id="Path_3004" data-name="Path 3004" class="cls-3" d="M50.027,97.669A47.642,47.642,0,1,1,97.67,50.027,47.642,47.642,0,0,1,50.027,97.669Z"/>
|
||||
<path id="Path_3005" data-name="Path 3005" class="cls-3" d="M50.027,4.769A45.258,45.258,0,1,1,4.769,50.027,45.258,45.258,0,0,1,50.027,4.769m0-4.769a50.027,50.027,0,1,0,50.027,50.027A50.027,50.027,0,0,0,50.027,0Z"/>
|
||||
<path id="Path_3006" data-name="Path 3006" class="cls-4" d="M31.5,33.854c-.154,1.712.058,3.482-.057,5.213a42.665,42.665,0,0,1-.693,5.156,9.532,9.532,0,0,1-4.1,5.829c4.078,2.654,4.539,6.771,4.809,10.946.135,2.25.077,4.52.308,6.752.173,1.731.846,2.174,2.636,2.231.731.02,1.48,0,2.327,0V75.33c-5.29.9-9.657-.6-10.734-5.079a30.76,30.76,0,0,1-.654-5c-.116-1.789.076-3.578-.058-5.367-.385-4.906-1.02-6.56-5.713-6.791v-6.1a9.189,9.189,0,0,1,1.019-.173c2.577-.135,3.674-.924,4.232-3.463a29.573,29.573,0,0,0,.481-4.329,81.809,81.809,0,0,1,.6-8.406c.674-3.982,3.136-5.906,7.234-6.137,1.154-.057,2.327,0,3.655,0v5.464c-.558.038-1.039.115-1.539.115C31.925,29.949,31.751,31.084,31.5,33.854Zm6.407,12.658H37.83a3.515,3.515,0,1,0-.347,7.021h.231a3.461,3.461,0,0,0,3.655-3.251V50.09a3.523,3.523,0,0,0-3.461-3.578Zm12.061,0a3.373,3.373,0,0,0-3.482,3.251,1.79,1.79,0,0,0,.02.327,3.3,3.3,0,0,0,3.578,3.443,3.264,3.264,0,0,0,3.444-3.558,3.309,3.309,0,0,0-3.559-3.463Zm12.351,0a3.592,3.592,0,0,0-3.655,3.482A3.529,3.529,0,0,0,62.2,53.533h.039c1.769.309,3.559-1.4,3.674-3.462a3.571,3.571,0,0,0-3.593-3.559Zm16.948.288c-2.232-.1-3.348-.846-3.9-2.962a21.523,21.523,0,0,1-.635-4.136c-.154-2.578-.135-5.175-.308-7.753-.4-6.117-4.828-8.252-11.254-7.195v5.31c1.019,0,1.808,0,2.6.019,1.366.019,2.4.539,2.539,2.059.136,1.385.136,2.789.27,4.193.269,2.79.423,5.618.9,8.369A8.715,8.715,0,0,0,73.4,50.052c-3.4,2.289-4.4,5.559-4.578,9.234-.1,2.52-.154,5.059-.289,7.6-.115,2.308-.923,3.058-3.251,3.116-.653.019-1.288.077-2.019.115v5.445c1.366,0,2.616.077,3.866,0,3.886-.231,6.233-2.117,7-5.887A48.9,48.9,0,0,0,74.7,63.4c.135-1.923.116-3.866.309-5.771.288-2.982,1.654-4.213,4.635-4.4a4.037,4.037,0,0,0,.828-.192v-6.1c-.5-.058-.844-.115-1.209-.135Z"/>
|
||||
<path id="Path_3007" data-name="Path 3007" class="cls-2" d="M151.972,58.122a11.231,11.231,0,0,1-4.383,9.425q-4.385,3.381-11.9,3.381-8.14,0-12.523-2.1V63.7a32.95,32.95,0,0,0,6.137,1.879,32.279,32.279,0,0,0,6.574.689q5.323,0,8.015-2.02a6.629,6.629,0,0,0,2.693-5.62,7.216,7.216,0,0,0-.955-3.9,8.868,8.868,0,0,0-3.194-2.8,44.614,44.614,0,0,0-6.809-2.911q-6.388-2.286-9.127-5.417a11.959,11.959,0,0,1-2.739-8.171A10.164,10.164,0,0,1,127.739,27q3.976-3.131,10.52-3.131a31,31,0,0,1,12.555,2.5L149.154,31a28.377,28.377,0,0,0-11.02-2.38,10.665,10.665,0,0,0-6.607,1.816,5.985,5.985,0,0,0-2.379,5.041,7.733,7.733,0,0,0,.876,3.9,8.259,8.259,0,0,0,2.959,2.786,36.732,36.732,0,0,0,6.371,2.8q7.2,2.568,9.91,5.509a10.843,10.843,0,0,1,2.708,7.647Z"/>
|
||||
<path id="Path_3008" data-name="Path 3008" class="cls-2" d="M184.988,70.3,178.7,50.17q-.6-1.848-2.223-8.391h-.25q-1.254,5.479-2.192,8.453L167.549,70.3h-6.012l-9.361-34.316h5.448q3.318,12.932,5.056,19.694a79.655,79.655,0,0,1,1.988,9.111h.251q.343-1.784,1.111-4.618t1.331-4.493l6.293-19.694h5.635l6.137,19.694a66.551,66.551,0,0,1,2.38,9.049h.25a33.338,33.338,0,0,1,.673-3.476q.547-2.347,6.528-25.267h5.385L191.156,70.3Z"/>
|
||||
<path id="Path_3009" data-name="Path 3009" class="cls-2" d="M224.815,70.3l-1.033-4.885h-.251a14.446,14.446,0,0,1-5.119,4.368,15.6,15.6,0,0,1-6.372,1.143q-5.1,0-8-2.63t-2.9-7.484q0-10.394,16.625-10.894l5.824-.189V47.6q0-4.04-1.738-5.964T216.3,39.713a22.644,22.644,0,0,0-9.706,2.63L205,38.366a24.492,24.492,0,0,1,5.558-2.16,24.094,24.094,0,0,1,6.058-.782q6.137,0,9.1,2.723t2.959,8.736V70.3Zm-11.741-3.663a10.548,10.548,0,0,0,7.624-2.662,9.846,9.846,0,0,0,2.771-7.452v-3.1l-5.2.219q-6.2.219-8.939,1.925a5.81,5.81,0,0,0-2.739,5.307,5.357,5.357,0,0,0,1.706,4.29,7.082,7.082,0,0,0,4.777,1.475Z"/>
|
||||
<path id="Path_3010" data-name="Path 3010" class="cls-2" d="M264.3,35.986v3.288l-6.356.751a11.175,11.175,0,0,1,2.254,6.858,10.144,10.144,0,0,1-3.444,8.046q-3.444,3.006-9.455,3.006a15.655,15.655,0,0,1-2.881-.251Q241.1,59.439,241.1,62.1a2.243,2.243,0,0,0,1.158,2.082,8.459,8.459,0,0,0,3.976.673h6.074q5.572,0,8.563,2.348a8.159,8.159,0,0,1,2.99,6.825,9.742,9.742,0,0,1-4.571,8.688q-4.57,2.991-13.337,2.99-6.732,0-10.379-2.5a8.088,8.088,0,0,1-3.648-7.076,7.943,7.943,0,0,1,2-5.416,10.209,10.209,0,0,1,5.635-3.1,5.435,5.435,0,0,1-2.207-1.847,4.887,4.887,0,0,1-.892-2.912,5.524,5.524,0,0,1,1-3.287,10.517,10.517,0,0,1,3.162-2.724,9.266,9.266,0,0,1-4.336-3.726,10.949,10.949,0,0,1-1.675-6.011q0-5.635,3.381-8.689t9.581-3.053a17.449,17.449,0,0,1,4.853.626ZM236.932,76.063a4.658,4.658,0,0,0,2.349,4.226,12.969,12.969,0,0,0,6.731,1.44q6.543,0,9.691-1.956a5.992,5.992,0,0,0,3.146-5.307q0-2.788-1.722-3.867t-6.481-1.08h-6.231a8.207,8.207,0,0,0-5.51,1.69,6.044,6.044,0,0,0-1.973,4.854Zm2.818-29.086a6.985,6.985,0,0,0,2.036,5.447,8.121,8.121,0,0,0,5.667,1.847q7.607,0,7.607-7.388,0-7.734-7.7-7.735a7.628,7.628,0,0,0-5.635,1.973q-1.974,1.979-1.975,5.856Z"/>
|
||||
<path id="Path_3011" data-name="Path 3011" class="cls-2" d="M298.835,35.986v3.288l-6.356.751a11.166,11.166,0,0,1,2.255,6.858,10.147,10.147,0,0,1-3.444,8.046q-3.444,3.006-9.456,3.006a15.644,15.644,0,0,1-2.88-.251q-3.32,1.755-3.319,4.415a2.242,2.242,0,0,0,1.159,2.082,8.456,8.456,0,0,0,3.976.673h6.074q5.573,0,8.563,2.348a8.159,8.159,0,0,1,2.99,6.825,9.742,9.742,0,0,1-4.571,8.688q-4.572,2.991-13.338,2.99-6.732,0-10.379-2.5a8.087,8.087,0,0,1-3.647-7.076,7.942,7.942,0,0,1,2-5.416,10.212,10.212,0,0,1,5.636-3.1,5.429,5.429,0,0,1-2.207-1.847A4.887,4.887,0,0,1,271,62.85a5.524,5.524,0,0,1,1-3.287,10.517,10.517,0,0,1,3.162-2.724,9.275,9.275,0,0,1-4.336-3.726,10.949,10.949,0,0,1-1.675-6.011q0-5.635,3.382-8.689t9.58-3.053a17.439,17.439,0,0,1,4.853.626ZM271.471,76.063a4.659,4.659,0,0,0,2.348,4.226,12.973,12.973,0,0,0,6.732,1.44q6.543,0,9.69-1.956a5.991,5.991,0,0,0,3.147-5.307q0-2.788-1.723-3.867t-6.481-1.08h-6.23a8.2,8.2,0,0,0-5.51,1.69A6.044,6.044,0,0,0,271.471,76.063Zm2.818-29.086a6.985,6.985,0,0,0,2.035,5.447,8.123,8.123,0,0,0,5.667,1.847q7.608,0,7.608-7.388,0-7.734-7.7-7.735a7.628,7.628,0,0,0-5.635,1.973q-1.976,1.979-1.975,5.856Z"/>
|
||||
<path id="Path_3012" data-name="Path 3012" class="cls-2" d="M316.477,70.928q-7.606,0-12.006-4.634t-4.4-12.868q0-8.3,4.086-13.181A13.567,13.567,0,0,1,315.13,35.36a12.94,12.94,0,0,1,10.208,4.24q3.762,4.246,3.762,11.2v3.288H305.457q.156,6.042,3.052,9.173t8.156,3.131a27.629,27.629,0,0,0,10.958-2.317v4.634a27.541,27.541,0,0,1-5.212,1.706,29.262,29.262,0,0,1-5.934.513Zm-1.408-31.215a8.488,8.488,0,0,0-6.591,2.692,12.41,12.41,0,0,0-2.9,7.451h17.94q0-4.914-2.192-7.529a7.712,7.712,0,0,0-6.257-2.614Z"/>
|
||||
<path id="Path_3013" data-name="Path 3013" class="cls-2" d="M350.6,35.36a20.372,20.372,0,0,1,4.1.376l-.72,4.822a17.72,17.72,0,0,0-3.757-.47A9.144,9.144,0,0,0,343.1,43.47a12.326,12.326,0,0,0-2.959,8.422V70.3h-5.2V35.986h4.289l.6,6.357h.251a15.069,15.069,0,0,1,4.6-5.166A10.366,10.366,0,0,1,350.6,35.36Z"/>
|
||||
<path id="Path_3014" data-name="Path 3014" class="cls-2" d="M394.454,70.3H368.937V24.527h25.517v4.729H374.26V44h18.973v4.7H374.26V65.543h20.194Z"/>
|
||||
<path id="Path_3015" data-name="Path 3015" class="cls-2" d="M423.683,65.7H423.4q-3.6,5.231-10.77,5.229-6.734,0-10.473-4.6t-3.742-13.087q0-8.484,3.757-13.181t10.458-4.7q6.981,0,10.707,5.073h.407l-.219-2.474-.125-2.41V21.585h5.2V70.3h-4.227Zm-10.394.877q5.322,0,7.718-2.9t2.395-9.346v-1.1q0-7.294-2.427-10.41t-7.749-3.115a7.953,7.953,0,0,0-7,3.553Q403.8,46.821,403.8,53.3q0,6.574,2.41,9.925a8.166,8.166,0,0,0,7.079,3.351Z"/>
|
||||
<path id="Path_3016" data-name="Path 3016" class="cls-2" d="M435.918,26.688a3.449,3.449,0,0,1,.877-2.614,3.243,3.243,0,0,1,4.351.016,3.385,3.385,0,0,1,.909,2.6,3.442,3.442,0,0,1-.909,2.615,3.182,3.182,0,0,1-4.351,0,3.515,3.515,0,0,1-.877-2.617ZM441.554,70.3h-5.2V35.986h5.2Z"/>
|
||||
<path id="Path_3017" data-name="Path 3017" class="cls-2" d="M461.26,66.639a17.07,17.07,0,0,0,2.661-.2,17.339,17.339,0,0,0,2.035-.423v3.976a9.508,9.508,0,0,1-2.489.674,18.741,18.741,0,0,1-2.959.266q-9.955,0-9.956-10.489V40.025h-4.915v-2.5l4.915-2.161,2.191-7.326h3.006V35.99h9.956v4.039h-9.956v20.2a6.943,6.943,0,0,0,1.472,4.759,5.117,5.117,0,0,0,4.039,1.651Z"/>
|
||||
<path id="Path_3018" data-name="Path 3018" class="cls-2" d="M499.014,53.113q0,8.391-4.227,13.1t-11.68,4.712a15.478,15.478,0,0,1-8.171-2.16,14.264,14.264,0,0,1-5.51-6.2,21.6,21.6,0,0,1-1.941-9.455q0-8.389,4.2-13.072t11.647-4.681q7.2,0,11.443,4.79T499.014,53.113Zm-26.144,0q0,6.575,2.63,10.019t7.732,3.444q5.1,0,7.75-3.429t2.645-10.034q0-6.543-2.645-9.941t-7.812-3.4q-5.1,0-7.7,3.35T472.87,53.113Z"/>
|
||||
<path id="Path_3019" data-name="Path 3019" class="cls-2" d="M520.726,35.36a20.376,20.376,0,0,1,4.1.376l-.721,4.822a17.712,17.712,0,0,0-3.757-.47,9.145,9.145,0,0,0-7.123,3.382,12.335,12.335,0,0,0-2.958,8.422V70.3h-5.2V35.986h4.289l.6,6.357h.251a15.078,15.078,0,0,1,4.6-5.166A10.368,10.368,0,0,1,520.726,35.36Z"/>
|
||||
<path id="Path_3020" data-name="Path 3020" class="cls-2" d="M255.556,96.638s-3.43-.391-4.85-.391c-2.058,0-3.111.735-3.111,2.18,0,1.568.882,1.935,3.748,2.719,3.527.98,4.8,1.911,4.8,4.777,0,3.675-2.3,5.267-5.61,5.267a35.667,35.667,0,0,1-5.486-.662l.269-2.18s3.307.441,5.046.441c2.082,0,3.037-.931,3.037-2.7,0-1.421-.759-1.91-3.331-2.523-3.626-.93-5.193-2.033-5.193-4.948,0-3.381,2.229-4.776,5.585-4.776a37.222,37.222,0,0,1,5.316.587Z"/>
|
||||
<path id="Path_3021" data-name="Path 3021" class="cls-2" d="M262.666,94.14h4.728l3.748,13.106L274.89,94.14h4.752v16.78H276.9V96.42h-.144l-4.192,13.816h-2.841L265.53,96.418h-.145v14.5h-2.719Z"/>
|
||||
<path id="Path_3022" data-name="Path 3022" class="cls-2" d="M321.756,94.14H334v2.425h-4.728V110.92h-2.743V96.565h-4.777Z"/>
|
||||
<path id="Path_3023" data-name="Path 3023" class="cls-2" d="M345.837,94.14c3.331,0,5.119,1.249,5.119,4.361,0,2.033-.636,3.037-1.984,3.772,1.445.563,2.4,1.592,2.4,3.9,0,3.43-2.082,4.752-5.34,4.752h-6.566V94.14Zm-3.651,2.352v4.8h3.6c1.667,0,2.4-.832,2.4-2.474,0-1.617-.833-2.327-2.5-2.327Zm0,7.1v4.973h3.7c1.689,0,2.694-.539,2.694-2.548,0-1.911-1.421-2.425-2.743-2.425Z"/>
|
||||
<path id="Path_3024" data-name="Path 3024" class="cls-2" d="M358.113,94.14H368.7v2.377h-7.864v4.751h6.394V103.6h-6.394v4.924H368.7v2.4H358.113Z"/>
|
||||
<path id="Path_3025" data-name="Path 3025" class="cls-2" d="M378.446,94.14h5.414l4.164,16.78h-2.743l-1.24-4.92h-5.777l-1.239,4.923h-2.719Zm.362,9.456h4.707l-1.737-7.178h-1.225Z"/>
|
||||
<path id="Path_3026" data-name="Path 3026" class="cls-2" d="M396.8,105.947v4.973h-2.72V94.14h6.37c3.7,0,5.683,2.12,5.683,5.843,0,2.376-.956,4.519-2.744,5.352l2.769,5.585h-2.989l-2.425-4.973Zm3.65-9.455H396.8v7.1h3.7c2.058,0,2.841-1.85,2.841-3.589-.005-1.9-.935-3.511-2.895-3.511Z"/>
|
||||
<path id="Path_3027" data-name="Path 3027" class="cls-2" d="M289.712,94.14h5.413l4.165,16.78h-2.744L295.307,106H289.53l-1.239,4.923h-2.719Zm.361,9.456h4.707l-1.737-7.178h-1.224Z"/>
|
||||
<path id="Path_3028" data-name="Path 3028" class="cls-2" d="M308.061,105.947v4.973h-2.719V94.14h6.369c3.7,0,5.683,2.12,5.683,5.843,0,2.376-.955,4.519-2.743,5.352l2.768,5.585h-2.988l-2.426-4.973Zm3.65-9.455h-3.65v7.1h3.7c2.057,0,2.841-1.85,2.841-3.589C314.6,98.1,313.671,96.492,311.711,96.492Z"/>
|
||||
<path id="Path_3029" data-name="Path 3029" class="cls-2" d="M130.306,107.644a3.021,3.021,0,0,1-1.18,2.536,5.115,5.115,0,0,1-3.2.91,8.009,8.009,0,0,1-3.371-.565v-1.381a8.932,8.932,0,0,0,1.651.5,8.672,8.672,0,0,0,1.77.186,3.57,3.57,0,0,0,2.158-.544,1.782,1.782,0,0,0,.724-1.512,1.943,1.943,0,0,0-.257-1.049,2.382,2.382,0,0,0-.859-.755,12.173,12.173,0,0,0-1.833-.784,5.853,5.853,0,0,1-2.457-1.458,3.218,3.218,0,0,1-.737-2.2,2.734,2.734,0,0,1,1.07-2.266,4.44,4.44,0,0,1,2.832-.844,8.337,8.337,0,0,1,3.379.675l-.447,1.247a7.635,7.635,0,0,0-2.966-.641,2.877,2.877,0,0,0-1.778.489,1.613,1.613,0,0,0-.641,1.357,2.078,2.078,0,0,0,.236,1.049,2.218,2.218,0,0,0,.8.75,9.873,9.873,0,0,0,1.715.755,6.77,6.77,0,0,1,2.667,1.483,2.914,2.914,0,0,1,.724,2.062Z"/>
|
||||
<path id="Path_3030" data-name="Path 3030" class="cls-2" d="M134.147,101.686v5.992a2.41,2.41,0,0,0,.514,1.685,2.091,2.091,0,0,0,1.609.556,2.627,2.627,0,0,0,2.12-.792,4,4,0,0,0,.67-2.587v-4.854h1.4v9.236H139.3l-.2-1.238h-.076a2.8,2.8,0,0,1-1.192,1.045,4.019,4.019,0,0,1-1.74.361,3.527,3.527,0,0,1-2.524-.8,3.408,3.408,0,0,1-.839-2.561v-6.043Z"/>
|
||||
<path id="Path_3031" data-name="Path 3031" class="cls-2" d="M147.906,111.09a4,4,0,0,1-1.648-.332,3.113,3.113,0,0,1-1.251-1.024h-.1a12.458,12.458,0,0,1,.1,1.534v3.8h-1.4V101.686h1.137l.194,1.263h.068a3.244,3.244,0,0,1,1.256-1.094,3.81,3.81,0,0,1,1.643-.337,3.414,3.414,0,0,1,2.836,1.255,6.686,6.686,0,0,1-.018,7.057,3.417,3.417,0,0,1-2.817,1.26Zm-.2-8.385a2.48,2.48,0,0,0-2.047.784,4.03,4.03,0,0,0-.649,2.494v.312a4.616,4.616,0,0,0,.649,2.785,2.464,2.464,0,0,0,2.081.839,2.162,2.162,0,0,0,1.875-.97,4.588,4.588,0,0,0,.679-2.67,4.423,4.423,0,0,0-.679-2.65,2.232,2.232,0,0,0-1.915-.924Z"/>
|
||||
<path id="Path_3032" data-name="Path 3032" class="cls-2" d="M158.739,111.09a4,4,0,0,1-1.648-.332,3.113,3.113,0,0,1-1.251-1.024h-.1a12.461,12.461,0,0,1,.1,1.534v3.8h-1.4V101.686h1.137l.194,1.263h.068a3.244,3.244,0,0,1,1.256-1.094,3.81,3.81,0,0,1,1.643-.337,3.414,3.414,0,0,1,2.836,1.255,6.686,6.686,0,0,1-.018,7.057,3.417,3.417,0,0,1-2.817,1.26Zm-.2-8.385a2.48,2.48,0,0,0-2.048.784,4.03,4.03,0,0,0-.649,2.494v.312a4.616,4.616,0,0,0,.649,2.785,2.464,2.464,0,0,0,2.081.839,2.162,2.162,0,0,0,1.875-.97,4.588,4.588,0,0,0,.679-2.67,4.423,4.423,0,0,0-.679-2.65,2.231,2.231,0,0,0-1.91-.924Z"/>
|
||||
<path id="Path_3033" data-name="Path 3033" class="cls-2" d="M173.312,106.3a5.092,5.092,0,0,1-1.138,3.527,4,4,0,0,1-3.143,1.268,4.172,4.172,0,0,1-2.2-.581,3.843,3.843,0,0,1-1.482-1.669,5.8,5.8,0,0,1-.523-2.545,5.087,5.087,0,0,1,1.129-3.518,4,4,0,0,1,3.135-1.259,3.9,3.9,0,0,1,3.08,1.289A5.067,5.067,0,0,1,173.312,106.3Zm-7.037,0a4.384,4.384,0,0,0,.708,2.7,2.809,2.809,0,0,0,4.167,0,4.36,4.36,0,0,0,.713-2.7,4.294,4.294,0,0,0-.713-2.676,2.5,2.5,0,0,0-2.1-.914,2.462,2.462,0,0,0-2.073.9A4.344,4.344,0,0,0,166.275,106.3Z"/>
|
||||
<path id="Path_3034" data-name="Path 3034" class="cls-2" d="M180.224,101.518a5.5,5.5,0,0,1,1.1.1l-.193,1.3a4.8,4.8,0,0,0-1.012-.127,2.467,2.467,0,0,0-1.917.91,3.324,3.324,0,0,0-.8,2.268v4.955H176v-9.236h1.154l.161,1.71h.067a4.056,4.056,0,0,1,1.239-1.39,2.786,2.786,0,0,1,1.6-.49Z"/>
|
||||
<path id="Path_3035" data-name="Path 3035" class="cls-2" d="M187.062,109.936a4.53,4.53,0,0,0,.717-.055,4.647,4.647,0,0,0,.548-.113v1.07a2.613,2.613,0,0,1-.67.181,5.124,5.124,0,0,1-.8.071q-2.679,0-2.679-2.822v-5.5h-1.324V102.1l1.324-.581.589-1.973h.809v2.141h2.68v1.087h-2.68v5.436a1.868,1.868,0,0,0,.4,1.28,1.378,1.378,0,0,0,1.086.446Z"/>
|
||||
<path id="Path_3036" data-name="Path 3036" class="cls-2" d="M194.238,111.09a4.243,4.243,0,0,1-3.232-1.246,4.833,4.833,0,0,1-1.184-3.464,5.352,5.352,0,0,1,1.1-3.548,3.651,3.651,0,0,1,2.953-1.314,3.48,3.48,0,0,1,2.747,1.141,4.374,4.374,0,0,1,1.012,3.013v.885h-6.363a3.665,3.665,0,0,0,.822,2.468,2.839,2.839,0,0,0,2.2.844,7.453,7.453,0,0,0,2.95-.624v1.247a7.377,7.377,0,0,1-1.4.459,7.875,7.875,0,0,1-1.6.139Zm-.379-8.4a2.282,2.282,0,0,0-1.774.726,3.329,3.329,0,0,0-.78,2h4.829a3.073,3.073,0,0,0-.59-2.026,2.077,2.077,0,0,0-1.685-.7Z"/>
|
||||
<path id="Path_3037" data-name="Path 3037" class="cls-2" d="M206.65,109.684h-.075a3.289,3.289,0,0,1-2.9,1.406,3.428,3.428,0,0,1-2.819-1.238,5.452,5.452,0,0,1-1.007-3.523,5.548,5.548,0,0,1,1.011-3.548,3.4,3.4,0,0,1,2.815-1.263,3.359,3.359,0,0,1,2.882,1.365h.109l-.059-.666-.033-.649V97.81h1.4v13.112h-1.138Zm-2.8.235a2.55,2.55,0,0,0,2.077-.779,3.94,3.94,0,0,0,.645-2.516v-.3a4.638,4.638,0,0,0-.653-2.8,2.485,2.485,0,0,0-2.086-.839,2.142,2.142,0,0,0-1.883.957,4.754,4.754,0,0,0-.654,2.7,4.57,4.57,0,0,0,.649,2.672,2.2,2.2,0,0,0,1.908.905Z"/>
|
||||
<path id="Path_3038" data-name="Path 3038" class="cls-2" d="M220.411,101.534a3.437,3.437,0,0,1,2.828,1.243,6.659,6.659,0,0,1-.009,7.053,3.42,3.42,0,0,1-2.819,1.26,3.993,3.993,0,0,1-1.647-.332,3.109,3.109,0,0,1-1.252-1.024h-.1l-.295,1.188h-1V97.81h1.4v3.184q0,1.073-.067,1.922h.067a3.32,3.32,0,0,1,2.894-1.382Zm-.2,1.171a2.446,2.446,0,0,0-2.065.822,6.338,6.338,0,0,0,.017,5.553,2.466,2.466,0,0,0,2.082.839,2.16,2.16,0,0,0,1.921-.939,4.832,4.832,0,0,0,.632-2.7,4.645,4.645,0,0,0-.632-2.689,2.239,2.239,0,0,0-1.957-.886Z"/>
|
||||
<path id="Path_3039" data-name="Path 3039" class="cls-2" d="M225.458,101.686h1.5l2.022,5.267a20.027,20.027,0,0,1,.826,2.6h.067q.11-.431.46-1.471t2.287-6.4h1.5L230.152,112.2a5.256,5.256,0,0,1-1.378,2.212,2.933,2.933,0,0,1-1.934.653,5.645,5.645,0,0,1-1.264-.143V113.8a4.9,4.9,0,0,0,1.036.1,2.137,2.137,0,0,0,2.056-1.618l.514-1.314Z"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 17 KiB |
@@ -0,0 +1,393 @@
|
||||
import React from "react"
|
||||
import PropTypes from "prop-types"
|
||||
import Swagger from "swagger-client"
|
||||
import URL from "url"
|
||||
import "whatwg-fetch"
|
||||
import DropdownMenu from "./DropdownMenu"
|
||||
import reactFileDownload from "react-file-download"
|
||||
import YAML from "js-yaml"
|
||||
import beautifyJson from "json-beautify"
|
||||
|
||||
import Logo from "./logo_small.svg"
|
||||
|
||||
export default class Topbar extends React.Component {
|
||||
constructor(props, context) {
|
||||
super(props, context)
|
||||
|
||||
this.state = {
|
||||
swaggerClient: null,
|
||||
clients: [],
|
||||
servers: [],
|
||||
definitionVersion: "Unknown"
|
||||
}
|
||||
}
|
||||
|
||||
getGeneratorUrl = () => {
|
||||
const { isOAS3, isSwagger2 } = this.props.specSelectors
|
||||
const { swagger2GeneratorUrl, oas3GeneratorUrl } = this.props.getConfigs()
|
||||
|
||||
return isOAS3() ? oas3GeneratorUrl : (
|
||||
isSwagger2() ? swagger2GeneratorUrl : null
|
||||
)
|
||||
}
|
||||
|
||||
instantiateGeneratorClient = () => {
|
||||
|
||||
const generatorUrl = this.getGeneratorUrl()
|
||||
|
||||
const isOAS3 = this.props.specSelectors.isOAS3()
|
||||
|
||||
if(!generatorUrl) {
|
||||
return this.setState({
|
||||
clients: [],
|
||||
servers: []
|
||||
})
|
||||
}
|
||||
|
||||
Swagger(generatorUrl, {
|
||||
requestInterceptor: (req) => {
|
||||
req.headers["Accept"] = "application/json"
|
||||
req.headers["Content-Type"] = "application/json"
|
||||
}
|
||||
})
|
||||
.then(client => {
|
||||
this.setState({
|
||||
swaggerClient: client
|
||||
})
|
||||
|
||||
const clientGetter = isOAS3 ? client.apis.clients.clientLanguages : client.apis.clients.clientOptions
|
||||
const serverGetter = isOAS3 ? client.apis.servers.serverLanguages : client.apis.servers.serverOptions
|
||||
|
||||
|
||||
clientGetter({}, {
|
||||
// contextUrl is needed because swagger-client is curently
|
||||
// not building relative server URLs correctly
|
||||
contextUrl: generatorUrl
|
||||
})
|
||||
.then(res => {
|
||||
this.setState({ clients: res.body || [] })
|
||||
})
|
||||
|
||||
serverGetter({}, {
|
||||
// contextUrl is needed because swagger-client is curently
|
||||
// not building relative server URLs correctly
|
||||
contextUrl: generatorUrl
|
||||
})
|
||||
.then(res => {
|
||||
this.setState({ servers: res.body || [] })
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
downloadFile = (content, fileName) => {
|
||||
if(window.Cypress) {
|
||||
// HACK: temporary workaround for https://github.com/cypress-io/cypress/issues/949
|
||||
// allows e2e tests to proceed without choking on file download native event
|
||||
return
|
||||
}
|
||||
return reactFileDownload(content, fileName)
|
||||
}
|
||||
|
||||
// Menu actions
|
||||
|
||||
importFromURL = () => {
|
||||
let url = prompt("Enter the URL to import from:")
|
||||
|
||||
if(url) {
|
||||
fetch(url)
|
||||
.then(res => res.text())
|
||||
.then(text => {
|
||||
this.props.specActions.updateSpec(
|
||||
YAML.safeDump(YAML.safeLoad(text), {
|
||||
lineWidth: -1
|
||||
})
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
saveAsYaml = () => {
|
||||
let editorContent = this.props.specSelectors.specStr()
|
||||
let language = this.getDefinitionLanguage()
|
||||
let fileName = this.getFileName()
|
||||
|
||||
if(this.hasParserErrors()) {
|
||||
if(language === "yaml") {
|
||||
const shouldContinue = confirm("Swagger-Editor isn't able to parse your API definition. Are you sure you want to save the editor content as YAML?")
|
||||
if(!shouldContinue) return
|
||||
} else {
|
||||
return alert("Save as YAML is not currently possible because Swagger-Editor wasn't able to parse your API definiton.")
|
||||
}
|
||||
}
|
||||
|
||||
if(language === "yaml") {
|
||||
//// the content is YAML,
|
||||
//// so download as-is
|
||||
return this.downloadFile(editorContent, `${fileName}.yaml`)
|
||||
}
|
||||
|
||||
//// the content is JSON,
|
||||
//// so convert and download
|
||||
|
||||
// JSON String -> JS object
|
||||
let jsContent = YAML.safeLoad(editorContent)
|
||||
// JS object -> YAML string
|
||||
let yamlContent = YAML.safeDump(jsContent)
|
||||
this.downloadFile(yamlContent, `${fileName}.yaml`)
|
||||
}
|
||||
|
||||
saveAsJson = () => {
|
||||
let editorContent = this.props.specSelectors.specStr()
|
||||
let fileName = this.getFileName()
|
||||
|
||||
if(this.hasParserErrors()) {
|
||||
// we can't recover from a parser error in save as JSON
|
||||
// because we are always parsing so we can beautify
|
||||
return alert("Save as JSON is not currently possible because Swagger-Editor wasn't able to parse your API definiton.")
|
||||
}
|
||||
|
||||
// JSON or YAML String -> JS object
|
||||
let jsContent = YAML.safeLoad(editorContent)
|
||||
// JS Object -> pretty JSON string
|
||||
let prettyJsonContent = beautifyJson(jsContent, null, 2)
|
||||
this.downloadFile(prettyJsonContent, `${fileName}.json`)
|
||||
}
|
||||
|
||||
saveAsText = () => {
|
||||
// Download raw text content
|
||||
console.warn("DEPRECATED: saveAsText will be removed in the next minor version.")
|
||||
let editorContent = this.props.specSelectors.specStr()
|
||||
let isOAS3 = this.props.specSelectors.isOAS3()
|
||||
let fileName = isOAS3 ? "openapi.txt" : "swagger.txt"
|
||||
this.downloadFile(editorContent, fileName)
|
||||
}
|
||||
|
||||
convertToYaml = () => {
|
||||
// Editor content -> JS object -> YAML string
|
||||
let editorContent = this.props.specSelectors.specStr()
|
||||
let jsContent = YAML.safeLoad(editorContent)
|
||||
let yamlContent = YAML.safeDump(jsContent)
|
||||
this.props.specActions.updateSpec(yamlContent)
|
||||
}
|
||||
|
||||
downloadGeneratedFile = (type, name) => {
|
||||
let { specSelectors } = this.props
|
||||
let swaggerClient = this.state.swaggerClient
|
||||
if(!swaggerClient) {
|
||||
// Swagger client isn't ready yet.
|
||||
return
|
||||
}
|
||||
|
||||
if(specSelectors.isOAS3()) {
|
||||
// Generator 3 only has one generate endpoint for all types of things...
|
||||
// since we're using the tags interface we may as well use the client reference to it
|
||||
swaggerClient.apis.clients.generate({}, {
|
||||
requestBody: {
|
||||
spec: specSelectors.specJson(),
|
||||
type: type.toUpperCase(),
|
||||
lang: name
|
||||
},
|
||||
contextUrl: this.getGeneratorUrl()
|
||||
}).then(res => {
|
||||
this.downloadFile(res.data, `${name}-${type}-generated.zip`)
|
||||
})
|
||||
} else if(type === "server") {
|
||||
swaggerClient.apis.servers.generateServerForLanguage({
|
||||
framework : name,
|
||||
body: JSON.stringify({
|
||||
spec: specSelectors.specJson()
|
||||
}),
|
||||
headers: JSON.stringify({
|
||||
Accept: "application/json"
|
||||
})
|
||||
})
|
||||
.then(res => this.handleResponse(res, { type, name }))
|
||||
} else if(type === "client") {
|
||||
swaggerClient.apis.clients.generateClient({
|
||||
language : name,
|
||||
body: JSON.stringify({
|
||||
spec: specSelectors.specJson()
|
||||
})
|
||||
})
|
||||
.then(res => this.handleResponse(res, { type, name }))
|
||||
}
|
||||
}
|
||||
|
||||
handleResponse = (res, { type, name }) => {
|
||||
if(!res.ok) {
|
||||
return console.error(res)
|
||||
}
|
||||
|
||||
let downloadUrl = URL.parse(res.body.link)
|
||||
|
||||
// HACK: workaround for Swagger.io Generator 2.0's lack of HTTPS downloads
|
||||
if(downloadUrl.hostname === "generator.swagger.io") {
|
||||
downloadUrl.protocol = "https:"
|
||||
delete downloadUrl.port
|
||||
delete downloadUrl.host
|
||||
}
|
||||
|
||||
fetch(URL.format(downloadUrl))
|
||||
.then(res => res.blob())
|
||||
.then(res => {
|
||||
this.downloadFile(res, `${name}-${type}-generated.zip`)
|
||||
})
|
||||
}
|
||||
|
||||
clearEditor = () => {
|
||||
if(window.localStorage) {
|
||||
window.localStorage.removeItem("swagger-editor-content")
|
||||
this.props.specActions.updateSpec("")
|
||||
}
|
||||
}
|
||||
|
||||
// Helpers
|
||||
showModal = (name) => {
|
||||
this.setState({
|
||||
[name]: true
|
||||
})
|
||||
}
|
||||
|
||||
hideModal = (name) => {
|
||||
this.setState({
|
||||
[name]: false
|
||||
})
|
||||
}
|
||||
|
||||
// Logic helpers
|
||||
|
||||
hasParserErrors = () => {
|
||||
return this.props.errSelectors.allErrors().filter(err => err.get("source") === "parser").size > 0
|
||||
}
|
||||
|
||||
getFileName = () => {
|
||||
// Use `isSwagger2` here, because we want to default to `openapi` if we don't know.
|
||||
if(this.props.specSelectors.isSwagger2 && this.props.specSelectors.isSwagger2()) {
|
||||
return "swagger"
|
||||
}
|
||||
|
||||
return "openapi"
|
||||
}
|
||||
|
||||
getDefinitionLanguage = () => {
|
||||
let editorContent = this.props.specSelectors.specStr() || ""
|
||||
|
||||
if(editorContent.trim()[0] === "{") {
|
||||
return "json"
|
||||
}
|
||||
|
||||
return "yaml"
|
||||
}
|
||||
|
||||
|
||||
getDefinitionVersion = () => {
|
||||
const { isOAS3, isSwagger2 } = this.props.specSelectors
|
||||
|
||||
return isOAS3() ? "OAS3" : (
|
||||
isSwagger2() ? "Swagger2" : "Unknown"
|
||||
)
|
||||
}
|
||||
|
||||
///// Lifecycle
|
||||
|
||||
componentDidMount() {
|
||||
this.instantiateGeneratorClient()
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
const version = this.getDefinitionVersion()
|
||||
|
||||
if(this.state.definitionVersion !== version) {
|
||||
// definition version has changed; need to reinstantiate
|
||||
// our Generator client
|
||||
// --
|
||||
// TODO: fix this if there's A Better Way
|
||||
// eslint-disable-next-line react/no-did-update-set-state
|
||||
this.setState({
|
||||
definitionVersion: version
|
||||
}, () => this.instantiateGeneratorClient())
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
let { getComponent, specSelectors, topbarActions } = this.props
|
||||
const Link = getComponent("Link")
|
||||
const TopbarInsert = getComponent("TopbarInsert")
|
||||
const ImportFileMenuItem = getComponent("ImportFileMenuItem")
|
||||
const ConvertDefinitionMenuItem = getComponent("ConvertDefinitionMenuItem")
|
||||
|
||||
let showServersMenu = this.state.servers && this.state.servers.length
|
||||
let showClientsMenu = this.state.clients && this.state.clients.length
|
||||
|
||||
let definitionLanguage = this.getDefinitionLanguage()
|
||||
|
||||
let isJson = definitionLanguage === "json"
|
||||
|
||||
let makeMenuOptions = (name) => {
|
||||
let stateKey = `is${name}MenuOpen`
|
||||
let toggleFn = () => this.setState({ [stateKey]: !this.state[stateKey] })
|
||||
return {
|
||||
isOpen: !!this.state[stateKey],
|
||||
close: () => this.setState({ [stateKey]: false }),
|
||||
align: "left",
|
||||
toggle: <span className="menu-item" onClick={toggleFn}>{ name }</span>
|
||||
}
|
||||
}
|
||||
|
||||
const saveAsElements = []
|
||||
|
||||
if(isJson) {
|
||||
saveAsElements.push(<li><button type="button" onClick={this.saveAsJson}>Save as JSON</button></li>)
|
||||
saveAsElements.push(<li><button type="button" onClick={this.saveAsYaml}>Convert and save as YAML</button></li>)
|
||||
} else {
|
||||
saveAsElements.push(<li><button type="button" onClick={this.saveAsYaml}>Save as YAML</button></li>)
|
||||
saveAsElements.push(<li><button type="button" onClick={this.saveAsJson}>Convert and save as JSON</button></li>)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="swagger-editor-standalone">
|
||||
<div className="topbar">
|
||||
<div className="topbar-wrapper">
|
||||
<Link href="#">
|
||||
<img height="35" className="topbar-logo__img" src={ Logo } alt=""/>
|
||||
</Link>
|
||||
<DropdownMenu {...makeMenuOptions("File")}>
|
||||
<li><button type="button" onClick={this.importFromURL}>Import URL</button></li>
|
||||
<ImportFileMenuItem onDocumentLoad={content => this.props.specActions.updateSpec(content)} />
|
||||
<li role="separator"></li>
|
||||
{saveAsElements}
|
||||
<li role="separator"></li>
|
||||
<li><button type="button" onClick={this.clearEditor}>Clear editor</button></li>
|
||||
</DropdownMenu>
|
||||
<DropdownMenu {...makeMenuOptions("Edit")}>
|
||||
<li><button type="button" onClick={this.convertToYaml}>Convert to YAML</button></li>
|
||||
<ConvertDefinitionMenuItem
|
||||
isSwagger2={specSelectors.isSwagger2()}
|
||||
onClick={() => topbarActions.showModal("convert")}
|
||||
/>
|
||||
</DropdownMenu>
|
||||
<TopbarInsert {...this.props} />
|
||||
{ showServersMenu ? <DropdownMenu className="long" {...makeMenuOptions("Generate Server")}>
|
||||
{ this.state.servers
|
||||
.map((serv, i) => <li key={i}><button type="button" onClick={this.downloadGeneratedFile.bind(null, "server", serv)}>{serv}</button></li>) }
|
||||
</DropdownMenu> : null }
|
||||
{ showClientsMenu ? <DropdownMenu className="long" {...makeMenuOptions("Generate Client")}>
|
||||
{ this.state.clients
|
||||
.map((cli, i) => <li key={i}><button type="button" onClick={this.downloadGeneratedFile.bind(null, "client", cli)}>{cli}</button></li>) }
|
||||
</DropdownMenu> : null }
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Topbar.propTypes = {
|
||||
specSelectors: PropTypes.object.isRequired,
|
||||
errSelectors: PropTypes.object.isRequired,
|
||||
specActions: PropTypes.object.isRequired,
|
||||
topbarActions: PropTypes.object.isRequired,
|
||||
getComponent: PropTypes.func.isRequired,
|
||||
getConfigs: PropTypes.func.isRequired
|
||||
}
|
||||
Reference in New Issue
Block a user