mirror of https://github.com/Budibase/budibase.git
3 changed files with 13 additions and 389 deletions
@ -1,143 +0,0 @@ |
|||
import { types } from "./types"; |
|||
import { |
|||
createProps, arrayElementComponentName |
|||
} from "./createProps"; |
|||
import { isString } from "util"; |
|||
import { |
|||
includes, filter, map, keys, |
|||
flatten, flattenDeep, each, |
|||
indexOf, isUndefined |
|||
} from "lodash/fp"; |
|||
import { common } from "../../../../core/src"; |
|||
import { |
|||
isBinding |
|||
} from "../../common/binding"; |
|||
|
|||
const pipe = common.$; |
|||
|
|||
const makeError = (errors, propName, stack) => (message) => |
|||
errors.push({ |
|||
stack, |
|||
propName, |
|||
error: message |
|||
}); |
|||
|
|||
export const recursivelyValidate = (rootProps, getComponent, stack = []) => { |
|||
|
|||
if (!rootProps._component) { |
|||
const errs = []; |
|||
makeError(errs, "_component", stack)("Component is not set"); |
|||
return errs; |
|||
// this would break everything else anyway
|
|||
} |
|||
|
|||
const componentDef = getComponent( |
|||
rootProps._component); |
|||
|
|||
|
|||
const errors = validateProps( |
|||
componentDef, |
|||
rootProps, |
|||
stack, |
|||
true); |
|||
|
|||
const validateChildren = (_props, _stack) => |
|||
!_props._children |
|||
? [] |
|||
: pipe(_props._children, [ |
|||
map(child => recursivelyValidate( |
|||
child, |
|||
getComponent, |
|||
[..._stack, _props._children.indexOf(child)])) |
|||
]); |
|||
|
|||
const childErrors = validateChildren( |
|||
rootProps, stack); |
|||
|
|||
return flattenDeep([errors, ...childErrors]); |
|||
} |
|||
|
|||
const expandPropDef = propDef => |
|||
isString(propDef) |
|||
? types[propDef].defaultDefinition() |
|||
: propDef; |
|||
|
|||
|
|||
|
|||
export const validateProps = (componentDefinition, props, stack = [], isFinal = true) => { |
|||
|
|||
const errors = []; |
|||
|
|||
if (isFinal && !props._component) { |
|||
makeError(errors, "_component", stack)("Component is not set"); |
|||
return errors; |
|||
// this would break everything else anyway
|
|||
} |
|||
|
|||
const propsDefinition = componentDefinition.props; |
|||
|
|||
for (let propDefName in props) { |
|||
|
|||
const ignore = ['_component', "_children", '_layout', 'name', 'description', 'location']; |
|||
|
|||
if (ignore.includes(propDefName)) continue; |
|||
|
|||
const propDef = expandPropDef(propsDefinition[propDefName]); |
|||
|
|||
const type = types[propDef.type]; |
|||
|
|||
const error = makeError(errors, propDefName, stack); |
|||
|
|||
const propValue = props[propDefName]; |
|||
|
|||
// component declarations dont need to define al props.
|
|||
if (!isFinal && isUndefined(propValue)) continue; |
|||
|
|||
if (isFinal && propDef.required && propValue) { |
|||
error(`Property ${propDefName} is required`); |
|||
continue; |
|||
} |
|||
|
|||
if (isBinding(propValue)) { |
|||
if (propDef.type === "event") { |
|||
error(`Cannot apply binding to type ${propDef.type}`); |
|||
continue; |
|||
} |
|||
} |
|||
else if (!type.isOfType(propValue)) { |
|||
error(`Property ${propDefName} is not of type ${propDef.type}. Actual value ${propValue}`) |
|||
continue; |
|||
} |
|||
|
|||
|
|||
if (propDef.type === "options" |
|||
&& propValue |
|||
&& !isBinding(propValue) |
|||
&& !includes(propValue)(propDef.options)) { |
|||
error(`Property ${propDefName} is not one of allowed options. Acutal value is ${propValue}`); |
|||
} |
|||
|
|||
} |
|||
|
|||
return errors; |
|||
} |
|||
|
|||
export const validateComponentDefinition = (componentDefinition) => { |
|||
const { errors } = createProps(componentDefinition); |
|||
|
|||
const propDefinitions = expandPropDef(componentDefinition.props); |
|||
|
|||
pipe(propDefinitions, [ |
|||
keys, |
|||
map(k => ({ |
|||
propDef: propDefinitions[k], |
|||
propName: k |
|||
})), |
|||
filter(d => d.propDef.type === "options" |
|||
&& (!d.propDef.options || d.propDef.options.length === 0)), |
|||
each(d => makeError(errors, d.propName)(`${d.propName} does not have any options`)) |
|||
]); |
|||
|
|||
return errors; |
|||
|
|||
} |
|||
@ -1,232 +0,0 @@ |
|||
import { |
|||
validateComponentDefinition, |
|||
validateProps, |
|||
recursivelyValidate |
|||
} from "../src/userInterface/pagesParsing/validateProps"; |
|||
import { createProps } from "../src/userInterface/pagesParsing/createProps"; |
|||
import { |
|||
setBinding |
|||
} from "../src/common/binding"; |
|||
|
|||
// not that allot of this functionality is covered
|
|||
// in createDefaultProps - as validate props uses that.
|
|||
|
|||
describe("validateComponentDefinition", () => { |
|||
|
|||
|
|||
it("should return error when no options for options field", () => { |
|||
|
|||
const compDef = { |
|||
name:"some_component", |
|||
props: { |
|||
size: { |
|||
type: "options", |
|||
options: [] |
|||
} |
|||
} |
|||
}; |
|||
|
|||
const errors = validateComponentDefinition(compDef); |
|||
|
|||
expect(errors.length).toEqual(1); |
|||
expect(errors[0].propName).toBe("size"); |
|||
|
|||
}); |
|||
|
|||
it("should not return error when options field has options", () => { |
|||
|
|||
const compDef = { |
|||
name: "some_component", |
|||
props: { |
|||
size: { |
|||
type: "options", |
|||
options: ["small", "medium", "large"] |
|||
} |
|||
} |
|||
}; |
|||
|
|||
const errors = validateComponentDefinition(compDef); |
|||
|
|||
expect(errors).toEqual([]); |
|||
|
|||
}); |
|||
|
|||
}); |
|||
|
|||
const validComponentDef = { |
|||
name: "some_component", |
|||
props: { |
|||
size: { |
|||
type: "options", |
|||
options: ["small", "medium", "large"], |
|||
default:"medium" |
|||
}, |
|||
rowCount : "number" |
|||
} |
|||
}; |
|||
|
|||
const childComponentDef = { |
|||
name: "child_component", |
|||
props: { |
|||
width: "number", |
|||
units: { |
|||
type: "string", |
|||
default: "px" |
|||
} |
|||
} |
|||
}; |
|||
|
|||
const validProps = () => { |
|||
|
|||
const { props } = createProps(validComponentDef); |
|||
props._children.push( |
|||
createProps(childComponentDef)); |
|||
return props; |
|||
} |
|||
|
|||
describe("validateProps", () => { |
|||
|
|||
it("should have no errors with a big list of valid props", () => { |
|||
|
|||
const errors = validateProps(validComponentDef, validProps(), [], true); |
|||
expect(errors).toEqual([]); |
|||
|
|||
}); |
|||
|
|||
it("should return error with invalid value", () => { |
|||
|
|||
const props = validProps(); |
|||
props.rowCount = "1"; |
|||
const errors = validateProps(validComponentDef, props, [], true); |
|||
expect(errors.length).toEqual(1); |
|||
expect(errors[0].propName).toBe("rowCount"); |
|||
|
|||
}); |
|||
|
|||
it("should return error with invalid option", () => { |
|||
|
|||
const props = validProps(); |
|||
props.size = "really_small"; |
|||
const errors = validateProps(validComponentDef, props, [], true); |
|||
expect(errors.length).toEqual(1); |
|||
expect(errors[0].propName).toBe("size"); |
|||
|
|||
}); |
|||
|
|||
it("should not return error when has binding", () => { |
|||
const props = validProps(); |
|||
props._children[0].width = setBinding({path:"some_path"}); |
|||
props.size = setBinding({path:"other path", fallback:"small"}); |
|||
const errors = validateProps(validComponentDef, props, [], true); |
|||
expect(errors.length).toEqual(0); |
|||
}); |
|||
}); |
|||
|
|||
describe("recursivelyValidateProps", () => { |
|||
|
|||
const rootComponent = { |
|||
name: "rootComponent", |
|||
children: true, |
|||
props: { |
|||
width: "number" |
|||
} |
|||
}; |
|||
|
|||
const todoListComponent = { |
|||
name: "todoListComponent", |
|||
props:{ |
|||
showTitle: "bool" |
|||
} |
|||
}; |
|||
|
|||
const headerComponent = { |
|||
name: "headerComponent", |
|||
props: { |
|||
text: "string" |
|||
} |
|||
}; |
|||
|
|||
const iconComponent = { |
|||
name: "iconComponent", |
|||
props: { |
|||
iconName: "string" |
|||
} |
|||
}; |
|||
|
|||
const navItemComponent = { |
|||
name: "navItemComponent", |
|||
props: { |
|||
text: "string" |
|||
} |
|||
}; |
|||
|
|||
const getComponent = name => ({ |
|||
rootComponent, |
|||
todoListComponent, |
|||
headerComponent, |
|||
iconComponent, |
|||
navItemComponent |
|||
})[name]; |
|||
|
|||
const rootProps = () => ({ |
|||
_component: "rootComponent", |
|||
width: 100, |
|||
_children: [{ |
|||
_component: "todoListComponent", |
|||
showTitle: true, |
|||
_children : [ |
|||
{ |
|||
_component: "navItemComponent", |
|||
text: "todos" |
|||
}, |
|||
{ |
|||
_component: "headerComponent", |
|||
text: "Your todo list" |
|||
}, |
|||
{ |
|||
_component: "iconComponent", |
|||
iconName: "fa fa-list" |
|||
}, |
|||
{ |
|||
_component: "iconComponent", |
|||
iconName:"fa fa-cog" |
|||
} |
|||
] |
|||
}] |
|||
}); |
|||
|
|||
it("should return no errors for valid structure", () => { |
|||
const result = recursivelyValidate( |
|||
rootProps(), |
|||
getComponent); |
|||
|
|||
expect(result).toEqual([]); |
|||
}); |
|||
|
|||
it("should return error on root component", () => { |
|||
const root = rootProps(); |
|||
root.width = "yeeeoooo"; |
|||
const result = recursivelyValidate(root, getComponent); |
|||
expect(result.length).toBe(1); |
|||
expect(result[0].propName).toBe("width"); |
|||
}); |
|||
|
|||
it("should return error on first nested child component", () => { |
|||
const root = rootProps(); |
|||
root._children[0].showTitle = "yeeeoooo"; |
|||
const result = recursivelyValidate(root, getComponent); |
|||
expect(result.length).toBe(1); |
|||
expect(result[0].stack).toEqual([0]); |
|||
expect(result[0].propName).toBe("showTitle"); |
|||
}); |
|||
|
|||
it("should return error on second nested child component", () => { |
|||
const root = rootProps(); |
|||
root._children[0]._children[0].text = false; |
|||
const result = recursivelyValidate(root, getComponent); |
|||
expect(result.length).toBe(1); |
|||
expect(result[0].stack).toEqual([0,0]); |
|||
expect(result[0].propName).toBe("text"); |
|||
}); |
|||
|
|||
}); |
|||
Loading…
Reference in new issue