Skip to content

Commit

Permalink
feature: drag and drop files into the editor (swagger-api#1660)
Browse files Browse the repository at this point in the history
* Enable users to drag and drop spec files into the editor
* adding some tests
* Update tests to use expect, and remove sinon
* Adding some documentation for the drag and drop feature
* refine documentation
* `spec` -> `document`
* meta: increate async test timeout to 110ms in CI
  • Loading branch information
zainkhalid authored and shockey committed Feb 9, 2018
1 parent c1d3e14 commit 6a705cf
Show file tree
Hide file tree
Showing 8 changed files with 177 additions and 13 deletions.
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,10 @@ docker run -d -p 80:8080 swagger-editor

You can then view the app by navigating to `http://localhost` in your browser.

## Documentation

* [Importing your OpenAPI document](docs/import.md)

## Security contact

Please disclose any security-related issues or vulnerabilities by emailing [[email protected]](mailto:[email protected]), instead of using the public issue tracker.
Expand Down
Binary file added docs/drag-and-drop.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
17 changes: 17 additions & 0 deletions docs/import.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Importing OpenAPI documents

Swagger Editor can import your OpenAPI document, which can be formatted as JSON or YAML.

### File → Import File

Click **Choose File** and select import. The file you are importing has to be a valid JSON or YAML OpenAPI document. Swagger Editor will prompt you about validation errors, if any exist.

### File → Import URL

Paste the URL to your OpenAPI document.

### Drag and Drop

Simply drag and drop your OpenAPI JSON or YAML document into the Swagger Editor browser window.

![Swagger Editor drag and drop demo](./drag-and-drop.gif)
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
"react-addons-css-transition-group": "^15.4.2",
"react-dd-menu": "^2.0.0",
"react-dom": "^15.6.2",
"react-dropzone": "^4.2.7",
"react-file-download": "^0.3.2",
"react-redux": "^4.x.x",
"react-transition-group": "^1.1.1",
Expand Down
57 changes: 46 additions & 11 deletions src/layout.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React from "react"
import PropTypes from "prop-types"
import Dropzone from "react-dropzone"

export default class EditorLayout extends React.Component {

Expand All @@ -17,6 +18,23 @@ export default class EditorLayout extends React.Component {
this.props.specActions.updateSpec(newYaml)
}

onDrop = (acceptedFiles, rejectedFiles) => {
const someFilesWereRejected = rejectedFiles && rejectedFiles.length > 0
const thereIsExactlyOneAcceptedFile = acceptedFiles && acceptedFiles.length === 1
if ( someFilesWereRejected || !thereIsExactlyOneAcceptedFile) {
alert("Sorry, there was an error processing your file(s).\nPlease drag and drop one (and only one) .yaml or .json OpenAPI spec file.")
} else {
const file = acceptedFiles[0]
const reader = new FileReader()
reader.onloadend = () => {
const spec = reader.result
this.onChange(spec)
}

reader.readAsText(file, "utf-8")
}
}

render() {
let { getComponent } = this.props

Expand All @@ -29,16 +47,33 @@ export default class EditorLayout extends React.Component {
return (
<div>
<Container className='container'>
<SplitPaneMode>
<EditorContainer
onChange={this.onChange}
/>
<UIBaseLayout/>
</SplitPaneMode>
</Container>
</div>

)
<Dropzone
className="dropzone"
accept=".yaml,application/json"
multiple={false}
onDrop={this.onDrop}
disablePreview
disableClick
>
{({ isDragActive }) => {
if (isDragActive) {
return (
<div className="dropzone__overlay">
Please drop a .yaml or .json OpenAPI spec.
</div>
)
} else {
return (
<SplitPaneMode>
<EditorContainer onChange={this.onChange} />
<UIBaseLayout/>
</SplitPaneMode>
)
}
}}
</Dropzone>
</Container>
</div>
)
}

}
19 changes: 18 additions & 1 deletion src/plugins/editor/components/editor.less
Original file line number Diff line number Diff line change
@@ -1,11 +1,28 @@
#swagger-editor {
#ace-editor {
height:~"calc(100vh - 46px)" !important;
height: ~"calc(100vh - 46px)" !important;
}

.SplitPane {
height: ~"calc(100% - 46px)" !important;
}

.dropzone {
height: 100vh;
width: 100vw;

.dropzone__overlay {
padding-top: 20px;
height: 100vh;
width: 100vw;
position: absolute;
left: 0;
background: #2D2D2D;
text-align: center;
color: #fff;
font-size: 1rem;
}
}
}

@import './read-only-watermark.less';
Expand Down
90 changes: 90 additions & 0 deletions test/layout.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import expect from "expect"

describe("EditorLayout", function() {
let EditorLayout

// If alert isn't defined, create a dummy one, and remember to clean it up afterwards
if (typeof global.alert === "undefined") {
before(function () {
global.alert = function() { }
})
after(function () {
delete global.alert
})
}

// Same for FileReader
if (typeof global.FileReader === "undefined") {
before(function () {
global.FileReader = function() {}
})
after(function () {
delete global.FileReader
})
}

// Create spies for alert and FileReader, and then load the module under test.
before(function () {
expect.spyOn(global, "alert")
expect.spyOn(global, "FileReader")
EditorLayout = require("src/layout").default
})
// Undo the spies afterwards
after(function () {
expect.restoreSpies()
})

// Reset spies after each test
afterEach(function () {
global.alert.reset()
global.FileReader.reset()
})

describe("when file(s) are dropped", function() {
describe("if one or more files are of an unexpected type", function() {
it("should alert the user that their file(s) were rejected", () => {
const editorLayout = new EditorLayout()

editorLayout.onDrop([], ["rejected.file.1"])
editorLayout.onDrop([], ["rejected.file.1", "rejected.file.2"])

expect(global.alert.calls.length).toEqual(2)

global.alert.calls.forEach(call => {
expect(call.arguments[0]).toMatch(/^Sorry.*/)
})
})
})

describe("if more than one file of an expected type is dropped", function() {
it("should alert the user that their file(s) were rejected", () => {
const editorLayout = new EditorLayout()

editorLayout.onDrop(["accepted.file.1", "accepted.file.2"], [])
expect(global.alert.calls.length).toEqual(1)
expect(global.alert.calls[0].arguments[0]).toMatch(/^Sorry.*/)
})
})

describe("if exactly one file of an expected type is dropped", function() {
it("should call the updateSpec function passed in as props with the contents of the file", () => {
const fileContents = "This is my awesome file!"
const props = {
specActions: {
updateSpec: expect.createSpy()
}
}
global.FileReader.andReturn({
readAsText: function () { this.onloadend() },
result: fileContents
})

const editorLayout = new EditorLayout(props)

editorLayout.onDrop(["accepted.file"])

expect(props.specActions.updateSpec).toHaveBeenCalledWith(fileContents)
})
})
})
})
2 changes: 1 addition & 1 deletion test/plugins/validate-semantic/validate-helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import ValidateBasePlugin from "plugins/validate-base"
import ValidateSemanticPlugin from "plugins/validate-semantic"
import ASTPlugin from "plugins/ast"

const DELAY_MS = process.env.CI === "true" ? 100 : 60
const DELAY_MS = process.env.CI === "true" ? 110 : 60

export default function validateHelper(spec) {
return new Promise((resolve) => {
Expand Down

0 comments on commit 6a705cf

Please sign in to comment.