mirror of
https://github.com/smartxworks/sunmao-ui.git
synced 2025-04-18 22:00:22 +08:00
validate nested property expression
This commit is contained in:
parent
ffc4f5377d
commit
8d295f2ef8
@ -23,7 +23,7 @@ describe('ComponentModel test', () => {
|
||||
expect(button1.parentSlot).toEqual('content');
|
||||
expect(button1.prevSilbling).toBe(appModel.getComponentById('text2' as ComponentId));
|
||||
expect(button1.nextSilbing).toBe(null);
|
||||
expect(button1.properties['text'].rawValue).toEqual({ raw: 'text', format: 'plain' });
|
||||
expect(button1.rawProperties.text).toEqual({ raw: 'text', format: 'plain' });
|
||||
const apiFetch = appModel.getComponentById('apiFetch' as ComponentId)!;
|
||||
expect(apiFetch.stateKeys).toEqual(['fetch']);
|
||||
expect(apiFetch.methods[0].name).toEqual('triggerFetch');
|
||||
|
@ -23,7 +23,11 @@ describe('Validate component', () => {
|
||||
});
|
||||
it('detect using non-exist variables in expression', () => {
|
||||
const result = schemaValidator.validate(ComponentWrongPropertyExpressionSchema);
|
||||
expect(result[0].message).toBe(`Cannot find 'input' in store.`);
|
||||
expect(result[0].message).toBe(`Cannot find 'data' in store.`);
|
||||
});
|
||||
it('detect using non-exist variables in expression of array property', () => {
|
||||
const result = schemaValidator.validate(ComponentWrongPropertyExpressionSchema);
|
||||
expect(result[1].message).toBe(`Cannot find 'fetch' in store.`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -69,9 +69,16 @@ export const ComponentPropertyExpressionSchema: ComponentSchema[] = [
|
||||
|
||||
export const ComponentWrongPropertyExpressionSchema: ComponentSchema[] = [
|
||||
{
|
||||
id: 'hstack1',
|
||||
type: 'chakra_ui/v1/hstack',
|
||||
properties: { spacing: '24px', align: '{{input.value}}' },
|
||||
id: 'input1',
|
||||
type: 'chakra_ui/v1/input',
|
||||
properties: {
|
||||
variant: 'outline',
|
||||
placeholder: '{{data.value}}',
|
||||
size: 'md',
|
||||
isDisabled: false,
|
||||
isRequired: false,
|
||||
defaultValue: '',
|
||||
},
|
||||
traits: [],
|
||||
},
|
||||
{
|
||||
@ -90,21 +97,11 @@ export const ComponentWrongPropertyExpressionSchema: ComponentSchema[] = [
|
||||
handlers: [
|
||||
{
|
||||
type: 'onClick',
|
||||
componentId: 'button1',
|
||||
componentId: 'input1',
|
||||
method: {
|
||||
name: 'click',
|
||||
name: 'setInputValue',
|
||||
parameters: {
|
||||
value: '666',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'onClick',
|
||||
componentId: 'dialog1',
|
||||
method: {
|
||||
name: 'click',
|
||||
parameters: {
|
||||
value: '666',
|
||||
value: '{{fetch.data.value}}',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -1,8 +1,4 @@
|
||||
import {
|
||||
ComponentSchema,
|
||||
MethodSchema,
|
||||
RuntimeComponent,
|
||||
} from '@sunmao-ui/core';
|
||||
import { ComponentSchema, MethodSchema, RuntimeComponent } from '@sunmao-ui/core';
|
||||
import { registry } from '../setup';
|
||||
import { genComponent, genTrait } from './utils';
|
||||
import {
|
||||
@ -22,14 +18,19 @@ import {
|
||||
} from './IAppModel';
|
||||
import { TraitModel } from './TraitModel';
|
||||
import { FieldModel } from './FieldModel';
|
||||
type ComponentSpecModel = RuntimeComponent<MethodName, StyleSlotName, SlotName, EventName>
|
||||
type ComponentSpecModel = RuntimeComponent<
|
||||
MethodName,
|
||||
StyleSlotName,
|
||||
SlotName,
|
||||
EventName
|
||||
>;
|
||||
const SlotTraitType: TraitType = 'core/v1/slot' as TraitType;
|
||||
export class ComponentModel implements IComponentModel {
|
||||
private spec: ComponentSpecModel;
|
||||
|
||||
id: ComponentId;
|
||||
type: ComponentType;
|
||||
properties: Record<string, IFieldModel> = {};
|
||||
properties: IFieldModel;
|
||||
children: Record<SlotName, IComponentModel[]> = {};
|
||||
parent: IComponentModel | null = null;
|
||||
parentId: ComponentId | null = null;
|
||||
@ -53,9 +54,7 @@ export class ComponentModel implements IComponentModel {
|
||||
}
|
||||
});
|
||||
|
||||
for (const key in schema.properties) {
|
||||
this.properties[key] = new FieldModel(schema.properties[key]);
|
||||
}
|
||||
this.properties = new FieldModel(schema.properties);
|
||||
}
|
||||
|
||||
get slots() {
|
||||
@ -85,7 +84,7 @@ export class ComponentModel implements IComponentModel {
|
||||
componentMethods.push({
|
||||
name: methodName,
|
||||
parameters: this.spec.spec.methods[methodName as MethodName]!,
|
||||
})
|
||||
});
|
||||
}
|
||||
const traitMethods: MethodSchema[] = this.traits.reduce(
|
||||
(acc, t) => acc.concat(t.methods),
|
||||
@ -99,11 +98,7 @@ export class ComponentModel implements IComponentModel {
|
||||
}
|
||||
|
||||
get rawProperties() {
|
||||
const obj: Record<string, any> = {};
|
||||
for (const key in this.properties) {
|
||||
obj[key] = this.properties[key].rawValue;
|
||||
}
|
||||
return obj;
|
||||
return this.properties.rawValue;
|
||||
}
|
||||
|
||||
get prevSilbling() {
|
||||
@ -146,11 +141,7 @@ export class ComponentModel implements IComponentModel {
|
||||
}
|
||||
|
||||
updateComponentProperty(propertyName: string, value: any) {
|
||||
if (!Reflect.has(this.properties, propertyName)) {
|
||||
this.properties[propertyName] = new FieldModel(value)
|
||||
} else {
|
||||
this.properties[propertyName].update(value);
|
||||
}
|
||||
this.properties.update({ [propertyName]: value });
|
||||
this._isDirty = true;
|
||||
}
|
||||
|
||||
@ -215,7 +206,7 @@ export class ComponentModel implements IComponentModel {
|
||||
child.parentId = newId;
|
||||
const slotTrait = child.traits.find(t => t.type === SlotTraitType);
|
||||
if (slotTrait) {
|
||||
slotTrait.properties.container.update({ id: newId, slot });
|
||||
slotTrait.properties.update({ container: { id: newId, slot } });
|
||||
slotTrait._isDirty = true;
|
||||
}
|
||||
child._isDirty = true;
|
||||
@ -263,17 +254,15 @@ export class ComponentModel implements IComponentModel {
|
||||
const trait = this.traits.find(t => t.id === traitId);
|
||||
if (!trait) return;
|
||||
for (const property in properties) {
|
||||
if (trait.properties[property]) {
|
||||
trait.updateProperty(property, properties[property])
|
||||
}
|
||||
trait._isDirty = true;
|
||||
trait.updateProperty(property, properties[property]);
|
||||
}
|
||||
trait._isDirty = true;
|
||||
this._isDirty = true;
|
||||
}
|
||||
|
||||
updateSlotTrait(parent: ComponentId, slot: SlotName) {
|
||||
if (this._slotTrait) {
|
||||
this._slotTrait.properties.container.update({ id: parent, slot });
|
||||
this._slotTrait.properties.update({ container: { id: parent, slot } });
|
||||
this._slotTrait._isDirty = true;
|
||||
} else {
|
||||
this.addTrait(SlotTraitType, { container: { id: parent, slot } });
|
||||
|
@ -22,6 +22,22 @@ export class FieldModel implements IFieldModel {
|
||||
this.update(value);
|
||||
}
|
||||
|
||||
get rawValue() {
|
||||
if (isObject(this.value)) {
|
||||
if (isArray(this.value)) {
|
||||
return this.value.map((field) => field.rawValue)
|
||||
} else {
|
||||
const _thisValue = this.value as Record<string, IFieldModel>;
|
||||
const res: Record<string, any> = {};
|
||||
for (const key in _thisValue) {
|
||||
res[key] = _thisValue[key].rawValue;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
}
|
||||
return this.value;
|
||||
}
|
||||
|
||||
update(value: unknown) {
|
||||
if (isObject(value)) {
|
||||
if (!isObject(this.value)) {
|
||||
@ -44,7 +60,7 @@ export class FieldModel implements IFieldModel {
|
||||
this.parseReferences();
|
||||
}
|
||||
|
||||
getProperty(key?: string | number) {
|
||||
getProperty(key?: string | number): FieldModel | unknown {
|
||||
if (key === undefined) {
|
||||
return this.value;
|
||||
}
|
||||
@ -54,20 +70,20 @@ export class FieldModel implements IFieldModel {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
get rawValue() {
|
||||
if (isObject(this.value)) {
|
||||
if (isArray(this.value)) {
|
||||
return this.value.map((field) => field.rawValue)
|
||||
} else {
|
||||
const _thisValue = this.value as Record<string, IFieldModel>;
|
||||
const res: Record<string, any> = {};
|
||||
for (const key in _thisValue) {
|
||||
res[key] = _thisValue[key].rawValue;
|
||||
traverse(cb: (f: IFieldModel, key: string) => void) {
|
||||
function _traverse(field: FieldModel, key: string) {
|
||||
if (isObject(field.value)) {
|
||||
for (const _key in field.value) {
|
||||
const val = field.getProperty(_key)
|
||||
if (val instanceof FieldModel) {
|
||||
_traverse(val, `${key}.${_key}`)
|
||||
}
|
||||
}
|
||||
return res;
|
||||
} else {
|
||||
cb(field, key)
|
||||
}
|
||||
}
|
||||
return this.value;
|
||||
_traverse(this, '')
|
||||
}
|
||||
|
||||
private parseReferences() {
|
||||
|
@ -59,7 +59,7 @@ export interface IComponentModel {
|
||||
appModel: IAppModel;
|
||||
id: ComponentId;
|
||||
type: ComponentType;
|
||||
properties: Record<string, IFieldModel>;
|
||||
properties: IFieldModel;
|
||||
// just like properties in schema
|
||||
rawProperties: Record<string, any>;
|
||||
children: Record<SlotName, IComponentModel[]>;
|
||||
@ -102,7 +102,7 @@ export interface ITraitModel {
|
||||
parent: IComponentModel;
|
||||
type: TraitType;
|
||||
rawProperties: Record<string, any>;
|
||||
properties: Record<string, IFieldModel>;
|
||||
properties: IFieldModel;
|
||||
methods: MethodSchema[];
|
||||
stateKeys: StateKey[];
|
||||
_isDirty: boolean;
|
||||
@ -114,7 +114,8 @@ export interface IFieldModel {
|
||||
// value: any;
|
||||
isDynamic: boolean;
|
||||
update: (value: unknown) => void;
|
||||
getProperty: (key?: string) => unknown | void | Record<string, IFieldModel>;
|
||||
getProperty: (key?: string) => unknown | void | IFieldModel;
|
||||
traverse: (cb: (f: IFieldModel, key: string) => void) => void
|
||||
rawValue: any;
|
||||
// ids of used components in the expression
|
||||
refs: Array<ComponentId | ModuleId>;
|
||||
|
@ -18,7 +18,7 @@ export class TraitModel implements ITraitModel {
|
||||
private spec: RuntimeTrait;
|
||||
id: TraitId;
|
||||
type: TraitType;
|
||||
properties: Record<string, IFieldModel> = {};
|
||||
properties: IFieldModel;
|
||||
_isDirty = false;
|
||||
|
||||
constructor(trait: TraitSchema, public parent: IComponentModel) {
|
||||
@ -28,18 +28,11 @@ export class TraitModel implements ITraitModel {
|
||||
this.id = `${this.parent.id}_trait${traitIdCount++}` as TraitId;
|
||||
this.spec = registry.getTraitByType(this.type);
|
||||
|
||||
for (const key in trait.properties) {
|
||||
this.properties[key] = new FieldModel(trait.properties[key]);
|
||||
}
|
||||
this.properties;
|
||||
this.properties = new FieldModel(trait.properties);
|
||||
}
|
||||
|
||||
get rawProperties() {
|
||||
const obj: Record<string, any> = {};
|
||||
for (const key in this.properties) {
|
||||
obj[key] = this.properties[key].rawValue;
|
||||
}
|
||||
return obj;
|
||||
return this.properties.rawValue
|
||||
}
|
||||
|
||||
get methods() {
|
||||
@ -58,11 +51,7 @@ export class TraitModel implements ITraitModel {
|
||||
}
|
||||
|
||||
updateProperty(key: string, value: any) {
|
||||
if (this.properties[key]) {
|
||||
this.properties[key].update(value);
|
||||
} else {
|
||||
this.properties[key] = new FieldModel(value);
|
||||
}
|
||||
this.properties.update({[key]: value})
|
||||
this._isDirty = true;
|
||||
this.parent._isDirty = true;
|
||||
}
|
||||
|
@ -21,7 +21,7 @@ export class ModifyTraitPropertiesLeafOperation extends BaseLeafOperation<Modify
|
||||
}
|
||||
const trait = component.traits[this.context.traitIndex];
|
||||
for (const property in this.context.properties) {
|
||||
const oldValue = trait.properties[property]?.rawValue;
|
||||
const oldValue = trait.rawProperties[property];
|
||||
this.previousState[property] = oldValue;
|
||||
let newValue = this.context.properties[property];
|
||||
if (_.isFunction(newValue)) {
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { get } from 'lodash-es';
|
||||
import {
|
||||
ComponentValidatorRule,
|
||||
ComponentValidateContext,
|
||||
@ -27,15 +28,16 @@ class ComponentPropertyValidatorRule implements ComponentValidatorRule {
|
||||
if (!valid) {
|
||||
validate.errors!.forEach(error => {
|
||||
const { instancePath, params } = error;
|
||||
let key = ''
|
||||
let key = '';
|
||||
if (instancePath) {
|
||||
key = instancePath.split('/')[1];
|
||||
} else {
|
||||
key = params.missingProperty
|
||||
key = params.missingProperty;
|
||||
}
|
||||
const fieldModel = component.properties[key];
|
||||
const fieldModel = component.properties.getProperty(key);
|
||||
// if field is expression, ignore type error
|
||||
// fieldModel could be undefiend. if is undefined, still throw error.
|
||||
if (fieldModel?.isDynamic !== true) {
|
||||
if (get(fieldModel, 'isDynamic') !== true) {
|
||||
results.push({
|
||||
message: error.message || '',
|
||||
componentId: component.id,
|
||||
@ -45,8 +47,8 @@ class ComponentPropertyValidatorRule implements ComponentValidatorRule {
|
||||
});
|
||||
}
|
||||
|
||||
for (const key in component.properties) {
|
||||
const fieldModel = component.properties[key];
|
||||
// validate expression
|
||||
component.properties.traverse((fieldModel, key) => {
|
||||
fieldModel.refs.forEach((id: string) => {
|
||||
if (!componentIdSpecMap[id]) {
|
||||
results.push({
|
||||
@ -56,7 +58,7 @@ class ComponentPropertyValidatorRule implements ComponentValidatorRule {
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return results;
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ import {
|
||||
import { EventHandlerSchema } from '@sunmao-ui/runtime';
|
||||
import { isExpression } from '../utils';
|
||||
import { ComponentId, EventName } from '../../AppModel/IAppModel';
|
||||
import { get } from 'lodash-es';
|
||||
|
||||
class TraitPropertyValidatorRule implements TraitValidatorRule {
|
||||
kind: 'trait' = 'trait';
|
||||
@ -36,9 +37,10 @@ class TraitPropertyValidatorRule implements TraitValidatorRule {
|
||||
} else {
|
||||
key = params.missingProperty;
|
||||
}
|
||||
const fieldModel = component.properties[key];
|
||||
const fieldModel = trait.properties.getProperty(key);
|
||||
// if field is expression, ignore type error
|
||||
// fieldModel could be undefiend. if is undefined, still throw error.
|
||||
if (fieldModel?.isDynamic !== true) {
|
||||
if (get(fieldModel, 'isDynamic') !== true) {
|
||||
results.push({
|
||||
message: error.message || '',
|
||||
componentId: component.id,
|
||||
@ -49,18 +51,18 @@ class TraitPropertyValidatorRule implements TraitValidatorRule {
|
||||
});
|
||||
}
|
||||
|
||||
for (const key in trait.properties) {
|
||||
const fieldModel = trait.properties[key];
|
||||
fieldModel.refs.forEach(id => {
|
||||
// validate expression
|
||||
trait.properties.traverse((fieldModel, key) => {
|
||||
fieldModel.refs.forEach((id: string) => {
|
||||
if (!componentIdSpecMap[id]) {
|
||||
results.push({
|
||||
message: `Cannot find '${id}' in store.`,
|
||||
componentId: component.id,
|
||||
property: `traits/${key}`,
|
||||
property: key,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
return results;
|
||||
}
|
||||
}
|
||||
@ -127,7 +129,7 @@ class EventHandlerValidatorRule implements TraitValidatorRule {
|
||||
if (error.keyword === 'type') {
|
||||
const { instancePath } = error;
|
||||
const path = instancePath.split('/')[1];
|
||||
const value = trait.properties[path];
|
||||
const value = trait.rawProperties[path];
|
||||
|
||||
// if value is an expression, skip it
|
||||
if (isExpression(value)) {
|
||||
|
Loading…
x
Reference in New Issue
Block a user