mirror of
https://github.com/nocobase/nocobase
synced 2024-11-15 11:56:29 +00:00
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 moment from 'moment';
|
||||
|
||||
import { useCompile } from '../../hooks/useCompile';
|
||||
import { useCompile } from '../..';
|
||||
|
||||
const JT_VALUE_RE = /^\s*{{\s*([^{}]+)\s*}}\s*$/;
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -162,6 +162,7 @@ export default {
|
||||
'select',
|
||||
'multipleSelect',
|
||||
|
||||
'snapshot'
|
||||
// 'json'
|
||||
],
|
||||
useCurrentFields: '{{ useCurrentFields }}',
|
||||
|
@ -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 (
|
||||
|
@ -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);
|
||||
|
@ -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)}}',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -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': '快照数据',
|
||||
};
|
||||
|
@ -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() {
|
||||
|
Loading…
Reference in New Issue
Block a user