mirror of
https://gitee.com/nocobase/nocobase.git
synced 2025-07-02 03:02:19 +08:00
feat(plugin-workflow-manual): add task title column (#6051)
* feat(plugin-workflow-manual): add task title column * fix(plugin-workflow-manual): fix distribution and assignee logic
This commit is contained in:
parent
63ffe1a99d
commit
f26e501172
@ -132,6 +132,15 @@ export const todoCollection = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
type: 'string',
|
||||||
|
name: 'title',
|
||||||
|
uiSchema: {
|
||||||
|
type: 'string',
|
||||||
|
title: `{{t("Task title", { ns: "${NAMESPACE}" })}}`,
|
||||||
|
'x-component': 'Input',
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
type: 'belongsTo',
|
type: 'belongsTo',
|
||||||
name: 'node',
|
name: 'node',
|
||||||
@ -236,6 +245,21 @@ function UserJobStatusColumn(props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const tableColumns = {
|
const tableColumns = {
|
||||||
|
title: {
|
||||||
|
type: 'void',
|
||||||
|
'x-decorator': 'TableV2.Column.Decorator',
|
||||||
|
'x-component': 'TableV2.Column',
|
||||||
|
'x-component-props': {
|
||||||
|
width: null,
|
||||||
|
},
|
||||||
|
title: `{{t("Task title", { ns: "${NAMESPACE}" })}}`,
|
||||||
|
properties: {
|
||||||
|
title: {
|
||||||
|
'x-component': 'CollectionField',
|
||||||
|
'x-read-pretty': true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
workflow: {
|
workflow: {
|
||||||
type: 'void',
|
type: 'void',
|
||||||
'x-decorator': 'TableV2.Column.Decorator',
|
'x-decorator': 'TableV2.Column.Decorator',
|
||||||
@ -251,21 +275,6 @@ const tableColumns = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
node: {
|
|
||||||
type: 'void',
|
|
||||||
'x-decorator': 'TableV2.Column.Decorator',
|
|
||||||
'x-component': 'TableV2.Column',
|
|
||||||
'x-component-props': {
|
|
||||||
width: null,
|
|
||||||
},
|
|
||||||
title: `{{t("Task node", { ns: "${NAMESPACE}" })}}`,
|
|
||||||
properties: {
|
|
||||||
node: {
|
|
||||||
'x-component': 'NodeColumn',
|
|
||||||
'x-read-pretty': true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
status: {
|
status: {
|
||||||
type: 'void',
|
type: 'void',
|
||||||
'x-decorator': 'TableV2.Column.Decorator',
|
'x-decorator': 'TableV2.Column.Decorator',
|
||||||
@ -764,8 +773,6 @@ function TaskBlock() {
|
|||||||
userId: user?.data?.id,
|
userId: user?.data?.id,
|
||||||
},
|
},
|
||||||
appends: [
|
appends: [
|
||||||
'node.id',
|
|
||||||
'node.title',
|
|
||||||
'job.id',
|
'job.id',
|
||||||
'job.status',
|
'job.status',
|
||||||
'job.result',
|
'job.result',
|
||||||
@ -783,7 +790,7 @@ function TaskBlock() {
|
|||||||
type: 'void',
|
type: 'void',
|
||||||
'x-component': 'WorkflowTodo',
|
'x-component': 'WorkflowTodo',
|
||||||
'x-component-props': {
|
'x-component-props': {
|
||||||
columns: ['workflow', 'node', 'status', 'createdAt'],
|
columns: ['title', 'workflow', 'node', 'status', 'createdAt'],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -14,6 +14,7 @@ import {
|
|||||||
getCollectionFieldOptions,
|
getCollectionFieldOptions,
|
||||||
CollectionBlockInitializer,
|
CollectionBlockInitializer,
|
||||||
Instruction,
|
Instruction,
|
||||||
|
WorkflowVariableTextArea,
|
||||||
} from '@nocobase/plugin-workflow/client';
|
} from '@nocobase/plugin-workflow/client';
|
||||||
|
|
||||||
import { SchemaConfig, SchemaConfigButton } from './SchemaConfig';
|
import { SchemaConfig, SchemaConfigButton } from './SchemaConfig';
|
||||||
@ -70,6 +71,42 @@ function useVariables({ key, title, config }, { types, fieldNames = defaultField
|
|||||||
: null;
|
: null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function useInitializers(node): SchemaInitializerItemType | null {
|
||||||
|
const { getCollection } = useCollectionManager_deprecated();
|
||||||
|
const formKeys = Object.keys(node.config.forms ?? {});
|
||||||
|
if (!formKeys.length || node.config.mode) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const forms = formKeys
|
||||||
|
.map((formKey) => {
|
||||||
|
const form = node.config.forms[formKey];
|
||||||
|
const { fields = [] } = getCollection(form.collection);
|
||||||
|
|
||||||
|
return fields.length
|
||||||
|
? ({
|
||||||
|
name: form.title ?? formKey,
|
||||||
|
type: 'item',
|
||||||
|
title: form.title ?? formKey,
|
||||||
|
Component: CollectionBlockInitializer,
|
||||||
|
collection: form.collection,
|
||||||
|
dataPath: `$jobsMapByNodeKey.${node.key}.${formKey}`,
|
||||||
|
} as SchemaInitializerItemType)
|
||||||
|
: null;
|
||||||
|
})
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
|
return forms.length
|
||||||
|
? {
|
||||||
|
name: `#${node.id}`,
|
||||||
|
key: 'forms',
|
||||||
|
type: 'subMenu',
|
||||||
|
title: node.title,
|
||||||
|
children: forms,
|
||||||
|
}
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
export default class extends Instruction {
|
export default class extends Instruction {
|
||||||
title = `{{t("Manual", { ns: "${NAMESPACE}" })}}`;
|
title = `{{t("Manual", { ns: "${NAMESPACE}" })}}`;
|
||||||
type = 'manual';
|
type = 'manual';
|
||||||
@ -102,6 +139,13 @@ export default class extends Instruction {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
title: {
|
||||||
|
type: 'string',
|
||||||
|
title: `{{t("Task title", { ns: "${NAMESPACE}" })}}`,
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'WorkflowVariableTextArea',
|
||||||
|
description: `{{t("Title of each task item. Default to node title.", { ns: "${NAMESPACE}" })}}`,
|
||||||
|
},
|
||||||
schema: {
|
schema: {
|
||||||
type: 'void',
|
type: 'void',
|
||||||
title: `{{t("User interface", { ns: "${NAMESPACE}" })}}`,
|
title: `{{t("User interface", { ns: "${NAMESPACE}" })}}`,
|
||||||
@ -125,44 +169,10 @@ export default class extends Instruction {
|
|||||||
SchemaConfig,
|
SchemaConfig,
|
||||||
ModeConfig,
|
ModeConfig,
|
||||||
AssigneesSelect,
|
AssigneesSelect,
|
||||||
|
WorkflowVariableTextArea,
|
||||||
};
|
};
|
||||||
useVariables = useVariables;
|
useVariables = useVariables;
|
||||||
useInitializers(node): SchemaInitializerItemType | null {
|
useInitializers = useInitializers;
|
||||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
|
||||||
const { getCollection } = useCollectionManager_deprecated();
|
|
||||||
const formKeys = Object.keys(node.config.forms ?? {});
|
|
||||||
if (!formKeys.length || node.config.mode) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const forms = formKeys
|
|
||||||
.map((formKey) => {
|
|
||||||
const form = node.config.forms[formKey];
|
|
||||||
const { fields = [] } = getCollection(form.collection);
|
|
||||||
|
|
||||||
return fields.length
|
|
||||||
? ({
|
|
||||||
name: form.title ?? formKey,
|
|
||||||
type: 'item',
|
|
||||||
title: form.title ?? formKey,
|
|
||||||
Component: CollectionBlockInitializer,
|
|
||||||
collection: form.collection,
|
|
||||||
dataPath: `$jobsMapByNodeKey.${node.key}.${formKey}`,
|
|
||||||
} as SchemaInitializerItemType)
|
|
||||||
: null;
|
|
||||||
})
|
|
||||||
.filter(Boolean);
|
|
||||||
|
|
||||||
return forms.length
|
|
||||||
? {
|
|
||||||
name: `#${node.id}`,
|
|
||||||
key: 'forms',
|
|
||||||
type: 'subMenu',
|
|
||||||
title: node.title,
|
|
||||||
children: forms,
|
|
||||||
}
|
|
||||||
: null;
|
|
||||||
}
|
|
||||||
isAvailable({ engine, workflow, upstream, branchIndex }) {
|
isAvailable({ engine, workflow, upstream, branchIndex }) {
|
||||||
return !engine.isWorkflowSync(workflow);
|
return !engine.isWorkflowSync(workflow);
|
||||||
}
|
}
|
||||||
|
@ -29,5 +29,7 @@
|
|||||||
"Task node": "任务节点",
|
"Task node": "任务节点",
|
||||||
"Unprocessed": "未处理",
|
"Unprocessed": "未处理",
|
||||||
"Please check one of your update record form, and add at least one filter condition in form settings.": "请检查您的其中的更新数据表单,并在表单设置中至少添加一个筛选条件。",
|
"Please check one of your update record form, and add at least one filter condition in form settings.": "请检查您的其中的更新数据表单,并在表单设置中至少添加一个筛选条件。",
|
||||||
"My manual tasks": "我的人工任务"
|
"My manual tasks": "我的人工任务",
|
||||||
|
"Task title": "任务名称",
|
||||||
|
"Title of each task item. Default to node title.": "每项任务的名称。默认为节点名称。"
|
||||||
}
|
}
|
||||||
|
@ -25,6 +25,7 @@ export interface ManualConfig {
|
|||||||
forms: { [key: string]: FormType };
|
forms: { [key: string]: FormType };
|
||||||
assignees?: (number | string)[];
|
assignees?: (number | string)[];
|
||||||
mode?: number;
|
mode?: number;
|
||||||
|
title?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const MULTIPLE_ASSIGNED_MODE = {
|
const MULTIPLE_ASSIGNED_MODE = {
|
||||||
@ -115,7 +116,7 @@ export default class extends Instruction {
|
|||||||
if (!assignees.length) {
|
if (!assignees.length) {
|
||||||
return job;
|
return job;
|
||||||
}
|
}
|
||||||
|
const title = config.title ? processor.getParsedValue(config.title, node.id) : node.title;
|
||||||
// NOTE: batch create users jobs
|
// NOTE: batch create users jobs
|
||||||
const UserJobModel = this.workflow.app.db.getModel('users_jobs');
|
const UserJobModel = this.workflow.app.db.getModel('users_jobs');
|
||||||
await UserJobModel.bulkCreate(
|
await UserJobModel.bulkCreate(
|
||||||
@ -126,9 +127,10 @@ export default class extends Instruction {
|
|||||||
executionId: job.executionId,
|
executionId: job.executionId,
|
||||||
workflowId: node.workflowId,
|
workflowId: node.workflowId,
|
||||||
status: JOB_STATUS.PENDING,
|
status: JOB_STATUS.PENDING,
|
||||||
|
title,
|
||||||
})),
|
})),
|
||||||
{
|
{
|
||||||
// transaction: processor.transaction,
|
transaction: processor.mainTransaction,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -138,21 +140,29 @@ export default class extends Instruction {
|
|||||||
async resume(node, job, processor: Processor) {
|
async resume(node, job, processor: Processor) {
|
||||||
// NOTE: check all users jobs related if all done then continue as parallel
|
// NOTE: check all users jobs related if all done then continue as parallel
|
||||||
const { mode } = node.config as ManualConfig;
|
const { mode } = node.config as ManualConfig;
|
||||||
const assignees = [...new Set(processor.getParsedValue(node.config.assignees, node.id).flat().filter(Boolean))];
|
|
||||||
|
|
||||||
const UserJobModel = this.workflow.app.db.getModel('users_jobs');
|
const UserJobRepo = this.workflow.app.db.getRepository('users_jobs');
|
||||||
const distribution = await UserJobModel.count({
|
const jobs = await UserJobRepo.find({
|
||||||
where: {
|
where: {
|
||||||
jobId: job.id,
|
jobId: job.id,
|
||||||
},
|
},
|
||||||
group: ['status'],
|
transaction: processor.mainTransaction,
|
||||||
// transaction: processor.transaction,
|
|
||||||
});
|
});
|
||||||
|
const assignees = [];
|
||||||
|
const distributionMap = jobs.reduce((result, item) => {
|
||||||
|
if (result[item.status] == null) {
|
||||||
|
result[item.status] = 0;
|
||||||
|
}
|
||||||
|
result[item.status] += 1;
|
||||||
|
assignees.push(item.userId);
|
||||||
|
return result;
|
||||||
|
}, {});
|
||||||
|
const distribution = Object.keys(distributionMap).map((status) => ({
|
||||||
|
status: Number.parseInt(status, 10),
|
||||||
|
count: distributionMap[status],
|
||||||
|
}));
|
||||||
|
|
||||||
const submitted = distribution.reduce(
|
const submitted = jobs.reduce((count, item) => (item.status !== JOB_STATUS.PENDING ? count + 1 : count), 0);
|
||||||
(count, item) => (item.status !== JOB_STATUS.PENDING ? count + item.count : count),
|
|
||||||
0,
|
|
||||||
);
|
|
||||||
const status = job.status || (getMode(mode).getStatus(distribution, assignees) ?? JOB_STATUS.PENDING);
|
const status = job.status || (getMode(mode).getStatus(distribution, assignees) ?? JOB_STATUS.PENDING);
|
||||||
const result = mode ? (submitted || 0) / assignees.length : job.latestUserJob?.result ?? job.result;
|
const result = mode ? (submitted || 0) / assignees.length : job.latestUserJob?.result ?? job.result;
|
||||||
processor.logger.debug(`manual resume job and next status: ${status}`);
|
processor.logger.debug(`manual resume job and next status: ${status}`);
|
||||||
|
@ -37,6 +37,10 @@ export default defineCollection({
|
|||||||
foreignKey: 'userId',
|
foreignKey: 'userId',
|
||||||
primaryKey: false,
|
primaryKey: false,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
type: 'string',
|
||||||
|
name: 'title',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
type: 'belongsTo',
|
type: 'belongsTo',
|
||||||
name: 'execution',
|
name: 'execution',
|
||||||
|
@ -0,0 +1,45 @@
|
|||||||
|
/**
|
||||||
|
* This file is part of the NocoBase (R) project.
|
||||||
|
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||||
|
* Authors: NocoBase Team.
|
||||||
|
*
|
||||||
|
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||||
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Migration } from '@nocobase/server';
|
||||||
|
|
||||||
|
export default class extends Migration {
|
||||||
|
appVersion = '<1.6.0-alpha.13';
|
||||||
|
async up() {
|
||||||
|
const { db } = this.context;
|
||||||
|
const NodeRepo = db.getRepository('flow_nodes');
|
||||||
|
const TaskRepo = db.getRepository('users_jobs');
|
||||||
|
await db.sequelize.transaction(async (transaction) => {
|
||||||
|
const nodes = await NodeRepo.find({
|
||||||
|
filter: {
|
||||||
|
type: 'manual',
|
||||||
|
},
|
||||||
|
transaction,
|
||||||
|
});
|
||||||
|
|
||||||
|
await nodes.reduce(
|
||||||
|
(promise, node) =>
|
||||||
|
promise.then(() => {
|
||||||
|
return TaskRepo.update({
|
||||||
|
filter: {
|
||||||
|
nodeId: node.id,
|
||||||
|
},
|
||||||
|
values: {
|
||||||
|
title: node.title,
|
||||||
|
},
|
||||||
|
individualHooks: false,
|
||||||
|
silent: true,
|
||||||
|
transaction,
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
Promise.resolve(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -7,8 +7,6 @@
|
|||||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Context } from '@nocobase/actions';
|
|
||||||
import { parseCollectionName } from '@nocobase/data-source-manager';
|
|
||||||
import Trigger from '..';
|
import Trigger from '..';
|
||||||
import type Plugin from '../../Plugin';
|
import type Plugin from '../../Plugin';
|
||||||
import DateFieldScheduleTrigger from './DateFieldScheduleTrigger';
|
import DateFieldScheduleTrigger from './DateFieldScheduleTrigger';
|
||||||
|
Loading…
x
Reference in New Issue
Block a user