Added Swagger
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
export const JUMP_TO_LINE = "jump_to_line"
|
||||
|
||||
export function jumpToLine(line) {
|
||||
return {
|
||||
type: JUMP_TO_LINE,
|
||||
payload: line
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// This is a hook. Will have editor instance
|
||||
// It needs to be an async-function, to avoid dispatching an object to the reducer
|
||||
export const onLoad = () => () => {}
|
||||
@@ -0,0 +1,6 @@
|
||||
/* global ace */
|
||||
ace.define("ace/snippets/yaml",
|
||||
["require","exports","module"], function(e,t,n){ // eslint-disable-line no-unused-vars
|
||||
t.snippetText=undefined
|
||||
t.scope="yaml"
|
||||
})
|
||||
@@ -0,0 +1,65 @@
|
||||
import React from "react"
|
||||
import PropTypes from "prop-types"
|
||||
|
||||
export default class EditorContainer extends React.Component {
|
||||
|
||||
// This is already debounced by editor.jsx
|
||||
onChange = (value) => {
|
||||
this.props.onChange(value)
|
||||
}
|
||||
|
||||
render() {
|
||||
let { specSelectors, getComponent, errSelectors, fn, editorSelectors, configsSelectors } = this.props
|
||||
|
||||
let Editor = getComponent("Editor")
|
||||
|
||||
let wrapperClasses = ["editor-wrapper"]
|
||||
const readOnly = !!configsSelectors.get("readOnly")
|
||||
|
||||
if(readOnly) {
|
||||
wrapperClasses.push("read-only")
|
||||
}
|
||||
|
||||
let propsForEditor = this.props
|
||||
|
||||
const editorOptions = {
|
||||
enableLiveAutocompletion: configsSelectors.get("editorLiveAutocomplete"),
|
||||
readOnly: readOnly,
|
||||
highlightActiveLine: !readOnly,
|
||||
highlightGutterLine: !readOnly,
|
||||
}
|
||||
|
||||
return (
|
||||
<div id='editor-wrapper' className={wrapperClasses.join(" ")}>
|
||||
{ readOnly ? <h2 className="editor-readonly-watermark">Read Only</h2> : null }
|
||||
<Editor
|
||||
{...propsForEditor}
|
||||
value={specSelectors.specStr()}
|
||||
origin={specSelectors.specOrigin()}
|
||||
editorOptions={editorOptions}
|
||||
specObject={specSelectors.specJson().toJS()}
|
||||
errors={errSelectors.allErrors()}
|
||||
onChange={this.onChange}
|
||||
goToLine={editorSelectors.gotoLine()}
|
||||
AST={fn.AST}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
EditorContainer.defaultProps = {
|
||||
onChange: Function.prototype
|
||||
}
|
||||
|
||||
EditorContainer.propTypes = {
|
||||
specActions: PropTypes.object.isRequired,
|
||||
configsSelectors: PropTypes.object.isRequired,
|
||||
onChange: PropTypes.func,
|
||||
fn: PropTypes.object,
|
||||
specSelectors: PropTypes.object.isRequired,
|
||||
errSelectors: PropTypes.object.isRequired,
|
||||
editorSelectors: PropTypes.object.isRequired,
|
||||
getComponent: PropTypes.func.isRequired,
|
||||
}
|
||||
@@ -0,0 +1,326 @@
|
||||
import React from "react"
|
||||
import PropTypes from "prop-types"
|
||||
import AceEditor from "react-ace"
|
||||
import editorPluginsHook from "../editor-plugins/hook"
|
||||
import { placeMarkerDecorations } from "../editor-helpers/marker-placer"
|
||||
import Im, { fromJS } from "immutable"
|
||||
import ImPropTypes from "react-immutable-proptypes"
|
||||
|
||||
import win from "src/window"
|
||||
|
||||
import isUndefined from "lodash/isUndefined"
|
||||
import omit from "lodash/omit"
|
||||
import isEqual from "lodash/isEqual"
|
||||
import debounce from "lodash/debounce"
|
||||
|
||||
import ace from "brace"
|
||||
import "brace/mode/yaml"
|
||||
import "brace/theme/tomorrow_night_eighties"
|
||||
import "brace/ext/language_tools"
|
||||
import "brace/ext/searchbox"
|
||||
import "./brace-snippets-yaml"
|
||||
|
||||
const NOOP = Function.prototype // Apparently the best way to no-op
|
||||
|
||||
export default function makeEditor({ editorPluginsToRun }) {
|
||||
|
||||
class Editor extends React.Component {
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context)
|
||||
|
||||
this.editor = null
|
||||
|
||||
this.debouncedOnChange = props.debounce > 0
|
||||
? debounce(props.onChange, props.debounce)
|
||||
: props.onChange
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
specId: PropTypes.string,
|
||||
value: PropTypes.string,
|
||||
editorOptions: PropTypes.object,
|
||||
origin: PropTypes.string,
|
||||
debounce: PropTypes.number,
|
||||
|
||||
onChange: PropTypes.func,
|
||||
onMarkerLineUpdate: PropTypes.func,
|
||||
|
||||
markers: PropTypes.object,
|
||||
goToLine: PropTypes.object,
|
||||
specObject: PropTypes.object.isRequired,
|
||||
|
||||
editorActions: PropTypes.object,
|
||||
|
||||
AST: PropTypes.object.isRequired,
|
||||
|
||||
errors: ImPropTypes.list,
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
value: "",
|
||||
specId: "--unknown--",
|
||||
origin: "not-editor",
|
||||
onChange: NOOP,
|
||||
onMarkerLineUpdate: NOOP,
|
||||
markers: {},
|
||||
goToLine: {},
|
||||
errors: fromJS([]),
|
||||
editorActions: {onLoad(){}},
|
||||
editorOptions: {},
|
||||
debounce: 800 // 0.5 imperial seconds™
|
||||
|
||||
}
|
||||
|
||||
checkForSilentOnChange = (value) => {
|
||||
if(!this.silent) {
|
||||
this.debouncedOnChange(value)
|
||||
}
|
||||
}
|
||||
|
||||
onLoad = (editor) => {
|
||||
|
||||
const { props } = this
|
||||
const { AST, specObject } = props
|
||||
|
||||
const langTools = ace.acequire("ace/ext/language_tools")
|
||||
const session = editor.getSession()
|
||||
|
||||
this.editor = editor
|
||||
|
||||
// fixes a warning, see https://github.com/ajaxorg/ace/issues/2499
|
||||
editor.$blockScrolling = Infinity
|
||||
|
||||
|
||||
session.setUseWrapMode(true)
|
||||
session.on("changeScrollLeft", xPos => { // eslint-disable-line no-unused-vars
|
||||
session.setScrollLeft(0)
|
||||
})
|
||||
|
||||
// TODO Remove this in favour of editorActions.onLoad
|
||||
editorPluginsHook(editor, props, editorPluginsToRun || [], {
|
||||
langTools, AST, specObject
|
||||
})
|
||||
|
||||
editor.setHighlightActiveLine(false)
|
||||
editor.setHighlightActiveLine(true)
|
||||
this.syncOptionsFromState(props.editorOptions)
|
||||
if(props.editorActions && props.editorActions.onLoad)
|
||||
props.editorActions.onLoad({...props, langTools, editor})
|
||||
|
||||
this.updateMarkerAnnotations(this.props)
|
||||
}
|
||||
|
||||
onResize = () => {
|
||||
const { editor } = this
|
||||
if(editor) {
|
||||
let session = editor.getSession()
|
||||
editor.resize()
|
||||
let wrapLimit = session.getWrapLimit()
|
||||
editor.setPrintMarginColumn(wrapLimit)
|
||||
}
|
||||
}
|
||||
|
||||
onClick = () => {
|
||||
// onClick is deferred by 40ms, to give element resizes time to settle.
|
||||
setTimeout(() => {
|
||||
if(this.getWidth() !== this.width) {
|
||||
this.onResize()
|
||||
this.width = this.getWidth()
|
||||
}
|
||||
}, 40)
|
||||
}
|
||||
|
||||
getWidth = () => {
|
||||
let el = win.document.getElementById("editor-wrapper")
|
||||
return el ? el.getBoundingClientRect().width : null
|
||||
}
|
||||
|
||||
updateErrorAnnotations = (nextProps) => {
|
||||
if(this.editor && nextProps.errors) {
|
||||
let editorAnnotations = nextProps.errors.toJS().map(err => {
|
||||
// Create annotation objects that ACE can use
|
||||
return {
|
||||
row: err.line - 1,
|
||||
column: 0,
|
||||
type: err.level,
|
||||
text: err.message
|
||||
}
|
||||
})
|
||||
|
||||
this.editor.getSession().setAnnotations(editorAnnotations)
|
||||
}
|
||||
}
|
||||
|
||||
updateMarkerAnnotations = (props) => {
|
||||
const { editor } = this
|
||||
|
||||
const markers = Im.Map.isMap(props.markers) ? props.markers.toJS() : {}
|
||||
this._removeMarkers = placeMarkerDecorations({
|
||||
editor,
|
||||
markers,
|
||||
onMarkerLineUpdate: props.onMarkerLineUpdate,
|
||||
})
|
||||
}
|
||||
|
||||
removeMarkers = () => {
|
||||
if(this._removeMarkers) {
|
||||
this._removeMarkers()
|
||||
this._removeMarkers = null
|
||||
}
|
||||
}
|
||||
|
||||
shouldUpdateYaml = (props) => {
|
||||
// No editor instance
|
||||
if(!this.editor)
|
||||
return false
|
||||
|
||||
// Origin is editor
|
||||
if(props.origin === "editor")
|
||||
return false
|
||||
|
||||
// Redundant
|
||||
if(this.editor.getValue() === props.value)
|
||||
return false
|
||||
|
||||
// Value and origin are same, no update.
|
||||
if(this.props.value === props.value
|
||||
&& this.props.origin === props.origin)
|
||||
return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
shouldUpdateMarkers = (props) => {
|
||||
const { markers } = props
|
||||
if(Im.Map.isMap(markers)) {
|
||||
return !Im.is(markers, this.props.markers) // Different from previous?
|
||||
}
|
||||
return true // Not going to do a deep compare of object-like markers
|
||||
}
|
||||
|
||||
updateYamlAndMarkers = (props) => {
|
||||
// If we update the yaml, we need to "lift" the yaml first
|
||||
if(this.shouldUpdateYaml(props)) {
|
||||
this.removeMarkers()
|
||||
this.updateYaml(props)
|
||||
this.updateMarkerAnnotations(props)
|
||||
|
||||
} else if (this.shouldUpdateMarkers(props)) {
|
||||
this.removeMarkers()
|
||||
this.updateMarkerAnnotations(props)
|
||||
}
|
||||
}
|
||||
|
||||
updateYaml = (props) => {
|
||||
if (props.origin === "insert") {
|
||||
// Don't clobber the undo stack in this case.
|
||||
this.editor.session.doc.setValue(props.value)
|
||||
this.editor.selection.clearSelection()
|
||||
} else {
|
||||
// session.setValue does not trigger onChange, nor add to undo stack.
|
||||
// Neither of which we want here.
|
||||
this.editor.session.setValue(props.value)
|
||||
}
|
||||
}
|
||||
|
||||
syncOptionsFromState = (editorOptions={}) => {
|
||||
const { editor } = this
|
||||
if(!editor) {
|
||||
return
|
||||
}
|
||||
|
||||
const setOptions = omit(editorOptions, ["readOnly"])
|
||||
editor.setOptions(setOptions)
|
||||
|
||||
|
||||
const readOnly = isUndefined(editorOptions.readOnly)
|
||||
? false
|
||||
: editorOptions.readOnly // If its undefined, default to false.
|
||||
editor.setReadOnly(readOnly)
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
// add user agent info to document
|
||||
// allows our custom Editor styling for IE10 to take effect
|
||||
var doc = win.document.documentElement
|
||||
doc.setAttribute("data-useragent", win.navigator.userAgent)
|
||||
this.syncOptionsFromState(this.props.editorOptions)
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
// eslint-disable-next-line react/no-did-mount-set-state
|
||||
|
||||
this.width = this.getWidth()
|
||||
win.document.addEventListener("click", this.onClick)
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
win.document.removeEventListener("click", this.onClick)
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
let hasChanged = (k) => !isEqual(nextProps[k], this.props[k])
|
||||
const editor = this.editor
|
||||
|
||||
// Change the debounce value/func
|
||||
if(this.props.debounce !== nextProps.debounce) {
|
||||
if(this.debouncedOnChange.flush)
|
||||
this.debouncedOnChange.flush()
|
||||
|
||||
this.debouncedOnChange = nextProps.debounce > 0
|
||||
? debounce(nextProps.onChange, nextProps.debounce)
|
||||
: nextProps.onChange
|
||||
}
|
||||
|
||||
this.updateYamlAndMarkers(nextProps)
|
||||
this.updateErrorAnnotations(nextProps)
|
||||
|
||||
if(hasChanged("editorOptions")) {
|
||||
this.syncOptionsFromState(nextProps.editorOptions)
|
||||
}
|
||||
|
||||
if(editor && nextProps.goToLine && nextProps.goToLine.line && hasChanged("goToLine")) {
|
||||
editor.gotoLine(nextProps.goToLine.line)
|
||||
nextProps.editorActions.jumpToLine(null)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
shouldComponentUpdate() {
|
||||
return false // Never update, see: componentWillRecieveProps and this.updateYaml for where we update things.
|
||||
}
|
||||
|
||||
render() {
|
||||
// NOTE: we're manually managing the value lifecycle, outside of react render
|
||||
// This will only render once.
|
||||
return (
|
||||
<AceEditor
|
||||
mode="yaml"
|
||||
theme="tomorrow_night_eighties"
|
||||
value={this.props.value /* This will only load once, thereafter it'll be via updateYaml */}
|
||||
onLoad={this.onLoad}
|
||||
onChange={this.checkForSilentOnChange}
|
||||
name="ace-editor"
|
||||
width="100%"
|
||||
height="100%"
|
||||
tabSize={2}
|
||||
fontSize={14}
|
||||
useSoftTabs="true"
|
||||
wrapEnabled={true}
|
||||
editorProps={{
|
||||
"display_indent_guides": true,
|
||||
folding: "markbeginandend"
|
||||
}}
|
||||
setOptions={{
|
||||
cursorStyle: "smooth",
|
||||
wrapBehavioursEnabled: true
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return Editor
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
// This code is registered as a helper, not a plugin, because its lifecycle is
|
||||
// unique to the needs of the marker placement logic.
|
||||
|
||||
import countBy from "lodash/countBy"
|
||||
import map from "lodash/map"
|
||||
|
||||
let removers = []
|
||||
|
||||
function setRemovers(arr) {
|
||||
removers.forEach(fn => fn()) // remove existing anchors & gutters
|
||||
removers = arr // use parent scope to persist reference
|
||||
}
|
||||
|
||||
export function placeMarkerDecorations({editor, markers, onMarkerLineUpdate}) {
|
||||
|
||||
if(typeof editor !== "object") {
|
||||
return
|
||||
}
|
||||
|
||||
let markerLines = countBy(Object.values(markers), "position")
|
||||
|
||||
let removeFns = map(markerLines, (count, line) => {
|
||||
let className = `editor-marker-${count > 8 ? "9-plus" : count}`
|
||||
let s = editor.getSession()
|
||||
let anchor = s.getDocument().createAnchor(+line, 0)
|
||||
|
||||
anchor.setPosition(+line, 0) // noClip = true
|
||||
s.addGutterDecoration(+line, className)
|
||||
anchor.on("change", function (e) {
|
||||
var oldLine = e.old.row
|
||||
var newLine = e.value.row
|
||||
|
||||
s.removeGutterDecoration(oldLine, className)
|
||||
s.addGutterDecoration(newLine, className)
|
||||
onMarkerLineUpdate([oldLine, newLine, line])
|
||||
})
|
||||
|
||||
return function () {
|
||||
// // Remove the anchor & decoration
|
||||
let currentLine = +anchor.getPosition().row
|
||||
editor.getSession().removeGutterDecoration(currentLine, className)
|
||||
anchor.detach()
|
||||
}
|
||||
})
|
||||
|
||||
setRemovers(removeFns)
|
||||
|
||||
// To manually remove them
|
||||
return () => setRemovers([])
|
||||
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import isFunction from "lodash/isFunction"
|
||||
|
||||
export default function(editor, { onGutterClick }) {
|
||||
editor.on("guttermousedown", (e) => {
|
||||
let editor = e.editor
|
||||
let line = e.getDocumentPosition().row
|
||||
let region = editor.renderer.$gutterLayer.getRegion(e)
|
||||
|
||||
e.stop()
|
||||
|
||||
if(isFunction(onGutterClick)) {
|
||||
onGutterClick({ region, line })
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
// TODO: Turn these into actions, that we can override
|
||||
import GutterClick from "./gutter-click"
|
||||
import JsonToYaml from "./json-to-yaml"
|
||||
import TabHandler from "./tab-handler"
|
||||
|
||||
const plugins = [
|
||||
{fn: GutterClick, name: "gutterClick"},
|
||||
{fn: JsonToYaml, name: "jsonToYaml"},
|
||||
{fn: TabHandler, name: "tabHandler"},
|
||||
]
|
||||
|
||||
export default function (editor, props = {}, editorPluginsToRun = [], helpers = {}) {
|
||||
plugins
|
||||
.filter(plugin => ~editorPluginsToRun.indexOf(plugin.name))
|
||||
.forEach( plugin => {
|
||||
try {
|
||||
plugin.fn(editor, props, helpers)
|
||||
} catch(e) {
|
||||
console.error(`${plugin.name || ""} plugin error:`, e)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import YAML from "js-yaml"
|
||||
|
||||
export default function(editor) {
|
||||
editor.on("paste", e => {
|
||||
const originalStr = e.text
|
||||
if (!isJSON(originalStr)) {
|
||||
return
|
||||
}
|
||||
|
||||
let yamlString
|
||||
try {
|
||||
yamlString = YAML.safeDump(YAML.safeLoad(originalStr), {
|
||||
lineWidth: -1 // don't generate line folds
|
||||
})
|
||||
} catch (e) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!confirm("Would you like to convert your JSON into YAML?")) {
|
||||
return
|
||||
}
|
||||
|
||||
// using SelectionRange instead of CursorPosition, because:
|
||||
// SR.start|end === CP when there's no selection
|
||||
// and it catches indentation edge cases when there is one
|
||||
const padding = makePadding(editor.getSelectionRange().start.column)
|
||||
|
||||
// update the pasted content
|
||||
e.text = yamlString
|
||||
.split("\n")
|
||||
.map((line, i) => i == 0 ? line : padding + line) // don't pad first line, it's already indented
|
||||
.join("\n")
|
||||
.replace(/\t/g, " ") // tabs -> spaces, just to be sure
|
||||
})
|
||||
}
|
||||
|
||||
function isJSON (str){
|
||||
// basic test: "does this look like JSON?"
|
||||
let regex = /^[ \r\n\t]*[{\[]/
|
||||
|
||||
return regex.test(str)
|
||||
|
||||
}
|
||||
|
||||
function makePadding(len) {
|
||||
let str = ""
|
||||
|
||||
while(str.length < len) {
|
||||
str += " "
|
||||
}
|
||||
|
||||
return str
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
export default function(editor) {
|
||||
// NOTE: react-ace has an onPaste prop.. we could refactor to that.
|
||||
editor.on("paste", e => {
|
||||
// replace all U+0009 tabs in pasted string with two spaces
|
||||
e.text = e.text.replace(/\t/g, " ")
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import makeEditor from "./components/editor"
|
||||
import EditorContainer from "./components/editor-container"
|
||||
import * as actions from "./actions"
|
||||
import reducers from "./reducers"
|
||||
import * as selectors from "./selectors"
|
||||
import EditorSpecPlugin from "./spec"
|
||||
|
||||
let Editor = makeEditor({
|
||||
editorPluginsToRun: ["gutterClick", "jsonToYaml", "pasteHandler"]
|
||||
})
|
||||
|
||||
export default function () {
|
||||
return [EditorSpecPlugin, {
|
||||
components: { Editor, EditorContainer },
|
||||
statePlugins: {
|
||||
editor: {
|
||||
reducers,
|
||||
actions,
|
||||
selectors
|
||||
}
|
||||
}
|
||||
}]
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import {
|
||||
JUMP_TO_LINE
|
||||
} from "./actions"
|
||||
|
||||
export default {
|
||||
[JUMP_TO_LINE]: (state, { payload } ) =>{
|
||||
return state.set("gotoLine", { line: payload })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { createSelector } from "reselect"
|
||||
import Im from "immutable"
|
||||
|
||||
const state = state => {
|
||||
return state || Im.Map()
|
||||
}
|
||||
|
||||
export const gotoLine = createSelector(
|
||||
state,
|
||||
state => {
|
||||
return state.get("gotoLine") || null
|
||||
}
|
||||
)
|
||||
@@ -0,0 +1,34 @@
|
||||
const SPEC_UPDATE_ORIGIN = "spec_update_spec_origin"
|
||||
|
||||
// wraps updateSpec to include the "origin" parameter, defaulting to "not-editor"
|
||||
// Includes a selector to get the origin, specSelectors.specOrigin
|
||||
export default function EditorSpecPlugin() {
|
||||
return {
|
||||
statePlugins: {
|
||||
spec: {
|
||||
wrapActions: {
|
||||
updateSpec: (ori, system) => (specStr, origin) => {
|
||||
system.specActions.updateSpecOrigin(origin)
|
||||
ori(specStr)
|
||||
}
|
||||
},
|
||||
reducers: {
|
||||
[SPEC_UPDATE_ORIGIN]: (state, action) => {
|
||||
return state.set("specOrigin", action.payload)
|
||||
}
|
||||
},
|
||||
selectors: {
|
||||
specOrigin: (state) => state.get("specOrigin") || "not-editor"
|
||||
},
|
||||
actions: {
|
||||
updateSpecOrigin(origin="not-editor") {
|
||||
return {
|
||||
payload: origin+"",
|
||||
type: SPEC_UPDATE_ORIGIN,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user