Added Swagger

This commit is contained in:
2020-06-10 08:25:21 +02:00
parent 5c6f37eaf7
commit af76cbca87
257 changed files with 48861 additions and 12 deletions
+6
View File
@@ -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
+23
View File
@@ -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>
)
}
}
+3
View File
@@ -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;
}
}
}
}
}
}
+114
View File
@@ -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">&times;</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
}
}
@@ -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">&times;</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
})
}
@@ -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>
)
}
}
+34
View File
@@ -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

+393
View File
@@ -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
}