Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: jsonSchemaComponent file/files #6000

Merged
merged 1 commit into from
May 14, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 17 additions & 2 deletions src/core/curlify.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,19 @@
import win from "./window"

/**
* if duplicate key name existed from FormData entries,
* we mutated the key name by appending a hashIdx
* @param {String} k - possibly mutated key name
* @return {String} - src key name
*/
const extractKey = (k) => {
const hashIdx = "_**[]"
if (k.indexOf(hashIdx) < 0) {
return k
}
return k.split(hashIdx)[0].trim()
}

export default function curl( request ){
let curlified = []
let type = ""
Expand All @@ -21,11 +35,12 @@ export default function curl( request ){

if(type === "multipart/form-data" && request.get("method") === "POST") {
for( let [ k,v ] of request.get("body").entrySeq()) {
let extractedKey = extractKey(k)
curlified.push( "-F" )
if (v instanceof win.File) {
curlified.push( `"${k}=@${v.name}${v.type ? `;type=${v.type}` : ""}"` )
curlified.push(`"${extractedKey}=@${v.name}${v.type ? `;type=${v.type}` : ""}"` )
} else {
curlified.push( `"${k}=${v}"` )
curlified.push(`"${extractedKey}=${v}"` )
}
}
} else {
Expand Down
100 changes: 68 additions & 32 deletions src/core/json-schema-components.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ export class JsonSchemaForm extends Component {
let { schema, errors, value, onChange, getComponent, fn, disabled } = this.props
const format = schema && schema.get ? schema.get("format") : null
const type = schema && schema.get ? schema.get("type") : null

let getComponentSilently = (name) => getComponent(name, false, { failSilently: true })
let Comp = type ? format ?
getComponentSilently(`JsonSchema_${type}_${format}`) :
Expand All @@ -63,7 +64,7 @@ export class JsonSchema_string extends Component {
static propTypes = JsonSchemaPropShape
static defaultProps = JsonSchemaDefaultProps
onChange = (e) => {
const value = this.props.schema && this.props.schema["type"] === "file" ? e.target.files[0] : e.target.value
const value = this.props.schema && this.props.schema.get("type") === "file" ? e.target.files[0] : e.target.value
this.props.onChange(value, this.props.keyName)
}
onEnumChange = (val) => this.props.onChange(val)
Expand Down Expand Up @@ -92,23 +93,27 @@ export class JsonSchema_string extends Component {
const isDisabled = disabled || (schemaIn && schemaIn === "formData" && !("FormData" in window))
const Input = getComponent("Input")
if (type && type === "file") {
return (<Input type="file"
className={ errors.length ? "invalid" : ""}
title={ errors.length ? errors : ""}
onChange={ this.onChange }
disabled={isDisabled}/>)
return (
<Input type="file"
className={errors.length ? "invalid" : ""}
title={errors.length ? errors : ""}
onChange={this.onChange}
disabled={isDisabled} />
)
}
else {
return (<DebounceInput
type={ format && format === "password" ? "password" : "text" }
className={ errors.length ? "invalid" : ""}
title={ errors.length ? errors : ""}
value={value}
minLength={0}
debounceTimeout={350}
placeholder={description}
onChange={ this.onChange }
disabled={isDisabled}/>)
return (
<DebounceInput
type={format && format === "password" ? "password" : "text"}
className={errors.length ? "invalid" : ""}
title={errors.length ? errors : ""}
value={value}
minLength={0}
debounceTimeout={350}
placeholder={description}
onChange={this.onChange}
disabled={isDisabled} />
)
}
}
}
Expand Down Expand Up @@ -170,14 +175,15 @@ export class JsonSchema_array extends PureComponent {
const schemaItemsSchema = schema.getIn(["items", "schema"])
let ArrayItemsComponent
let isArrayItemText = false
let isArrayItemFile = schemaItemsType === "file" ? true : false
if (schemaItemsType && schemaItemsFormat) {
ArrayItemsComponent = getComponent(`JsonSchema_${schemaItemsType}_${schemaItemsFormat}`)
} else if (schemaItemsType === "boolean" || schemaItemsType === "array" || schemaItemsType === "object") {
ArrayItemsComponent = getComponent(`JsonSchema_${schemaItemsType}`)
}
// if ArrayItemsComponent not assigned or does not exist,
// use default schemaItemsType === "string" & JsonSchemaArrayItemText component
if (!ArrayItemsComponent) {
if (!ArrayItemsComponent && !isArrayItemFile) {
isArrayItemText = true
}

Expand Down Expand Up @@ -205,22 +211,30 @@ export class JsonSchema_array extends PureComponent {
return (
<div key={i} className="json-schema-form-item">
{
isArrayItemText ?
<JsonSchemaArrayItemText
value={item}
onChange={(val) => this.onItemChange(val, i)}
disabled={disabled}
errors={errors}
/>
: <ArrayItemsComponent {...this.props}
value={item}
onChange={(val) => this.onItemChange(val, i)}
disabled={disabled}
errors={errors}
schema={schemaItemsSchema}
getComponent={getComponent}
fn={fn}
isArrayItemFile ?
<JsonSchemaArrayItemFile
value={item}
onChange={(val)=> this.onItemChange(val, i)}
disabled={disabled}
errors={errors}
getComponent={getComponent}
/>
: isArrayItemText ?
<JsonSchemaArrayItemText
value={item}
onChange={(val) => this.onItemChange(val, i)}
disabled={disabled}
errors={errors}
/>
: <ArrayItemsComponent {...this.props}
value={item}
onChange={(val) => this.onItemChange(val, i)}
disabled={disabled}
errors={errors}
schema={schemaItemsSchema}
getComponent={getComponent}
fn={fn}
/>
}
{!disabled ? (
<Button
Expand Down Expand Up @@ -275,6 +289,28 @@ export class JsonSchemaArrayItemText extends Component {
}
}

export class JsonSchemaArrayItemFile extends Component {
static propTypes = JsonSchemaPropShape
static defaultProps = JsonSchemaDefaultProps

onFileChange = (e) => {
const value = e.target.files[0]
this.props.onChange(value, this.props.keyName)
}

render() {
let { getComponent, errors, disabled } = this.props
const Input = getComponent("Input")
const isDisabled = disabled || !("FormData" in window)

return (<Input type="file"
className={errors.length ? "invalid" : ""}
title={errors.length ? errors : ""}
onChange={this.onFileChange}
disabled={isDisabled} />)
}
}

export class JsonSchema_boolean extends Component {
static propTypes = JsonSchemaPropShape
static defaultProps = JsonSchemaDefaultProps
Expand Down
63 changes: 55 additions & 8 deletions src/core/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,17 +67,64 @@ export function arrayify (thing) {
return normalizeArray(thing)
}

export function fromJSOrdered (js) {
if(isImmutable(js))
export function fromJSOrdered(js) {
if (isImmutable(js)) {
return js // Can't do much here

if (js instanceof win.File)
}
if (js instanceof win.File) {
return js
}
if (!isObject(js)) {
return js
}
if (Array.isArray(js)) {
return Im.Seq(js).map(fromJSOrdered).toList()
}
if (js.entries) {
// handle multipart/form-data
const objWithHashedKeys = createObjWithHashedKeys(js)
return Im.OrderedMap(objWithHashedKeys).map(fromJSOrdered)
}
return Im.OrderedMap(js).map(fromJSOrdered)
}

return !isObject(js) ? js :
Array.isArray(js) ?
Im.Seq(js).map(fromJSOrdered).toList() :
Im.OrderedMap(js).map(fromJSOrdered)
/**
* Convert a FormData object into plain object
* Append a hashIdx and counter to the key name, if multiple exists
* if single, key name = <original>
* if multiple, key name = <original><hashIdx><count>
* @param {FormData} fdObj - a FormData object
* @return {Object} - a plain object
*/
export function createObjWithHashedKeys (fdObj) {
if (!fdObj.entries) {
return fdObj // not a FormData object with iterable
}
const newObj = {}
const hashIdx = "_**[]" // our internal identifier
const trackKeys = {}
for (let pair of fdObj.entries()) {
if (!newObj[pair[0]] && !(trackKeys[pair[0]] && trackKeys[pair[0]].containsMultiple)) {
newObj[pair[0]] = pair[1] // first key name: no hash required
} else {
if (!trackKeys[pair[0]]) {
// initiate tracking key for multiple
trackKeys[pair[0]] = {
containsMultiple: true,
length: 1
}
// "reassign" first pair to matching hashed format for multiple
let hashedKeyFirst = `${pair[0]}${hashIdx}${trackKeys[pair[0]].length}`
newObj[hashedKeyFirst] = newObj[pair[0]]
// remove non-hashed key of multiple
delete newObj[pair[0]] // first
}
trackKeys[pair[0]].length += 1
let hashedKeyCurrent = `${pair[0]}${hashIdx}${trackKeys[pair[0]].length}`
newObj[hashedKeyCurrent] = pair[1]
}
}
return newObj
}

export function bindToState(obj, state) {
Expand Down
20 changes: 20 additions & 0 deletions test/mocha/core/curlify.js
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,26 @@ describe("curlify", function() {
expect(curlified).toEqual("curl -X POST \"http://example.com\" -H \"content-type: multipart/form-data\" -F \"id=123\" -F \"name=Sahar\"")
})

it("should print a curl with formData that extracts array representation with hashIdx", function() {
// Note: hashIdx = `_**[]${counter}`
// Usage of hashIdx is an internal SwaggerUI method to convert formData array into something curlify can handle
const req = {
url: "http://example.com",
method: "POST",
headers: { "content-type": "multipart/form-data" },
body: {
id: "123",
"fruits[]_**[]1": "apple",
"fruits[]_**[]2": "banana",
"fruits[]_**[]3": "grape"
}
}

let curlified = curl(Im.fromJS(req))

expect(curlified).toEqual("curl -X POST \"http://example.com\" -H \"content-type: multipart/form-data\" -F \"id=123\" -F \"fruits[]=apple\" -F \"fruits[]=banana\" -F \"fruits[]=grape\"")
})

it("should print a curl with formData and file", function() {
var file = new win.File()
file.name = "file.txt"
Expand Down