Feat(plugin workflow): add changed fields to model trigger config (#332)

* feat(plugin-workflow): add changed fields to model trigger config

* fix(plugin-workflow): avoid system fields to input
This commit is contained in:
Junyi 2022-04-28 10:18:44 +08:00 committed by GitHub
parent 27399f4e5e
commit f5cc368cef
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 88 additions and 53 deletions

View File

@ -395,12 +395,7 @@ export const CollectionFieldset = observer(({ value, onChange, useProps }: any)
} }
`}> `}>
{fields.length {fields.length
? fields ? fields.map(field => {
.filter(field => (
!field.hidden
&& (field.uiSchema ? !field.uiSchema['x-read-pretty'] : true)
))
.map(field => {
const operand = typeof value[field.name] === 'string' const operand = typeof value[field.name] === 'string'
? parseStringValue(value[field.name], VTypes) ? parseStringValue(value[field.name], VTypes)
: { type: 'constant', value: value[field.name] }; : { type: 'constant', value: value[field.name] };

View File

@ -12,8 +12,8 @@ export default {
group: 'model', group: 'model',
fieldset: { fieldset: {
'config.collection': { 'config.collection': {
name: 'config.collection',
...collection, ...collection,
name: 'config.collection'
}, },
// multiple: { // multiple: {
// type: 'boolean', // type: 'boolean',

View File

@ -1,13 +1,13 @@
import { CloseOutlined, DeleteOutlined } from '@ant-design/icons'; import { CloseOutlined, DeleteOutlined } from '@ant-design/icons';
import { css, cx } from '@emotion/css'; import { css, cx } from '@emotion/css';
import { Field } from '@formily/core'; import { ISchema, useForm } from '@formily/react';
import { ISchema, RecursionField, useField, useForm } from '@formily/react';
import { Registry } from '@nocobase/utils'; import { Registry } from '@nocobase/utils';
import { Button, Modal, Tag } from 'antd'; import { Button, Modal, Tag } from 'antd';
import React, { useContext } from 'react'; import React, { useContext } from 'react';
import { SchemaComponent, useActionContext, useAPIClient, useCollection, useRequest, useResourceActionContext } from '../..'; import { SchemaComponent, useActionContext, useAPIClient, useCollection, useRequest, useResourceActionContext } from '../..';
import { nodeBlockClass, nodeCardClass, nodeClass, nodeHeaderClass, nodeTitleClass } from '../style'; import { nodeBlockClass, nodeCardClass, nodeClass, nodeHeaderClass, nodeTitleClass } from '../style';
import { AddButton, useFlowContext } from '../WorkflowCanvas'; import { AddButton, useFlowContext } from '../WorkflowCanvas';
import calculation from './calculation'; import calculation from './calculation';
import condition from './condition'; import condition from './condition';
import create from './create'; import create from './create';
@ -18,10 +18,6 @@ import update from './update';
export interface Instruction { export interface Instruction {
title: string; title: string;
type: string; type: string;
@ -148,14 +144,6 @@ export function RemoveButton() {
); );
} }
function Fieldset({ schema }) {
const field = useField<Field>();
return (
<RecursionField basePath={field.address} schema={schema} />
);
}
export function NodeDefaultView(props) { export function NodeDefaultView(props) {
const { data, children } = props; const { data, children } = props;
const instruction = instructions.get(data.type); const instruction = instructions.get(data.type);
@ -174,10 +162,7 @@ export function NodeDefaultView(props) {
</div> </div>
<SchemaComponent <SchemaComponent
scope={instruction.scope} scope={instruction.scope}
components={{ components={instruction.components}
...instruction.components,
Fieldset
}}
schema={{ schema={{
type: 'void', type: 'void',
properties: { properties: {
@ -197,9 +182,9 @@ export function NodeDefaultView(props) {
'x-decorator': 'Form', 'x-decorator': 'Form',
'x-decorator-props': { 'x-decorator-props': {
useValues(options) { useValues(options) {
const data = useNodeContext(); const d = useNodeContext();
return useRequest(() => { return useRequest(() => {
return Promise.resolve({ data }); return Promise.resolve({ data: d });
}, options); }, options);
} }
}, },

View File

@ -29,7 +29,11 @@ export const values = {
useProps() { useProps() {
const { getCollectionFields } = useCollectionManager(); const { getCollectionFields } = useCollectionManager();
const { values: form } = useForm(); const { values: form } = useForm();
const fields = getCollectionFields(form?.config?.collection); const fields = getCollectionFields(form?.config?.collection)
.filter(field => (
!field.hidden
&& (field.uiSchema ? !field.uiSchema['x-read-pretty'] : false)
));
return { fields }; return { fields };
} }
} }
@ -50,7 +54,7 @@ export const filter = {
'x-component-props': { 'x-component-props': {
useProps() { useProps() {
const { values } = useForm(); const { values } = useForm();
const options = useCollectionFilterOptions(values.config.collection); const options = useCollectionFilterOptions(values.config?.collection);
return { return {
options, options,
className: css` className: css`

View File

@ -3,7 +3,7 @@ import { ISchema, useForm } from "@formily/react";
import { cx } from "@emotion/css"; import { cx } from "@emotion/css";
import { Registry } from "@nocobase/utils"; import { Registry } from "@nocobase/utils";
import { SchemaComponent, useActionContext, useAPIClient, useRecord, useResourceActionContext } from '../../'; import { SchemaComponent, useActionContext, useAPIClient, useRecord, useRequest, useResourceActionContext } from '../../';
import model from './model'; import model from './model';
import { nodeCardClass } from "../style"; import { nodeCardClass } from "../style";
@ -19,12 +19,7 @@ function useUpdateConfigAction() {
await form.submit(); await form.submit();
await api.resource('workflows', record.id).update({ await api.resource('workflows', record.id).update({
filterByTk: record.id, filterByTk: record.id,
values: { values: form.values
config: {
...record.config,
...form.values
}
},
}); });
ctx.setVisible(false); ctx.setVisible(false);
refresh(); refresh();
@ -72,10 +67,16 @@ export const TriggerConfig = () => {
'x-component': 'Action.Drawer', 'x-component': 'Action.Drawer',
'x-decorator': 'Form', 'x-decorator': 'Form',
'x-decorator-props': { 'x-decorator-props': {
initialValue: config initialValue: { config }
}, },
properties: { properties: {
...fieldset, config: {
type: 'void',
name: 'config',
'x-component': 'fieldset',
'x-component-props': {},
properties: fieldset
},
actions: { actions: {
type: 'void', type: 'void',
'x-component': 'Action.Drawer.Footer', 'x-component': 'Action.Drawer.Footer',

View File

@ -1,5 +1,7 @@
import React from 'react'; import React from 'react';
import { Select } from 'antd'; import { Select } from 'antd';
import { action } from '@formily/reactive';
import { useForm } from '@formily/react';
import { useCollectionDataSource, useCollectionManager } from '../../collection-manager'; import { useCollectionDataSource, useCollectionManager } from '../../collection-manager';
import { useCompile } from '../../schema-component'; import { useCompile } from '../../schema-component';
@ -8,15 +10,36 @@ import { useFlowContext } from '../WorkflowCanvas';
import { BaseTypeSet } from '../calculators'; import { BaseTypeSet } from '../calculators';
import { collection, filter } from '../schemas/collection'; import { collection, filter } from '../schemas/collection';
function useCollectionFieldsDataSource() {
const compile = useCompile();
const { getCollectionFields } = useCollectionManager();
const { values } = useForm();
const fields = getCollectionFields(values?.config?.collection);
return (field: any) => {
action.bound((data: any) => {
field.dataSource = data
.filter(field => (
!field.hidden
&& (field.uiSchema ? !field.uiSchema['x-read-pretty'] : true)
))
.map(field => ({
label: compile(field.uiSchema?.title),
value: field.name
}));
})(fields);
};
}
export default { export default {
title: '数据表事件', title: '数据表事件',
type: 'model', type: 'model',
fieldset: { fieldset: {
collection, 'config.collection': collection,
mode: { 'config.mode': {
type: 'number', type: 'number',
title: '触发时机', title: '触发时机',
name: 'mode', name: 'config.mode',
'x-decorator': 'FormItem', 'x-decorator': 'FormItem',
'x-component': 'Select', 'x-component': 'Select',
'x-component-props': { 'x-component-props': {
@ -28,13 +51,29 @@ export default {
] ]
} }
}, },
filter: { 'config.changed': {
type: 'array',
name: 'changed',
title: '发生变动的字段',
description: '只有被选中的某个字段发生变动时才会触发。如果不选择,则表示任何字段变动时都会触发。新增或删除数据时,任意字段都被认为发生变动。',
'x-decorator': 'FormItem',
'x-component': 'Select',
'x-component-props': {
mode: 'multiple',
},
'x-reactions': [
'{{useCollectionFieldsDataSource()}}'
]
},
'config.condition': {
...filter, ...filter,
name: 'config.condition',
title: '满足条件' title: '满足条件'
} }
}, },
scope: { scope: {
useCollectionDataSource useCollectionDataSource,
useCollectionFieldsDataSource
}, },
getter({ type, options, onChange }) { getter({ type, options, onChange }) {
const compile = useCompile(); const compile = useCompile();

View File

@ -4,7 +4,7 @@ export interface ModelChangeTriggerConfig {
collection: string; collection: string;
mode: number; mode: number;
// TODO: ICondition // TODO: ICondition
filter: any; condition: any;
} }
const MODE_BITMAP = { const MODE_BITMAP = {
@ -18,26 +18,37 @@ MODE_BITMAP_EVENTS.set(MODE_BITMAP.CREATE, 'afterCreate');
MODE_BITMAP_EVENTS.set(MODE_BITMAP.UPDATE, 'afterUpdate'); MODE_BITMAP_EVENTS.set(MODE_BITMAP.UPDATE, 'afterUpdate');
MODE_BITMAP_EVENTS.set(MODE_BITMAP.DESTROY, 'afterDestroy'); MODE_BITMAP_EVENTS.set(MODE_BITMAP.DESTROY, 'afterDestroy');
// async function, should return promise
function bindHandler(this: WorkflowModel, callback: Function) {
const { condition, changed } = this.config;
return (data: any, options) => {
// NOTE: if no configured fields changed, do not trigger
if (changed && changed.length && changed.every(name => !data.changed(name))) {
return;
}
// NOTE: if no configured condition match, do not trigger
if (condition && condition.$and.length) {
// TODO: check all conditions in condition against data
// const calculation = toCalculation(condition);
}
return callback({ data: data.get() }, options);
};
}
export default { export default {
name: 'model', name: 'model',
on(this: WorkflowModel, callback: Function) { on(this: WorkflowModel, callback: Function) {
const { database } = <typeof WorkflowModel>this.constructor; const { database } = <typeof WorkflowModel>this.constructor;
const { collection, mode, filter } = this.config; const { collection, mode } = this.config;
const Collection = database.getCollection(collection); const Collection = database.getCollection(collection);
if (!Collection) { if (!Collection) {
return; return;
} }
// async function, should return promise
const handler = (data: any, options) => {
if (filter) {
// TODO: check all conditions in filter against data
}
return callback({ data: data.get() }, options);
};
// TODO: duplication when mode change should be considered // TODO: duplication when mode change should be considered
for (let [key, event] of MODE_BITMAP_EVENTS.entries()) { for (let [key, event] of MODE_BITMAP_EVENTS.entries()) {
if (mode & key) { if (mode & key) {
Collection.model.addHook(event, this.getHookId(), handler); Collection.model.addHook(event, this.getHookId(), bindHandler.call(this, callback));
} }
} }
}, },