forked from tsai/budibase
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