Merge pull request #257 from webzard-io/auto-complete

Auto complete: part 2
This commit is contained in:
tanbowensg 2022-02-07 10:31:11 +08:00 committed by GitHub
commit 744b795753
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 78 additions and 25 deletions

View File

@ -3,10 +3,9 @@ import * as acorn from 'acorn';
import * as acornLoose from 'acorn-loose';
import { simple as simpleWalk } from 'acorn-walk';
import { flattenDeep, isArray, isObject } from 'lodash-es';
import { isExpression } from '../validator/utils';
import { ComponentId, IFieldModel, ModuleId } from './IAppModel';
const regExp = /.*{{.*}}.*/;
export class FieldModel implements IFieldModel {
isDynamic = false;
refs: Record<ComponentId | ModuleId, string[]> = {};
@ -62,13 +61,13 @@ export class FieldModel implements IFieldModel {
(isArrayValue
? []
: shouldExtendValues && isOldValueObject
? this.value
: {}) as Record<string, IFieldModel>
? this.value
: {}) as Record<string, IFieldModel>
);
} else {
this.value = value;
}
this.isDynamic = typeof value === 'string' && regExp.test(value);
this.isDynamic = isExpression(value);
this.parseReferences();
}

View File

@ -8,6 +8,8 @@ import {
Button,
} from '@chakra-ui/react';
import { isEmpty } from 'lodash-es';
import { AnyKind, UnknownKind } from '@sinclair/typebox';
import { isExpression as _isExpression } from '../../../validator/utils';
import { FieldProps, getCodeMode, getDisplayLabel } from './fields';
import { widgets } from './widgets/widgets';
import StringField from './StringField';
@ -88,7 +90,7 @@ const SchemaField: React.FC<Props> = props => {
const { schema, label, formData, onChange, registry, stateManager } = props;
const [isExpression, setIsExpression] = useState(
// FIXME: regexp copied from FieldModel.ts, is this a stable way to check expression?
() => typeof formData === 'string' && /.*{{.*}}.*/.test(formData)
() => _isExpression(formData)
);
if (isEmpty(schema)) {
@ -118,6 +120,10 @@ const SchemaField: React.FC<Props> = props => {
Component = NullField;
} else if ('anyOf' in schema || 'oneOf' in schema) {
Component = MultiSchemaField;
} else if (
[AnyKind, UnknownKind].includes((schema as unknown as { kind: symbol }).kind)
) {
Component = widgets.expression;
} else {
console.info('Found unsupported schema', schema);
}

View File

@ -9,7 +9,13 @@ const UnsupportedField: React.FC<Props> = props => {
return (
<div>
Unsupported field schema
<p>
<b>schema:</b>
</p>
<pre>{JSON.stringify(schema, null, 2)}</pre>
<p>
<b>value:</b>
</p>
<pre>{JSON.stringify(formData, null, 2)}</pre>
</div>
);

View File

@ -29,6 +29,7 @@ export function getCodeMode(schema: Schema): boolean {
case 'array':
case 'object':
case 'boolean':
case 'number':
return true;
default:
}

View File

@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react';
import React, { useEffect, useMemo, useState } from 'react';
import {
toNumber,
isString,
@ -11,6 +11,7 @@ import {
} from 'lodash-es';
import { FieldProps } from '../fields';
import { ExpressionEditor } from '../../../CodeEditor';
import { isExpression } from '../../../../validator/utils';
type Props = FieldProps;
@ -89,27 +90,52 @@ const customTreeTypeDefCreator = (dataTree: Record<string, Record<string, unknow
return { ...def };
};
const getDefaultCode = (value: unknown): { defaultCode: string; type: string } => {
const type = typeof value;
if (type === 'object' || type === 'boolean') {
value = JSON.stringify(value, null, 2);
} else {
value = String(value);
}
return {
defaultCode: value as string,
type,
};
};
const getParsedValue = (raw: string, type: string) => {
if (isExpression(raw)) {
return raw;
}
if (type === 'object' || type === 'boolean') {
try {
return JSON.parse(raw);
} catch (error) {
// TODO: handle error
return {};
}
}
if (type === 'number') {
return toNumber(raw);
}
return raw;
};
export const ExpressionWidget: React.FC<Props> = props => {
const { formData, onChange, stateManager } = props;
const [defs, setDefs] = useState<any>();
useEffect(() => {
setDefs([customTreeTypeDefCreator(stateManager.store)]);
}, [stateManager]);
const { defaultCode, type } = useMemo(() => {
return getDefaultCode(formData);
}, [formData]);
return (
<ExpressionEditor
// TODO: better serialization
defaultCode={String(formData)}
defaultCode={defaultCode}
onBlur={_v => {
// TODO: move into expression editor?
let v: string | number | boolean = _v;
if (isNumeric(v)) {
v = toNumber(v);
} else if (v === 'true') {
v = true;
} else if (v === 'false') {
v = false;
}
const v = getParsedValue(_v, type);
onChange(v);
}}
defs={defs}

View File

@ -1,4 +1,3 @@
export function isExpression (str: unknown) {
const regExp = new RegExp('.*{{.*}}.*');
return typeof str === 'string' && regExp.test(str)
export function isExpression(str: unknown) {
return typeof str === 'string' && /[\s\S]*{{[\s\S]*}}[\s\S]*/m.test(str);
}

View File

@ -22,6 +22,19 @@ describe('parseExpression function', () => {
[' input1.value '],
'!',
]);
const multiline = parseExpression(`{{
{ id: 1 }
}}`);
expect(multiline).toMatchInlineSnapshot(`
Array [
Array [
"
{ id: 1 }
",
],
]
`);
});
it('can parse $listItem expression', () => {

View File

@ -33,7 +33,7 @@ export class StateManager {
clear = () => {
this.store = reactive<Record<string, any>>({});
}
};
evalExp = (expChunk: ExpChunk, scopeObject = {}): unknown => {
if (typeof expChunk === 'string') {
@ -43,7 +43,10 @@ export class StateManager {
const evalText = expChunk.map(ex => this.evalExp(ex, scopeObject)).join('');
let evaled;
try {
evaled = new Function(`with(this) { return ${evalText} }`).call({
evaled = new Function(
// trim leading space and newline
`with(this) { return ${evalText.replace(/^\s+/g, '')} }`
).call({
...this.store,
...this.dependencies,
...scopeObject,
@ -178,8 +181,8 @@ export const parseExpression = (exp: string, parseListItem = false): ExpChunk[]
let item;
while ((item = tokens.shift())) {
if (item == '}}') return result;
result.push(item == '{{' ? build(tokens) : item);
if (item === '}}') return result;
result.push(item === '{{' ? build(tokens) : item);
}
return result;
}