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:
parent
15d067120d
commit
e752686c7e
@ -6,7 +6,7 @@ import { cx, css } from '@emotion/css';
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
|
|
||||||
import { useCompile } from '../../hooks/useCompile';
|
import { useCompile } from '../..';
|
||||||
|
|
||||||
const JT_VALUE_RE = /^\s*{{\s*([^{}]+)\s*}}\s*$/;
|
const JT_VALUE_RE = /^\s*{{\s*([^{}]+)\s*}}\s*$/;
|
||||||
|
|
||||||
|
@ -42,7 +42,7 @@ export function JSONInput(props) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const variable = `"{{${selected.join('.')}}}"`;
|
const variable = `{{${selected.join('.')}}}`;
|
||||||
|
|
||||||
const { textArea } = inputRef.current.resizableTextArea;
|
const { textArea } = inputRef.current.resizableTextArea;
|
||||||
const nextValue = textArea.value.slice(0, textArea.selectionStart) + variable + textArea.value.slice(textArea.selectionEnd);
|
const nextValue = textArea.value.slice(0, textArea.selectionStart) + variable + textArea.value.slice(textArea.selectionEnd);
|
||||||
|
@ -3,6 +3,7 @@ import { Input, Cascader, Tooltip, Button } from 'antd';
|
|||||||
import { useForm } from '@formily/react';
|
import { useForm } from '@formily/react';
|
||||||
import { cx, css } from '@emotion/css';
|
import { cx, css } from '@emotion/css';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useCompile } from '../..';
|
||||||
|
|
||||||
const VARIABLE_RE = /{{\s*([^{}]+)\s*}}/g;
|
const VARIABLE_RE = /{{\s*([^{}]+)\s*}}/g;
|
||||||
|
|
||||||
@ -105,9 +106,10 @@ function createVariableTagHTML(variable, keyLabelMap) {
|
|||||||
|
|
||||||
export function TextArea(props) {
|
export function TextArea(props) {
|
||||||
const { value = '', scope, onChange, multiline = true, button } = props;
|
const { value = '', scope, onChange, multiline = true, button } = props;
|
||||||
|
const compile = useCompile();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const inputRef = useRef<HTMLDivElement>(null);
|
const inputRef = useRef<HTMLDivElement>(null);
|
||||||
const options = (typeof scope === 'function' ? scope() : scope) ?? [];
|
const options = compile((typeof scope === 'function' ? scope() : scope) ?? []);
|
||||||
const form = useForm();
|
const form = useForm();
|
||||||
const keyLabelMap = useMemo(() => createOptionsValueLabelMap(options), [scope]);
|
const keyLabelMap = useMemo(() => createOptionsValueLabelMap(options), [scope]);
|
||||||
const [changed, setChanged] = useState(false);
|
const [changed, setChanged] = useState(false);
|
||||||
|
@ -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;
|
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 = {}) {
|
export function evaluate(this: Evaluator, expression: string, scope: Scope = {}) {
|
||||||
|
const context = cloneDeep(scope);
|
||||||
const exp = expression.trim().replace(/{{\s*([^{}]+)\s*}}/g, (_, v) => {
|
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"]');
|
const key = v.replace(/\.(\d+)/g, '["$1"]');
|
||||||
return ` ${typeof item === 'function' ? item() : key} `;
|
return ` ${typeof item === 'function' ? item() : key} `;
|
||||||
});
|
});
|
||||||
return this(exp, scope);
|
return this(exp, context);
|
||||||
}
|
}
|
||||||
|
@ -162,6 +162,7 @@ export default {
|
|||||||
'select',
|
'select',
|
||||||
'multipleSelect',
|
'multipleSelect',
|
||||||
|
|
||||||
|
'snapshot'
|
||||||
// 'json'
|
// 'json'
|
||||||
],
|
],
|
||||||
useCurrentFields: '{{ useCurrentFields }}',
|
useCurrentFields: '{{ useCurrentFields }}',
|
||||||
|
@ -1,17 +1,20 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useCompile, Variable } from '@nocobase/client';
|
import { useCollectionManager, useCompile, Variable } from '@nocobase/client';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export const Expression = (props) => {
|
export const Expression = (props) => {
|
||||||
const { value = '', supports, useCurrentFields, onChange } = props;
|
const { value = '', supports = [], useCurrentFields, onChange } = props;
|
||||||
const compile = useCompile();
|
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 => ({
|
const options = fields.map(field => ({
|
||||||
label: compile(field.uiSchema.title),
|
label: compile(field.uiSchema.title),
|
||||||
value: field.name
|
value: field.name,
|
||||||
|
children: interfaces[field.interface].usePathOptions?.(field)
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -7,7 +7,7 @@ import {
|
|||||||
SchemaInitializerProvider,
|
SchemaInitializerProvider,
|
||||||
} from '@nocobase/client';
|
} from '@nocobase/client';
|
||||||
import React, { useContext, useEffect } from 'react';
|
import React, { useContext, useEffect } from 'react';
|
||||||
import { useSnapshotInterface } from './interface';
|
import { snapshot } from './interface';
|
||||||
import { SnapshotRecordPicker } from './SnapshotRecordPicker';
|
import { SnapshotRecordPicker } from './SnapshotRecordPicker';
|
||||||
import { SnapshotBlockInitializers } from './SnapshotBlock/SnapshotBlockInitializers/SnapshotBlockInitializers';
|
import { SnapshotBlockInitializers } from './SnapshotBlock/SnapshotBlockInitializers/SnapshotBlockInitializers';
|
||||||
import { SnapshotBlockInitializersDetailItem } from './SnapshotBlock/SnapshotBlockInitializers/SnapshotBlockInitializersDetailItem';
|
import { SnapshotBlockInitializersDetailItem } from './SnapshotBlock/SnapshotBlockInitializers/SnapshotBlockInitializersDetailItem';
|
||||||
@ -17,7 +17,6 @@ import { SnapshotOwnerCollectionFieldsSelect } from './components/SnapshotOwnerC
|
|||||||
|
|
||||||
export default React.memo((props) => {
|
export default React.memo((props) => {
|
||||||
const initializers = useContext(SchemaInitializerContext);
|
const initializers = useContext(SchemaInitializerContext);
|
||||||
const snapshot = useSnapshotInterface();
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
registerField(snapshot.group, snapshot.name as string, snapshot);
|
registerField(snapshot.group, snapshot.name as string, snapshot);
|
||||||
|
@ -1,9 +1,8 @@
|
|||||||
import type { Field } from '@formily/core';
|
import type { Field } from '@formily/core';
|
||||||
import { ISchema } from '@formily/react';
|
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 { cloneDeep } from 'lodash';
|
||||||
import { useMemo } from 'react';
|
import { NAMESPACE } from './locale';
|
||||||
import { useSnapshotTranslation } from './locale';
|
|
||||||
|
|
||||||
const { defaultProps } = interfacesProperties;
|
const { defaultProps } = interfacesProperties;
|
||||||
|
|
||||||
@ -27,114 +26,151 @@ const onTargetFieldChange = (field: Field) => {
|
|||||||
!targetField.getState().disabled && targetField.setValue([]);
|
!targetField.getState().disabled && targetField.setValue([]);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useSnapshotInterface = () => {
|
function makeFieldsPathOptions(fields, appends = []) {
|
||||||
const { t } = useSnapshotTranslation();
|
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 = {
|
const recordPickerViewer = {
|
||||||
type: 'void',
|
type: 'void',
|
||||||
title: t('View record'),
|
title: `{{t('View record')}}`,
|
||||||
'x-component': 'RecordPicker.Viewer',
|
'x-component': 'RecordPicker.Viewer',
|
||||||
'x-component-props': {
|
'x-component-props': {
|
||||||
className: 'nb-action-popup',
|
className: 'nb-action-popup',
|
||||||
},
|
},
|
||||||
properties: {
|
properties: {
|
||||||
tabs: {
|
tabs: {
|
||||||
type: 'void',
|
type: 'void',
|
||||||
'x-component': 'Tabs',
|
'x-component': 'Tabs',
|
||||||
'x-component-props': {},
|
'x-component-props': {},
|
||||||
// 'x-initializer': 'TabPaneInitializers',
|
// 'x-initializer': 'TabPaneInitializers',
|
||||||
properties: {
|
properties: {
|
||||||
tab1: {
|
tab1: {
|
||||||
type: 'void',
|
type: 'void',
|
||||||
title: t('Detail'),
|
title: `{{t('Detail')}}`,
|
||||||
'x-component': 'Tabs.TabPane',
|
'x-component': 'Tabs.TabPane',
|
||||||
'x-designer': 'Tabs.Designer',
|
'x-designer': 'Tabs.Designer',
|
||||||
'x-component-props': {},
|
'x-component-props': {},
|
||||||
properties: {
|
properties: {
|
||||||
grid: {
|
grid: {
|
||||||
type: 'void',
|
type: 'void',
|
||||||
'x-component': 'Grid',
|
'x-component': 'Grid',
|
||||||
'x-initializer': 'SnapshotBlockInitializers',
|
'x-initializer': 'SnapshotBlockInitializers',
|
||||||
properties: {},
|
properties: {},
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
},
|
||||||
|
};
|
||||||
const snapshot: IField = {
|
|
||||||
name: 'snapshot',
|
export const snapshot: IField = {
|
||||||
type: 'object',
|
name: 'snapshot',
|
||||||
group: 'advanced',
|
type: 'object',
|
||||||
title: t('Snapshot'),
|
group: 'advanced',
|
||||||
description: t('Snapshot to description'),
|
title: `{{t('Snapshot', {ns: '${NAMESPACE}'})}}`,
|
||||||
default: {
|
description: `{{t('Snapshot to description', {ns: '${NAMESPACE}'})}}`,
|
||||||
type: 'snapshot',
|
default: {
|
||||||
// name,
|
type: 'snapshot',
|
||||||
uiSchema: {
|
// name,
|
||||||
// title,
|
uiSchema: {
|
||||||
'x-component': 'SnapshotRecordPicker',
|
// title,
|
||||||
'x-component-props': {
|
'x-component': 'SnapshotRecordPicker',
|
||||||
multiple: true,
|
'x-component-props': {
|
||||||
fieldNames: {
|
multiple: true,
|
||||||
label: 'id',
|
fieldNames: {
|
||||||
value: 'id',
|
label: 'id',
|
||||||
},
|
value: 'id',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
schemaInitialize(schema: ISchema, { field, readPretty, action, block }) {
|
},
|
||||||
schema['properties'] = {
|
schemaInitialize(schema: ISchema, { field, readPretty, action, block }) {
|
||||||
viewer: cloneDeep(recordPickerViewer),
|
schema['properties'] = {
|
||||||
};
|
viewer: cloneDeep(recordPickerViewer),
|
||||||
},
|
};
|
||||||
initialize: (values: any) => {},
|
},
|
||||||
properties: {
|
initialize: (values: any) => {},
|
||||||
...defaultProps,
|
usePathOptions(field) {
|
||||||
[TARGET_FIELD]: {
|
const { appends = [], targetCollection } = field;
|
||||||
type: 'string',
|
const { getCollection } = useCollectionManager();
|
||||||
title: t('Association field'),
|
const { fields } = getCollection(targetCollection);
|
||||||
required: true,
|
|
||||||
'x-decorator': 'FormItem',
|
const result = makeFieldsPathOptions(fields, appends);
|
||||||
'x-component': 'SnapshotOwnerCollectionFieldsSelect',
|
|
||||||
'x-disabled': '{{ !createOnly || isOverride }}',
|
return [
|
||||||
'x-reactions': [
|
{
|
||||||
{
|
label: `{{t('Snapshot data', { ns: '${NAMESPACE}' })}}`,
|
||||||
target: APPENDS,
|
value: 'data',
|
||||||
when: '{{$self.value != undefined}}',
|
children: result,
|
||||||
fulfill: {
|
}
|
||||||
state: {
|
];
|
||||||
visible: true,
|
},
|
||||||
},
|
properties: {
|
||||||
},
|
...defaultProps,
|
||||||
otherwise: {
|
[TARGET_FIELD]: {
|
||||||
state: {
|
type: 'string',
|
||||||
visible: false,
|
title: `{{t('Association field', {ns: '${NAMESPACE}'})}}`,
|
||||||
},
|
required: true,
|
||||||
},
|
'x-decorator': 'FormItem',
|
||||||
},
|
'x-component': 'SnapshotOwnerCollectionFieldsSelect',
|
||||||
],
|
'x-disabled': '{{ !createOnly || isOverride }}',
|
||||||
},
|
'x-reactions': [
|
||||||
[APPENDS]: {
|
{
|
||||||
type: 'string',
|
target: APPENDS,
|
||||||
title: t('Deep copy fields'),
|
when: '{{$self.value != undefined}}',
|
||||||
description: t('When a record is created, relational data is backed up in a snapshot'),
|
fulfill: {
|
||||||
'x-decorator': 'FormItem',
|
state: {
|
||||||
'x-component': 'AppendsTreeSelect',
|
visible: true,
|
||||||
'x-reactions': [
|
},
|
||||||
{
|
},
|
||||||
dependencies: [TARGET_FIELD],
|
otherwise: {
|
||||||
when: '{{$deps[0]}}',
|
state: {
|
||||||
fulfill: {
|
visible: false,
|
||||||
run: '{{$self.setValue($self.value)}}',
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
[APPENDS]: {
|
||||||
};
|
type: 'string',
|
||||||
|
title: `{{t('Deep copy fields', {ns: '${NAMESPACE}'})}}`,
|
||||||
return useMemo<IField>(() => snapshot, [t]);
|
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)}}',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
@ -9,4 +9,5 @@ export default {
|
|||||||
'Deep copy fields': '深复制的字段',
|
'Deep copy fields': '深复制的字段',
|
||||||
'Please select': '请选择',
|
'Please select': '请选择',
|
||||||
'When a record is created, association data is backed up in a snapshot': '创建记录时,关系数据会备份到快照里',
|
'When a record is created, association data is backed up in a snapshot': '创建记录时,关系数据会备份到快照里',
|
||||||
|
'Snapshot data': '快照数据',
|
||||||
};
|
};
|
||||||
|
@ -31,18 +31,12 @@ export class SnapshotField extends Field {
|
|||||||
data = data.toJSON();
|
data = data.toJSON();
|
||||||
}
|
}
|
||||||
|
|
||||||
await model.update(
|
await model.update({
|
||||||
{
|
[name]: {
|
||||||
[name]: {
|
collectionName,
|
||||||
collectionName,
|
data,
|
||||||
data,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
}, { transaction });
|
||||||
transaction,
|
|
||||||
hooks: false,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
bind() {
|
bind() {
|
||||||
|
Loading…
Reference in New Issue
Block a user