feat(plugin-formula): calculation with snapshot field (#1498)

* feat(plugin-formula): calculation with snapshot field

* fix(plugin-snapshot): fix appends calcualtion

* fix(plugin-snapshot): fix appends calcualtion

* fix(plugin-snapshot): fix appends calcualtion
This commit is contained in:
Junyi 2023-02-28 17:55:58 +08:00 committed by GitHub
parent 15d067120d
commit e752686c7e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 181 additions and 130 deletions

View File

@ -6,7 +6,7 @@ import { cx, css } from '@emotion/css';
import { useTranslation } from 'react-i18next';
import moment from 'moment';
import { useCompile } from '../../hooks/useCompile';
import { useCompile } from '../..';
const JT_VALUE_RE = /^\s*{{\s*([^{}]+)\s*}}\s*$/;

View File

@ -42,7 +42,7 @@ export function JSONInput(props) {
return;
}
const variable = `"{{${selected.join('.')}}}"`;
const variable = `{{${selected.join('.')}}}`;
const { textArea } = inputRef.current.resizableTextArea;
const nextValue = textArea.value.slice(0, textArea.selectionStart) + variable + textArea.value.slice(textArea.selectionEnd);

View File

@ -3,6 +3,7 @@ import { Input, Cascader, Tooltip, Button } from 'antd';
import { useForm } from '@formily/react';
import { cx, css } from '@emotion/css';
import { useTranslation } from 'react-i18next';
import { useCompile } from '../..';
const VARIABLE_RE = /{{\s*([^{}]+)\s*}}/g;
@ -105,9 +106,10 @@ function createVariableTagHTML(variable, keyLabelMap) {
export function TextArea(props) {
const { value = '', scope, onChange, multiline = true, button } = props;
const compile = useCompile();
const { t } = useTranslation();
const inputRef = useRef<HTMLDivElement>(null);
const options = (typeof scope === 'function' ? scope() : scope) ?? [];
const options = compile((typeof scope === 'function' ? scope() : scope) ?? []);
const form = useForm();
const keyLabelMap = useMemo(() => createOptionsValueLabelMap(options), [scope]);
const [changed, setChanged] = useState(false);

View File

@ -1,4 +1,4 @@
import { get } from "lodash";
import { get, cloneDeep } from "lodash";
@ -6,11 +6,26 @@ export type Scope = { [key: string]: any };
export type Evaluator = (expression: string, scope?: Scope) => any;
function appendArrayColumn(scope, key) {
const paths = key.split('.');
let data = scope;
for (let p = 0; p < paths.length; p++) {
const path = paths[p];
const isIndex = path.match(/^\d+$/);
if (Array.isArray(data) && !isIndex && !data[path]) {
data[path] = data.map(item => item[path]);
}
data = data[path];
}
}
export function evaluate(this: Evaluator, expression: string, scope: Scope = {}) {
const context = cloneDeep(scope);
const exp = expression.trim().replace(/{{\s*([^{}]+)\s*}}/g, (_, v) => {
const item = get(scope, v);
appendArrayColumn(context, v);
const item = get(context, v);
const key = v.replace(/\.(\d+)/g, '["$1"]');
return ` ${typeof item === 'function' ? item() : key} `;
});
return this(exp, scope);
return this(exp, context);
}

View File

@ -162,6 +162,7 @@ export default {
'select',
'multipleSelect',
'snapshot'
// 'json'
],
useCurrentFields: '{{ useCurrentFields }}',

View File

@ -1,17 +1,20 @@
import React from 'react';
import { useCompile, Variable } from '@nocobase/client';
import { useCollectionManager, useCompile, Variable } from '@nocobase/client';
export const Expression = (props) => {
const { value = '', supports, useCurrentFields, onChange } = props;
const { value = '', supports = [], useCurrentFields, onChange } = props;
const compile = useCompile();
const { interfaces } = useCollectionManager();
const fields = useCurrentFields().filter(field => supports.includes(field.interface));
const fields = (useCurrentFields?.() ?? [])
.filter(field => supports.includes(field.interface));
const options = fields.map(field => ({
label: compile(field.uiSchema.title),
value: field.name
value: field.name,
children: interfaces[field.interface].usePathOptions?.(field)
}));
return (

View File

@ -7,7 +7,7 @@ import {
SchemaInitializerProvider,
} from '@nocobase/client';
import React, { useContext, useEffect } from 'react';
import { useSnapshotInterface } from './interface';
import { snapshot } from './interface';
import { SnapshotRecordPicker } from './SnapshotRecordPicker';
import { SnapshotBlockInitializers } from './SnapshotBlock/SnapshotBlockInitializers/SnapshotBlockInitializers';
import { SnapshotBlockInitializersDetailItem } from './SnapshotBlock/SnapshotBlockInitializers/SnapshotBlockInitializersDetailItem';
@ -17,7 +17,6 @@ import { SnapshotOwnerCollectionFieldsSelect } from './components/SnapshotOwnerC
export default React.memo((props) => {
const initializers = useContext(SchemaInitializerContext);
const snapshot = useSnapshotInterface();
useEffect(() => {
registerField(snapshot.group, snapshot.name as string, snapshot);

View File

@ -1,9 +1,8 @@
import type { Field } from '@formily/core';
import { ISchema } from '@formily/react';
import { IField, interfacesProperties, useRecord } from '@nocobase/client';
import { IField, interfacesProperties, useCollectionManager, useRecord } from '@nocobase/client';
import { cloneDeep } from 'lodash';
import { useMemo } from 'react';
import { useSnapshotTranslation } from './locale';
import { NAMESPACE } from './locale';
const { defaultProps } = interfacesProperties;
@ -27,114 +26,151 @@ const onTargetFieldChange = (field: Field) => {
!targetField.getState().disabled && targetField.setValue([]);
};
export const useSnapshotInterface = () => {
const { t } = useSnapshotTranslation();
function makeFieldsPathOptions(fields, appends = []) {
const { getCollection } = useCollectionManager();
const options = [];
fields.forEach(field => {
if (['belongsTo', 'hasOne', 'hasMany', 'belongsToMany'].includes(field.type)) {
const currentAppends = appends.filter(key => `${key}.`.startsWith(`${field.name}.`))
if (currentAppends.length) {
const nextCollection = getCollection(field.target);
const nextAppends = currentAppends
.filter(key => key !== field.name)
.map(key => key.replace(`${field.name}.`, ''))
.filter(key => key);
options.push({
label: field.uiSchema?.title ?? field.name,
value: field.name,
children: makeFieldsPathOptions(nextCollection.fields, nextAppends),
});
}
} else {
options.push({
label: field.uiSchema?.title ?? field.name,
value: field.name,
});
}
});
return options;
}
const recordPickerViewer = {
type: 'void',
title: t('View record'),
'x-component': 'RecordPicker.Viewer',
'x-component-props': {
className: 'nb-action-popup',
},
properties: {
tabs: {
type: 'void',
'x-component': 'Tabs',
'x-component-props': {},
// 'x-initializer': 'TabPaneInitializers',
properties: {
tab1: {
type: 'void',
title: t('Detail'),
'x-component': 'Tabs.TabPane',
'x-designer': 'Tabs.Designer',
'x-component-props': {},
properties: {
grid: {
type: 'void',
'x-component': 'Grid',
'x-initializer': 'SnapshotBlockInitializers',
properties: {},
},
const recordPickerViewer = {
type: 'void',
title: `{{t('View record')}}`,
'x-component': 'RecordPicker.Viewer',
'x-component-props': {
className: 'nb-action-popup',
},
properties: {
tabs: {
type: 'void',
'x-component': 'Tabs',
'x-component-props': {},
// 'x-initializer': 'TabPaneInitializers',
properties: {
tab1: {
type: 'void',
title: `{{t('Detail')}}`,
'x-component': 'Tabs.TabPane',
'x-designer': 'Tabs.Designer',
'x-component-props': {},
properties: {
grid: {
type: 'void',
'x-component': 'Grid',
'x-initializer': 'SnapshotBlockInitializers',
properties: {},
},
},
},
},
},
};
const snapshot: IField = {
name: 'snapshot',
type: 'object',
group: 'advanced',
title: t('Snapshot'),
description: t('Snapshot to description'),
default: {
type: 'snapshot',
// name,
uiSchema: {
// title,
'x-component': 'SnapshotRecordPicker',
'x-component-props': {
multiple: true,
fieldNames: {
label: 'id',
value: 'id',
},
},
},
},
schemaInitialize(schema: ISchema, { field, readPretty, action, block }) {
schema['properties'] = {
viewer: cloneDeep(recordPickerViewer),
};
},
initialize: (values: any) => {},
properties: {
...defaultProps,
[TARGET_FIELD]: {
type: 'string',
title: t('Association field'),
required: true,
'x-decorator': 'FormItem',
'x-component': 'SnapshotOwnerCollectionFieldsSelect',
'x-disabled': '{{ !createOnly || isOverride }}',
'x-reactions': [
{
target: APPENDS,
when: '{{$self.value != undefined}}',
fulfill: {
state: {
visible: true,
},
},
otherwise: {
state: {
visible: false,
},
},
},
],
},
[APPENDS]: {
type: 'string',
title: t('Deep copy fields'),
description: t('When a record is created, relational data is backed up in a snapshot'),
'x-decorator': 'FormItem',
'x-component': 'AppendsTreeSelect',
'x-reactions': [
{
dependencies: [TARGET_FIELD],
when: '{{$deps[0]}}',
fulfill: {
run: '{{$self.setValue($self.value)}}',
},
},
],
},
},
};
return useMemo<IField>(() => snapshot, [t]);
},
};
export const snapshot: IField = {
name: 'snapshot',
type: 'object',
group: 'advanced',
title: `{{t('Snapshot', {ns: '${NAMESPACE}'})}}`,
description: `{{t('Snapshot to description', {ns: '${NAMESPACE}'})}}`,
default: {
type: 'snapshot',
// name,
uiSchema: {
// title,
'x-component': 'SnapshotRecordPicker',
'x-component-props': {
multiple: true,
fieldNames: {
label: 'id',
value: 'id',
},
},
},
},
schemaInitialize(schema: ISchema, { field, readPretty, action, block }) {
schema['properties'] = {
viewer: cloneDeep(recordPickerViewer),
};
},
initialize: (values: any) => {},
usePathOptions(field) {
const { appends = [], targetCollection } = field;
const { getCollection } = useCollectionManager();
const { fields } = getCollection(targetCollection);
const result = makeFieldsPathOptions(fields, appends);
return [
{
label: `{{t('Snapshot data', { ns: '${NAMESPACE}' })}}`,
value: 'data',
children: result,
}
];
},
properties: {
...defaultProps,
[TARGET_FIELD]: {
type: 'string',
title: `{{t('Association field', {ns: '${NAMESPACE}'})}}`,
required: true,
'x-decorator': 'FormItem',
'x-component': 'SnapshotOwnerCollectionFieldsSelect',
'x-disabled': '{{ !createOnly || isOverride }}',
'x-reactions': [
{
target: APPENDS,
when: '{{$self.value != undefined}}',
fulfill: {
state: {
visible: true,
},
},
otherwise: {
state: {
visible: false,
},
},
},
],
},
[APPENDS]: {
type: 'string',
title: `{{t('Deep copy fields', {ns: '${NAMESPACE}'})}}`,
description: `{{t('When a record is created, relational data is backed up in a snapshot', {ns: '${NAMESPACE}'})}}`,
'x-decorator': 'FormItem',
'x-component': 'AppendsTreeSelect',
'x-reactions': [
{
dependencies: [TARGET_FIELD],
when: '{{$deps[0]}}',
fulfill: {
run: '{{$self.setValue($self.value)}}',
},
},
],
},
},
};

View File

@ -9,4 +9,5 @@ export default {
'Deep copy fields': '深复制的字段',
'Please select': '请选择',
'When a record is created, association data is backed up in a snapshot': '创建记录时,关系数据会备份到快照里',
'Snapshot data': '快照数据',
};

View File

@ -31,18 +31,12 @@ export class SnapshotField extends Field {
data = data.toJSON();
}
await model.update(
{
[name]: {
collectionName,
data,
},
await model.update({
[name]: {
collectionName,
data,
},
{
transaction,
hooks: false,
},
);
}, { transaction });
};
bind() {