mirror of
https://gitee.com/nocobase/nocobase.git
synced 2025-05-05 13:39:24 +08:00
refactor(plugin-workflow): change task center api and ui (#6272)
* refactor(plugin-workflow): change task center api and ui * fix(client): add className property for Grid.Col * refactor(plugin-workflow): adjust tasks menu style * fix(plugin-workflow): fix menu title * feat(plugin-workflow): automatically update tasks number * fix(plugin-workflow): ignore ws if not exist * fix(plugin-workflow): fix compatibility of no user approvals * refactor(server): revert ws api back * fix(plugin-workflow-manual): fix migration and renamed test cases * fix(plugin-workflow): fix acl for task resource * refactor(client): show badge number in toolbar * fix(plugin-workflow): fix toolbar number * fix(client): adjust badge font size * refactor(plugin-workflow): adjust task center style and api * fix(plugin-workflow-manual): fix constants * refactor(plugin-workflow-manual): change legacy workflow todo block to list style * test(plugin-workflow-manual): migrations * refactor(plugin-workflow): add workflow title component * fix(plugin-workflow-manual): fix e2e test cases * fix(plugin-workflow): fix test kit
This commit is contained in:
parent
4d1f28bf57
commit
e5507d0758
@ -241,36 +241,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"key": "qe7b1rsct5h",
|
||||
"name": "jobs",
|
||||
"type": "belongsToMany",
|
||||
"interface": null,
|
||||
"description": null,
|
||||
"collectionName": "users",
|
||||
"parentKey": null,
|
||||
"reverseKey": null,
|
||||
"through": "users_jobs",
|
||||
"foreignKey": "userId",
|
||||
"sourceKey": "id",
|
||||
"otherKey": "jobId",
|
||||
"targetKey": "id",
|
||||
"target": "jobs"
|
||||
},
|
||||
{
|
||||
"key": "vt0n1l1ruyz",
|
||||
"name": "usersJobs",
|
||||
"type": "hasMany",
|
||||
"interface": null,
|
||||
"description": null,
|
||||
"collectionName": "users",
|
||||
"parentKey": null,
|
||||
"reverseKey": null,
|
||||
"target": "users_jobs",
|
||||
"foreignKey": "userId",
|
||||
"sourceKey": "id",
|
||||
"targetKey": "id"
|
||||
},
|
||||
{
|
||||
"key": "ekol7p60nry",
|
||||
"name": "sortName",
|
||||
|
@ -20,6 +20,8 @@ import { PopupVisibleProvider, PopupVisibleProviderContext } from '../../schema-
|
||||
export const PopupContextProvider: React.FC<{
|
||||
visible?: boolean;
|
||||
setVisible?: (visible: boolean) => void;
|
||||
openMode?: string;
|
||||
openSize?: string;
|
||||
}> = (props) => {
|
||||
const { visible: visibleFromProps, setVisible: setVisibleFromProps } = props;
|
||||
const [visible, setVisible] = useState(false);
|
||||
@ -37,8 +39,8 @@ export const PopupContextProvider: React.FC<{
|
||||
},
|
||||
[setVisibleFromProps, setVisibleWithURL],
|
||||
);
|
||||
const openMode = fieldSchema['x-component-props']?.['openMode'] || 'drawer';
|
||||
const openSize = fieldSchema['x-component-props']?.['openSize'];
|
||||
const openMode = props.openMode || fieldSchema['x-component-props']?.['openMode'] || 'drawer';
|
||||
const openSize = props.openSize || fieldSchema['x-component-props']?.['openSize'];
|
||||
|
||||
return (
|
||||
<PopupVisibleProvider visible={false}>
|
||||
|
@ -32,12 +32,41 @@ const pinnedPluginListClassName = css`
|
||||
align-items: center;
|
||||
|
||||
.ant-btn {
|
||||
border: 0;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 46px;
|
||||
width: 46px;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
background: none;
|
||||
color: rgba(255, 255, 255, 0.65);
|
||||
vertical-align: middle;
|
||||
|
||||
a {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.ant-badge {
|
||||
color: rgba(255, 255, 255, 0.65);
|
||||
.anticon {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
line-height: 1em;
|
||||
font-size: initial;
|
||||
}
|
||||
> sup {
|
||||
height: 10px;
|
||||
line-height: 10px;
|
||||
font-size: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: rgba(255, 255, 255, 0.1) !important;
|
||||
}
|
||||
|
@ -563,7 +563,7 @@ Grid.Col = observer(
|
||||
|
||||
return (
|
||||
<GridColContext.Provider value={value}>
|
||||
<div ref={setNodeRef} style={colStyle} className={cls('nb-grid-col')}>
|
||||
<div ref={setNodeRef} style={colStyle} className={cls('nb-grid-col', props.className)}>
|
||||
{props.children}
|
||||
</div>
|
||||
</GridColContext.Provider>
|
||||
|
@ -93,7 +93,7 @@ const useFullScreenHeight = (props?) => {
|
||||
return pageReservedHeight;
|
||||
};
|
||||
|
||||
const InternalWorkflowCollection = ['users_jobs', 'approvals', 'approvalRecords'];
|
||||
const InternalWorkflowCollection = ['workflowManualTasks', 'approvals', 'approvalRecords'];
|
||||
// 表格区块高度计算
|
||||
const useTableHeight = () => {
|
||||
const { token } = theme.useToken();
|
||||
|
@ -76,10 +76,10 @@ export class Gateway extends EventEmitter {
|
||||
|
||||
public server: http.Server | null = null;
|
||||
public ipcSocketServer: IPCSocketServer | null = null;
|
||||
public wsServer: WSServer;
|
||||
loggers = new Registry<SystemLogger>();
|
||||
private port: number = process.env.APP_PORT ? parseInt(process.env.APP_PORT) : null;
|
||||
private host = '0.0.0.0';
|
||||
private wsServer: WSServer;
|
||||
private socketPath = resolve(process.cwd(), 'storage', 'gateway.sock');
|
||||
|
||||
private constructor() {
|
||||
|
@ -233,7 +233,7 @@ export class WSServer extends EventEmitter {
|
||||
const client = this.webSocketClients.get(clientId);
|
||||
// remove all tags with the given tagKey
|
||||
client.tags.forEach((tag) => {
|
||||
if (tag.startsWith(tagKey)) {
|
||||
if (tag.startsWith(`${tagKey}#`)) {
|
||||
client.tags.delete(tag);
|
||||
}
|
||||
});
|
||||
@ -293,6 +293,16 @@ export class WSServer extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
sendToAppUser(appName: string, userId: string, message: object) {
|
||||
this.sendToConnectionsByTags(
|
||||
[
|
||||
{ tagName: 'userId', tagValue: `${userId}` },
|
||||
{ tagName: 'app', tagValue: appName },
|
||||
],
|
||||
message,
|
||||
);
|
||||
}
|
||||
|
||||
loopThroughConnections(callback: (client: WebSocketClient) => void) {
|
||||
this.webSocketClients.forEach((client) => {
|
||||
callback(client);
|
||||
|
@ -241,36 +241,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"key": "qe7b1rsct5h",
|
||||
"name": "jobs",
|
||||
"type": "belongsToMany",
|
||||
"interface": null,
|
||||
"description": null,
|
||||
"collectionName": "users",
|
||||
"parentKey": null,
|
||||
"reverseKey": null,
|
||||
"through": "users_jobs",
|
||||
"foreignKey": "userId",
|
||||
"sourceKey": "id",
|
||||
"otherKey": "jobId",
|
||||
"targetKey": "id",
|
||||
"target": "jobs"
|
||||
},
|
||||
{
|
||||
"key": "vt0n1l1ruyz",
|
||||
"name": "usersJobs",
|
||||
"type": "hasMany",
|
||||
"interface": null,
|
||||
"description": null,
|
||||
"collectionName": "users",
|
||||
"parentKey": null,
|
||||
"reverseKey": null,
|
||||
"target": "users_jobs",
|
||||
"foreignKey": "userId",
|
||||
"sourceKey": "id",
|
||||
"targetKey": "id"
|
||||
},
|
||||
{
|
||||
"key": "ekol7p60nry",
|
||||
"name": "sortName",
|
||||
|
@ -136,14 +136,11 @@ const InnerInbox = (props) => {
|
||||
}}
|
||||
>
|
||||
<Tooltip title={t('Message')}>
|
||||
<Badge count={unreadMsgsCountObs.value} size="small" offset={[-12, 14]}>
|
||||
<Button
|
||||
className={styles.button}
|
||||
title={t('Message')}
|
||||
icon={<Icon type={'BellOutlined'} />}
|
||||
onClick={onIconClick}
|
||||
/>
|
||||
<Button className={styles.button} onClick={onIconClick}>
|
||||
<Badge count={unreadMsgsCountObs.value} size="small">
|
||||
<Icon type={'BellOutlined'} />
|
||||
</Badge>
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Drawer
|
||||
title={DrawerTitle}
|
||||
|
@ -7,14 +7,114 @@
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import { ExtendCollectionsProvider, storePopupContext, useRequest } from '@nocobase/client';
|
||||
import React, { createContext, FC, useContext } from 'react';
|
||||
import { getWorkflowTodoViewActionSchema, nodeCollection, todoCollection, workflowCollection } from './WorkflowTodo';
|
||||
import { JOB_STATUS } from '@nocobase/plugin-workflow/client';
|
||||
import { ExtendCollectionsProvider, storePopupContext } from '@nocobase/client';
|
||||
import React, { FC } from 'react';
|
||||
import { getWorkflowTodoViewActionSchema } from './WorkflowTodo';
|
||||
import { TaskStatusOptions } from '../common/constants';
|
||||
import { NAMESPACE } from '../locale';
|
||||
|
||||
const collections = [nodeCollection, workflowCollection, todoCollection];
|
||||
const workflowCollection = {
|
||||
title: `{{t("Workflow", { ns: "workflow" })}}`,
|
||||
name: 'workflows',
|
||||
fields: [
|
||||
{
|
||||
type: 'string',
|
||||
name: 'title',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const ManualTaskCountRequestContext = createContext({});
|
||||
const todoCollection = {
|
||||
title: `{{t("Workflow todos", { ns: "${NAMESPACE}" })}}`,
|
||||
name: 'workflowManualTasks',
|
||||
fields: [
|
||||
{
|
||||
type: 'string',
|
||||
name: 'title',
|
||||
interface: 'input',
|
||||
uiSchema: {
|
||||
type: 'string',
|
||||
title: `{{t("Task title", { ns: "${NAMESPACE}" })}}`,
|
||||
'x-component': 'Input',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'belongsTo',
|
||||
name: 'workflow',
|
||||
target: 'workflows',
|
||||
foreignKey: 'workflowId',
|
||||
interface: 'm2o',
|
||||
isAssociation: true,
|
||||
uiSchema: {
|
||||
type: 'number',
|
||||
title: `{{t("Workflow", { ns: "workflow" })}}`,
|
||||
'x-component': 'AssociationField',
|
||||
'x-component-props': {
|
||||
fieldNames: {
|
||||
label: 'title',
|
||||
value: 'id',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'belongsTo',
|
||||
name: 'user',
|
||||
target: 'users',
|
||||
foreignKey: 'userId',
|
||||
interface: 'm2o',
|
||||
isAssociation: true,
|
||||
uiSchema: {
|
||||
type: 'object',
|
||||
title: `{{t("Assignee", { ns: "${NAMESPACE}" })}}`,
|
||||
'x-component': 'AssociationField',
|
||||
'x-component-props': {
|
||||
fieldNames: {
|
||||
label: 'nickname',
|
||||
value: 'id',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'integer',
|
||||
name: 'status',
|
||||
interface: 'select',
|
||||
uiSchema: {
|
||||
type: 'number',
|
||||
title: `{{t("Status", { ns: "workflow" })}}`,
|
||||
'x-component': 'Select',
|
||||
enum: TaskStatusOptions,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'createdAt',
|
||||
type: 'date',
|
||||
interface: 'createdAt',
|
||||
uiSchema: {
|
||||
type: 'datetime',
|
||||
title: '{{t("Created at")}}',
|
||||
'x-component': 'DatePicker',
|
||||
'x-component-props': {
|
||||
showTime: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'updatedAt',
|
||||
type: 'date',
|
||||
interface: 'updatedAt',
|
||||
uiSchema: {
|
||||
type: 'datetime',
|
||||
title: '{{t("Last updated at")}}',
|
||||
'x-component': 'DatePicker',
|
||||
'x-component-props': {
|
||||
showTime: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
/**
|
||||
* 1. 扩展几个工作流相关的 collection,防止在区块中因找不到 collection 而报错;
|
||||
@ -22,36 +122,19 @@ const ManualTaskCountRequestContext = createContext({});
|
||||
* @returns
|
||||
*/
|
||||
export const WorkflowManualProvider: FC = (props) => {
|
||||
const request = useRequest<any>(
|
||||
{
|
||||
resource: 'users_jobs',
|
||||
action: 'countMine',
|
||||
params: {
|
||||
filter: {
|
||||
status: JOB_STATUS.PENDING,
|
||||
},
|
||||
},
|
||||
},
|
||||
{ manual: true },
|
||||
);
|
||||
|
||||
return (
|
||||
<ExtendCollectionsProvider collections={collections}>
|
||||
<ManualTaskCountRequestContext.Provider value={request}>{props.children}</ManualTaskCountRequestContext.Provider>
|
||||
<ExtendCollectionsProvider collections={[workflowCollection, todoCollection]}>
|
||||
{props.children}
|
||||
</ExtendCollectionsProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export function useCountRequest() {
|
||||
return useContext(ManualTaskCountRequestContext);
|
||||
}
|
||||
|
||||
/**
|
||||
* 2. 将区块相关的按钮 Schema 缓存起来,这样就可以在弹窗中获取到 Schema,进而实现“弹窗 URL”的功能;
|
||||
*/
|
||||
function cacheSchema(collectionNameList: string[]) {
|
||||
collectionNameList.forEach((collectionName) => {
|
||||
const defaultOpenMode = isMobile() ? 'drawer' : 'page';
|
||||
const defaultOpenMode = isMobile() ? 'page' : 'modal';
|
||||
const workflowTodoViewActionSchema = getWorkflowTodoViewActionSchema({ defaultOpenMode, collectionName });
|
||||
|
||||
storePopupContext(workflowTodoViewActionSchema['x-uid'], {
|
||||
@ -61,7 +144,7 @@ function cacheSchema(collectionNameList: string[]) {
|
||||
});
|
||||
}
|
||||
|
||||
cacheSchema(Object.values(collections).map((collection) => collection.name));
|
||||
cacheSchema([todoCollection.name]);
|
||||
|
||||
function isMobile() {
|
||||
return window.location.pathname.startsWith('/m/');
|
||||
|
@ -7,13 +7,19 @@
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import { observer, useField, useFieldSchema, useForm } from '@formily/react';
|
||||
import { Button, Space, Spin, Tag } from 'antd';
|
||||
import React, { createContext, useCallback, useContext, useEffect, useState } from 'react';
|
||||
import { useField, useFieldSchema, useForm } from '@formily/react';
|
||||
import { FormLayout } from '@formily/antd-v5';
|
||||
import { Button, Card, ConfigProvider, Descriptions, Space, Spin, Tag } from 'antd';
|
||||
import { TableOutlined } from '@ant-design/icons';
|
||||
import { useAntdToken } from 'antd-style';
|
||||
import dayjs from 'dayjs';
|
||||
import React, { createContext, useContext, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { get } from 'lodash';
|
||||
|
||||
import {
|
||||
css,
|
||||
PopupContextProvider,
|
||||
SchemaInitializerItem,
|
||||
useCollectionRecordData,
|
||||
useCompile,
|
||||
@ -21,221 +27,33 @@ import {
|
||||
usePlugin,
|
||||
useSchemaInitializer,
|
||||
useSchemaInitializerItem,
|
||||
} from '@nocobase/client';
|
||||
|
||||
import {
|
||||
SchemaComponent,
|
||||
SchemaComponentContext,
|
||||
TableBlockProvider,
|
||||
useAPIClient,
|
||||
useActionContext,
|
||||
useCurrentUserContext,
|
||||
useFormBlockContext,
|
||||
useTableBlockContext,
|
||||
List,
|
||||
OpenModeProvider,
|
||||
} from '@nocobase/client';
|
||||
import WorkflowPlugin, {
|
||||
DetailsBlockProvider,
|
||||
FlowContext,
|
||||
JobStatusOptions,
|
||||
JobStatusOptionsMap,
|
||||
linkNodes,
|
||||
useAvailableUpstreams,
|
||||
useFlowContext,
|
||||
EXECUTION_STATUS,
|
||||
JOB_STATUS,
|
||||
WorkflowTitle,
|
||||
} from '@nocobase/plugin-workflow/client';
|
||||
|
||||
import { NAMESPACE, useLang } from '../locale';
|
||||
import { FormBlockProvider } from './instruction/FormBlockProvider';
|
||||
import { ManualFormType, manualFormTypes } from './instruction/SchemaConfig';
|
||||
import { TableOutlined } from '@ant-design/icons';
|
||||
import { TaskStatusOptionsMap } from '../common/constants';
|
||||
|
||||
export const nodeCollection = {
|
||||
title: `{{t("Task", { ns: "${NAMESPACE}" })}}`,
|
||||
name: 'flow_nodes',
|
||||
fields: [
|
||||
{
|
||||
type: 'bigInt',
|
||||
name: 'id',
|
||||
interface: 'm2o',
|
||||
uiSchema: {
|
||||
type: 'number',
|
||||
title: 'ID',
|
||||
'x-component': 'RemoteSelect',
|
||||
'x-component-props': {
|
||||
fieldNames: {
|
||||
label: 'title',
|
||||
value: 'id',
|
||||
},
|
||||
service: {
|
||||
resource: 'flow_nodes',
|
||||
params: {
|
||||
filter: {
|
||||
type: 'manual',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'string',
|
||||
name: 'title',
|
||||
interface: 'input',
|
||||
uiSchema: {
|
||||
type: 'string',
|
||||
title: '{{t("Title")}}',
|
||||
'x-component': 'Input',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const workflowCollection = {
|
||||
title: `{{t("Workflow", { ns: "${NAMESPACE}" })}}`,
|
||||
name: 'workflows',
|
||||
fields: [
|
||||
{
|
||||
type: 'string',
|
||||
name: 'title',
|
||||
interface: 'input',
|
||||
uiSchema: {
|
||||
title: '{{t("Name")}}',
|
||||
type: 'string',
|
||||
'x-component': 'Input',
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const todoCollection = {
|
||||
title: `{{t("Workflow todos", { ns: "${NAMESPACE}" })}}`,
|
||||
name: 'users_jobs',
|
||||
fields: [
|
||||
{
|
||||
type: 'belongsTo',
|
||||
name: 'user',
|
||||
target: 'users',
|
||||
foreignKey: 'userId',
|
||||
interface: 'm2o',
|
||||
uiSchema: {
|
||||
type: 'number',
|
||||
title: '{{t("User")}}',
|
||||
'x-component': 'RemoteSelect',
|
||||
'x-component-props': {
|
||||
fieldNames: {
|
||||
label: 'nickname',
|
||||
value: 'id',
|
||||
},
|
||||
service: {
|
||||
resource: 'users',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'string',
|
||||
name: 'title',
|
||||
uiSchema: {
|
||||
type: 'string',
|
||||
title: `{{t("Task title", { ns: "${NAMESPACE}" })}}`,
|
||||
'x-component': 'Input',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'belongsTo',
|
||||
name: 'node',
|
||||
target: 'flow_nodes',
|
||||
foreignKey: 'nodeId',
|
||||
interface: 'm2o',
|
||||
isAssociation: true,
|
||||
uiSchema: {
|
||||
type: 'number',
|
||||
title: `{{t("Task", { ns: "${NAMESPACE}" })}}`,
|
||||
'x-component': 'RemoteSelect',
|
||||
'x-component-props': {
|
||||
fieldNames: {
|
||||
label: 'title',
|
||||
value: 'id',
|
||||
},
|
||||
service: {
|
||||
resource: 'flow_nodes',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'belongsTo',
|
||||
name: 'workflow',
|
||||
target: 'workflows',
|
||||
foreignKey: 'workflowId',
|
||||
interface: 'm2o',
|
||||
uiSchema: {
|
||||
type: 'number',
|
||||
title: `{{t("Workflow", { ns: "${NAMESPACE}" })}}`,
|
||||
'x-component': 'RemoteSelect',
|
||||
'x-component-props': {
|
||||
fieldNames: {
|
||||
label: 'title',
|
||||
value: 'id',
|
||||
},
|
||||
service: {
|
||||
resource: 'workflows',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'integer',
|
||||
name: 'status',
|
||||
interface: 'select',
|
||||
uiSchema: {
|
||||
type: 'number',
|
||||
title: `{{t("Status", { ns: "${NAMESPACE}" })}}`,
|
||||
'x-component': 'Select',
|
||||
enum: JobStatusOptions,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'createdAt',
|
||||
type: 'date',
|
||||
interface: 'createdAt',
|
||||
uiSchema: {
|
||||
type: 'datetime',
|
||||
title: '{{t("Created at")}}',
|
||||
'x-component': 'DatePicker',
|
||||
'x-component-props': {
|
||||
showTime: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const NodeColumn = observer(
|
||||
() => {
|
||||
const field = useField<any>();
|
||||
return field?.value?.title ?? `#${field.value?.id}`;
|
||||
},
|
||||
{ displayName: 'NodeColumn' },
|
||||
);
|
||||
|
||||
const WorkflowColumn = observer(
|
||||
() => {
|
||||
const field = useField<any>();
|
||||
return field?.value?.title ?? `#${field.value?.id}`;
|
||||
},
|
||||
{ displayName: 'WorkflowColumn' },
|
||||
);
|
||||
|
||||
const UserColumn = observer(
|
||||
() => {
|
||||
const field = useField<any>();
|
||||
return field?.value?.nickname ?? field.value?.id;
|
||||
},
|
||||
{ displayName: 'UserColumn' },
|
||||
);
|
||||
|
||||
function UserJobStatusColumn(props) {
|
||||
function TaskStatusColumn(props) {
|
||||
const recordData = useCollectionRecordData();
|
||||
const labelUnprocessed = useLang('Unprocessed');
|
||||
if (recordData?.execution?.status && !recordData?.status) {
|
||||
@ -244,102 +62,39 @@ function UserJobStatusColumn(props) {
|
||||
return props.children;
|
||||
}
|
||||
|
||||
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: {
|
||||
type: 'void',
|
||||
'x-decorator': 'TableV2.Column.Decorator',
|
||||
'x-component': 'TableV2.Column',
|
||||
'x-component-props': {
|
||||
width: null,
|
||||
},
|
||||
title: `{{t("Workflow", { ns: "workflow" })}}`,
|
||||
properties: {
|
||||
workflow: {
|
||||
'x-component': 'WorkflowColumn',
|
||||
'x-read-pretty': true,
|
||||
},
|
||||
},
|
||||
},
|
||||
status: {
|
||||
type: 'void',
|
||||
'x-decorator': 'TableV2.Column.Decorator',
|
||||
'x-component': 'TableV2.Column',
|
||||
'x-component-props': {
|
||||
width: 100,
|
||||
},
|
||||
title: `{{t("Status", { ns: "workflow" })}}`,
|
||||
properties: {
|
||||
status: {
|
||||
type: 'number',
|
||||
'x-decorator': 'UserJobStatusColumn',
|
||||
'x-component': 'CollectionField',
|
||||
'x-read-pretty': true,
|
||||
},
|
||||
},
|
||||
},
|
||||
user: {
|
||||
type: 'void',
|
||||
'x-decorator': 'TableV2.Column.Decorator',
|
||||
'x-component': 'TableV2.Column',
|
||||
'x-component-props': {
|
||||
width: 140,
|
||||
},
|
||||
title: `{{t("Assignee", { ns: "${NAMESPACE}" })}}`,
|
||||
properties: {
|
||||
user: {
|
||||
'x-component': 'UserColumn',
|
||||
'x-read-pretty': true,
|
||||
},
|
||||
},
|
||||
},
|
||||
createdAt: {
|
||||
type: 'void',
|
||||
'x-decorator': 'TableV2.Column.Decorator',
|
||||
'x-component': 'TableV2.Column',
|
||||
'x-component-props': {
|
||||
width: 160,
|
||||
},
|
||||
properties: {
|
||||
createdAt: {
|
||||
type: 'string',
|
||||
'x-component': 'CollectionField',
|
||||
'x-read-pretty': true,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
function RecordTitle(props) {
|
||||
const record = useCollectionRecordData();
|
||||
if (Array.isArray(props.dataIndex)) {
|
||||
for (const index of props.dataIndex) {
|
||||
const title = get(record, index);
|
||||
if (title) {
|
||||
return title;
|
||||
}
|
||||
}
|
||||
}
|
||||
return get(record, props.dataIndex);
|
||||
}
|
||||
|
||||
export const WorkflowTodo: React.FC<{ columns?: string[] }> & {
|
||||
export const WorkflowTodo: React.FC & {
|
||||
Initializer: React.FC;
|
||||
Drawer: React.FC;
|
||||
Decorator: React.FC;
|
||||
TaskBlock: React.FC;
|
||||
// TaskBlock: React.FC;
|
||||
} = (props) => {
|
||||
const { columns = Object.keys(tableColumns) } = props;
|
||||
const { defaultOpenMode } = useOpenModeContext();
|
||||
const viewSchema = getWorkflowTodoViewActionSchema({ defaultOpenMode, collectionName: 'workflowManualTasks' });
|
||||
|
||||
return (
|
||||
<SchemaComponentContext.Provider value={{ designable: false }}>
|
||||
<SchemaComponent
|
||||
scope={{
|
||||
useCollectionRecordData,
|
||||
}}
|
||||
components={{
|
||||
NodeColumn,
|
||||
WorkflowColumn,
|
||||
UserColumn,
|
||||
UserJobStatusColumn,
|
||||
FormLayout,
|
||||
// WorkflowColumn,
|
||||
// UserColumn,
|
||||
ContentDetailWithTitle,
|
||||
}}
|
||||
schema={{
|
||||
type: 'void',
|
||||
@ -363,6 +118,9 @@ export const WorkflowTodo: React.FC<{ columns?: string[] }> & {
|
||||
'x-component-props': {
|
||||
icon: 'FilterOutlined',
|
||||
},
|
||||
default: {
|
||||
$and: [{ title: { $includes: '' } }, { 'workflow.title': { $includes: '' } }],
|
||||
},
|
||||
'x-align': 'left',
|
||||
},
|
||||
refresher: {
|
||||
@ -381,35 +139,64 @@ export const WorkflowTodo: React.FC<{ columns?: string[] }> & {
|
||||
},
|
||||
},
|
||||
},
|
||||
table: {
|
||||
list: {
|
||||
type: 'array',
|
||||
'x-component': 'TableV2',
|
||||
'x-use-component-props': 'useTableBlockProps',
|
||||
'x-component-props': {
|
||||
rowKey: 'id',
|
||||
},
|
||||
'x-component': 'List',
|
||||
// 'x-use-component-props': 'useListBlockProps',
|
||||
properties: {
|
||||
item: {
|
||||
type: 'object',
|
||||
'x-component': 'List.Item',
|
||||
properties: {
|
||||
content: {
|
||||
type: 'void',
|
||||
'x-decorator': 'FormLayout',
|
||||
'x-decorator-props': {
|
||||
layout: 'horizontal',
|
||||
},
|
||||
'x-component': 'ContentDetailWithTitle',
|
||||
'x-component-props': {
|
||||
// NOTE: component in schema can not work with popup
|
||||
// title: (
|
||||
// <SchemaComponent
|
||||
// schema={{
|
||||
// name: 'title',
|
||||
// type: 'string',
|
||||
// 'x-component': 'CollectionField',
|
||||
// }}
|
||||
// />
|
||||
// ),
|
||||
// extra: (
|
||||
// <SchemaComponent
|
||||
// schema={{
|
||||
// name: 'workflow.title',
|
||||
// type: 'string',
|
||||
// 'x-component': 'CollectionField',
|
||||
// }}
|
||||
// />
|
||||
// ),
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
type: 'void',
|
||||
'x-decorator': 'TableV2.Column.Decorator',
|
||||
'x-component': 'TableV2.Column',
|
||||
'x-component': 'ActionBar',
|
||||
'x-use-component-props': 'useListActionBarProps',
|
||||
'x-component-props': {
|
||||
width: 60,
|
||||
layout: 'one-column',
|
||||
},
|
||||
title: '{{t("Actions")}}',
|
||||
'x-align': 'left',
|
||||
properties: {
|
||||
view: getWorkflowTodoViewActionSchema({ defaultOpenMode, collectionName: 'users_jobs' }),
|
||||
view: viewSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
...columns.reduce((schema, key) => {
|
||||
schema[key] = tableColumns[key];
|
||||
return schema;
|
||||
}, {}),
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</SchemaComponentContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
@ -518,7 +305,7 @@ function useSubmit() {
|
||||
field.data = field.data || {};
|
||||
field.data.loading = true;
|
||||
|
||||
await api.resource('users_jobs').submit({
|
||||
await api.resource('workflowManualTasks').submit({
|
||||
filterByTk: userJob.id,
|
||||
values: {
|
||||
result: { [formKey]: { ...values, ...assignedValues.values }, _: actionKey },
|
||||
@ -545,7 +332,7 @@ function FlowContextProvider(props) {
|
||||
return;
|
||||
}
|
||||
api
|
||||
.resource('users_jobs')
|
||||
.resource('workflowManualTasks')
|
||||
.get?.({
|
||||
filterByTk: id,
|
||||
appends: ['node', 'job', 'workflow', 'workflow.nodes', 'execution', 'execution.jobs'],
|
||||
@ -638,19 +425,18 @@ function useDetailsBlockProps() {
|
||||
function FooterStatus() {
|
||||
const compile = useCompile();
|
||||
const { status, updatedAt } = useCollectionRecordData() || {};
|
||||
const statusOption = JobStatusOptionsMap[status];
|
||||
const statusOption = TaskStatusOptionsMap[status];
|
||||
return status ? (
|
||||
<Space>
|
||||
<time
|
||||
<Space
|
||||
className={css`
|
||||
margin-bottom: 1em;
|
||||
time {
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
`}
|
||||
>
|
||||
{dayjs(updatedAt).format('YYYY-MM-DD HH:mm:ss')}
|
||||
</time>
|
||||
<Tag icon={statusOption.icon} color={statusOption.color}>
|
||||
{compile(statusOption.label)}
|
||||
</Tag>
|
||||
<time>{dayjs(updatedAt).format('YYYY-MM-DD HH:mm:ss')}</time>
|
||||
<Tag color={statusOption.color}>{compile(statusOption.label)}</Tag>
|
||||
</Space>
|
||||
) : null;
|
||||
}
|
||||
@ -699,8 +485,8 @@ function Drawer() {
|
||||
function Decorator(props) {
|
||||
const { params = {}, children } = props;
|
||||
const blockProps = {
|
||||
collection: 'users_jobs',
|
||||
resource: 'users_jobs',
|
||||
collection: 'workflowManualTasks',
|
||||
resource: 'workflowManualTasks',
|
||||
action: 'list',
|
||||
params: {
|
||||
pageSize: 20,
|
||||
@ -712,15 +498,12 @@ function Decorator(props) {
|
||||
appends: ['user', 'node', 'workflow', 'execution.status'],
|
||||
except: ['node.config', 'workflow.config', 'workflow.options'],
|
||||
},
|
||||
rowKey: 'id',
|
||||
showIndex: true,
|
||||
dragSort: false,
|
||||
};
|
||||
|
||||
return (
|
||||
<TableBlockProvider name="workflow-todo" {...blockProps}>
|
||||
{children}
|
||||
</TableBlockProvider>
|
||||
<OpenModeProvider defaultOpenMode="modal">
|
||||
<List.Decorator {...blockProps}>{children}</List.Decorator>
|
||||
</OpenModeProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@ -754,22 +537,150 @@ function Initializer() {
|
||||
WorkflowTodo.Initializer = Initializer;
|
||||
WorkflowTodo.Drawer = Drawer;
|
||||
WorkflowTodo.Decorator = Decorator;
|
||||
WorkflowTodo.TaskBlock = TaskBlock;
|
||||
|
||||
function TaskBlock() {
|
||||
const { data: user } = useCurrentUserContext();
|
||||
function ContentDetail(props) {
|
||||
const { t } = useTranslation();
|
||||
const token = useAntdToken();
|
||||
return (
|
||||
<SchemaComponent
|
||||
components={{
|
||||
WorkflowTodo,
|
||||
<ConfigProvider
|
||||
theme={{
|
||||
token: {
|
||||
fontSizeLG: 14,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Descriptions
|
||||
{...props}
|
||||
column={1}
|
||||
items={[
|
||||
{
|
||||
key: 'createdAt',
|
||||
label: t('Created at'),
|
||||
children: (
|
||||
<SchemaComponent
|
||||
schema={{
|
||||
name: 'todos',
|
||||
type: 'void',
|
||||
'x-decorator': 'WorkflowTodo.Decorator',
|
||||
'x-decorator-props': {
|
||||
params: {
|
||||
name: 'createdAt',
|
||||
type: 'string',
|
||||
'x-component': 'CollectionField',
|
||||
'x-read-pretty': true,
|
||||
}}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'status',
|
||||
label: t('Status', { ns: 'workflow' }),
|
||||
children: (
|
||||
<SchemaComponent
|
||||
components={{ TaskStatusColumn }}
|
||||
schema={{
|
||||
name: 'status',
|
||||
type: 'number',
|
||||
'x-decorator': 'TaskStatusColumn',
|
||||
'x-component': 'CollectionField',
|
||||
'x-read-pretty': true,
|
||||
}}
|
||||
/>
|
||||
),
|
||||
},
|
||||
]}
|
||||
className={css`
|
||||
.ant-descriptions-header {
|
||||
margin-bottom: 0.5em;
|
||||
.ant-descriptions-extra {
|
||||
color: ${token.colorTextDescription};
|
||||
}
|
||||
}
|
||||
.ant-descriptions-item-label {
|
||||
width: 6em;
|
||||
}
|
||||
`}
|
||||
/>
|
||||
</ConfigProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function ContentDetailWithTitle(props) {
|
||||
return (
|
||||
<ContentDetail
|
||||
title={<RecordTitle dataIndex={['title', 'node.title']} />}
|
||||
extra={<RecordTitle dataIndex={'workflow.title'} />}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function TaskItem() {
|
||||
const token = useAntdToken();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const record = useCollectionRecordData();
|
||||
const { t } = useTranslation();
|
||||
// const { defaultOpenMode } = useOpenModeContext();
|
||||
// const { openPopup } = usePopupUtils();
|
||||
// const { isPopupVisibleControlledByURL } = usePopupSettings();
|
||||
const onOpen = useCallback((e: React.MouseEvent) => {
|
||||
const targetElement = e.target as Element; // 将事件目标转换为Element类型
|
||||
const currentTargetElement = e.currentTarget as Element;
|
||||
if (currentTargetElement.contains(targetElement)) {
|
||||
setVisible(true);
|
||||
// if (!isPopupVisibleControlledByURL()) {
|
||||
// } else {
|
||||
// openPopup({
|
||||
// // popupUidUsedInURL: 'job',
|
||||
// customActionSchema: {
|
||||
// type: 'void',
|
||||
// 'x-uid': 'job-view',
|
||||
// 'x-action-context': {
|
||||
// dataSource: 'main',
|
||||
// collection: 'workflowManualTasks',
|
||||
// doNotUpdateContext: true,
|
||||
// },
|
||||
// properties: {},
|
||||
// },
|
||||
// });
|
||||
// }
|
||||
}
|
||||
e.stopPropagation();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card
|
||||
onClick={onOpen}
|
||||
hoverable
|
||||
size="small"
|
||||
title={record.title}
|
||||
extra={<WorkflowTitle {...record.workflow} />}
|
||||
className={css`
|
||||
.ant-card-extra {
|
||||
color: ${token.colorTextDescription};
|
||||
}
|
||||
`}
|
||||
>
|
||||
<ContentDetail />
|
||||
</Card>
|
||||
<PopupContextProvider visible={visible} setVisible={setVisible} openMode="modal">
|
||||
<Drawer />
|
||||
</PopupContextProvider>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const StatusFilterMap = {
|
||||
pending: {
|
||||
status: JOB_STATUS.PENDING,
|
||||
'execution.status': EXECUTION_STATUS.STARTED,
|
||||
},
|
||||
completed: {
|
||||
status: JOB_STATUS.RESOLVED,
|
||||
},
|
||||
};
|
||||
|
||||
function useTodoActionParams(status) {
|
||||
const { data: user } = useCurrentUserContext();
|
||||
const filter = StatusFilterMap[status] ?? {};
|
||||
return {
|
||||
filter: {
|
||||
...filter,
|
||||
userId: user?.data?.id,
|
||||
},
|
||||
appends: [
|
||||
@ -782,15 +693,36 @@ function TaskBlock() {
|
||||
'execution.id',
|
||||
'execution.status',
|
||||
],
|
||||
},
|
||||
},
|
||||
'x-component': 'CardItem',
|
||||
properties: {
|
||||
todos: {
|
||||
};
|
||||
}
|
||||
|
||||
function TodoExtraActions() {
|
||||
return (
|
||||
<SchemaComponent
|
||||
schema={{
|
||||
name: 'actions',
|
||||
type: 'void',
|
||||
'x-component': 'WorkflowTodo',
|
||||
'x-component': 'ActionBar',
|
||||
properties: {
|
||||
refresh: {
|
||||
type: 'void',
|
||||
title: '{{ t("Refresh") }}',
|
||||
'x-component': 'Action',
|
||||
'x-use-component-props': 'useRefreshActionProps',
|
||||
'x-component-props': {
|
||||
columns: ['title', 'workflow', 'node', 'status', 'createdAt'],
|
||||
icon: 'ReloadOutlined',
|
||||
},
|
||||
},
|
||||
filter: {
|
||||
type: 'void',
|
||||
title: '{{t("Filter")}}',
|
||||
'x-component': 'Filter.Action',
|
||||
'x-use-component-props': 'useFilterActionProps',
|
||||
'x-component-props': {
|
||||
icon: 'FilterOutlined',
|
||||
},
|
||||
default: {
|
||||
$and: [{ title: { $includes: '' } }, { 'workflow.title': { $includes: '' } }],
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -798,3 +730,11 @@ function TaskBlock() {
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export const manualTodo = {
|
||||
title: `{{t("My manual tasks", { ns: "${NAMESPACE}" })}}`,
|
||||
collection: 'workflowManualTasks',
|
||||
useActionParams: useTodoActionParams,
|
||||
component: TaskItem,
|
||||
extraActions: TodoExtraActions,
|
||||
};
|
||||
|
@ -6,4 +6,3 @@
|
||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
|
@ -130,7 +130,7 @@ test.describe('field data entry', () => {
|
||||
await page.getByRole('menuitem', { name: 'check-square Workflow todos' }).click();
|
||||
await page.mouse.move(300, 0, { steps: 100 });
|
||||
await page.waitForTimeout(300);
|
||||
await page.locator('tr', { hasText: manualNodeName }).getByLabel('action-Action.Link-View-view-').click();
|
||||
await page.locator('.ant-list', { hasText: manualNodeName }).getByLabel('action-Action.Link-View-view-').click();
|
||||
// 定义获取2位小数
|
||||
const manualNodeRecord = faker.number.float({ min: 0, max: 100, precision: 2 });
|
||||
await page.getByRole('spinbutton').fill(manualNodeRecord.toString());
|
||||
@ -266,7 +266,7 @@ test.describe('field data entry', () => {
|
||||
await page.getByRole('menuitem', { name: 'check-square Workflow todos' }).click();
|
||||
await page.mouse.move(300, 0, { steps: 100 });
|
||||
await page.waitForTimeout(300);
|
||||
await page.locator('tr', { hasText: manualNodeName }).getByLabel('action-Action.Link-View-view-').click();
|
||||
await page.locator('.ant-list', { hasText: manualNodeName }).getByLabel('action-Action.Link-View-view-').click();
|
||||
// const manualNodeRecord = faker.number.float();
|
||||
await page.getByRole('checkbox').check();
|
||||
await page.getByRole('button', { name: 'Continue the process' }).click();
|
||||
@ -401,7 +401,7 @@ test.describe('field data entry', () => {
|
||||
await page.getByRole('menuitem', { name: 'check-square Workflow todos' }).click();
|
||||
await page.mouse.move(300, 0, { steps: 100 });
|
||||
await page.waitForTimeout(300);
|
||||
await page.locator('tr', { hasText: manualNodeName }).getByLabel('action-Action.Link-View-view-').click();
|
||||
await page.locator('.ant-list', { hasText: manualNodeName }).getByLabel('action-Action.Link-View-view-').click();
|
||||
await page.getByTestId('select-single').click();
|
||||
await page.getByRole('option', { name: '存续' }).click();
|
||||
await page.getByRole('button', { name: 'Continue the process' }).click();
|
||||
|
@ -130,7 +130,7 @@ test.describe('action button', () => {
|
||||
await page.getByRole('menuitem', { name: 'check-square Workflow todos' }).click();
|
||||
await page.mouse.move(300, 0, { steps: 100 });
|
||||
await page.waitForTimeout(300);
|
||||
await page.locator('tr', { hasText: manualNodeName }).getByLabel('action-Action.Link-View-view-').click();
|
||||
await page.locator('.ant-list', { hasText: manualNodeName }).getByLabel('action-Action.Link-View-view-').click();
|
||||
const manualNodeRecord = manualNodeFieldDisplayName + dayjs().format('YYYYMMDDHHmmss.SSS').toString();
|
||||
await page.getByRole('textbox').fill(manualNodeRecord);
|
||||
await page.getByRole('button', { name: 'Continue the process' }).click();
|
||||
@ -271,7 +271,7 @@ test.describe('action button', () => {
|
||||
await page.getByRole('menuitem', { name: 'check-square Workflow todos' }).click();
|
||||
await page.mouse.move(300, 0, { steps: 100 });
|
||||
await page.waitForTimeout(300);
|
||||
await page.locator('tr', { hasText: manualNodeName }).getByLabel('action-Action.Link-View-view-').click();
|
||||
await page.locator('.ant-list', { hasText: manualNodeName }).getByLabel('action-Action.Link-View-view-').click();
|
||||
const manualNodeRecord = manualNodeFieldDisplayName + dayjs().format('YYYYMMDDHHmmss.SSS').toString();
|
||||
await page.getByRole('textbox').fill(manualNodeRecord);
|
||||
await page.getByRole('button', { name: 'Terminate the process' }).click();
|
||||
@ -412,7 +412,7 @@ test.describe('action button', () => {
|
||||
await page.getByRole('menuitem', { name: 'check-square Workflow todos' }).click();
|
||||
await page.mouse.move(300, 0, { steps: 100 });
|
||||
await page.waitForTimeout(300);
|
||||
await page.locator('tr', { hasText: manualNodeName }).getByLabel('action-Action.Link-View-view-').click();
|
||||
await page.locator('.ant-list', { hasText: manualNodeName }).getByLabel('action-Action.Link-View-view-').click();
|
||||
const manualNodeRecord = manualNodeFieldDisplayName + dayjs().format('YYYYMMDDHHmmss.SSS').toString();
|
||||
await page.getByRole('textbox').fill(manualNodeRecord);
|
||||
await page.getByRole('button', { name: 'Save temporarily' }).click();
|
||||
|
@ -130,7 +130,7 @@ test.describe('field data entry', () => {
|
||||
await page.getByRole('menuitem', { name: 'check-square Workflow todos' }).click();
|
||||
await page.mouse.move(300, 0, { steps: 100 });
|
||||
await page.waitForTimeout(300);
|
||||
await page.locator('tr', { hasText: manualNodeName }).getByLabel('action-Action.Link-View-view-').click();
|
||||
await page.locator('.ant-list', { hasText: manualNodeName }).getByLabel('action-Action.Link-View-view-').click();
|
||||
await page.getByTestId('select-multiple').click();
|
||||
await page.getByRole('option', { name: '软件销售', exact: true }).click();
|
||||
await page.getByRole('option', { name: '软件开发', exact: true }).click();
|
||||
@ -273,7 +273,7 @@ test.describe('field data entry', () => {
|
||||
await page.getByRole('menuitem', { name: 'check-square Workflow todos' }).click();
|
||||
await page.mouse.move(300, 0, { steps: 100 });
|
||||
await page.waitForTimeout(300);
|
||||
await page.locator('tr', { hasText: manualNodeName }).getByLabel('action-Action.Link-View-view-').click();
|
||||
await page.locator('.ant-list', { hasText: manualNodeName }).getByLabel('action-Action.Link-View-view-').click();
|
||||
await page.getByLabel('存续').check();
|
||||
await page.getByRole('button', { name: 'Continue the process' }).click();
|
||||
|
||||
@ -407,7 +407,7 @@ test.describe('field data entry', () => {
|
||||
await page.getByRole('menuitem', { name: 'check-square Workflow todos' }).click();
|
||||
await page.mouse.move(300, 0, { steps: 100 });
|
||||
await page.waitForTimeout(300);
|
||||
await page.locator('tr', { hasText: manualNodeName }).getByLabel('action-Action.Link-View-view-').click();
|
||||
await page.locator('.ant-list', { hasText: manualNodeName }).getByLabel('action-Action.Link-View-view-').click();
|
||||
await page.getByLabel('软件销售', { exact: true }).check();
|
||||
await page.getByLabel('软件开发', { exact: true }).check();
|
||||
await page.getByRole('button', { name: 'Continue the process' }).click();
|
||||
@ -548,7 +548,7 @@ test.describe('field data entry', () => {
|
||||
await page.getByRole('menuitem', { name: 'check-square Workflow todos' }).click();
|
||||
await page.mouse.move(300, 0, { steps: 100 });
|
||||
await page.waitForTimeout(300);
|
||||
await page.locator('tr', { hasText: manualNodeName }).getByLabel('action-Action.Link-View-view-').click();
|
||||
await page.locator('.ant-list', { hasText: manualNodeName }).getByLabel('action-Action.Link-View-view-').click();
|
||||
const manualNodeRecord = dayjs().format('YYYY-MM-DD');
|
||||
await page.getByPlaceholder('Select date').click();
|
||||
await page.getByTitle(manualNodeRecord.toString()).click();
|
||||
|
@ -130,7 +130,7 @@ test.describe('field data entry', () => {
|
||||
await page.getByRole('menuitem', { name: 'check-square Workflow todos' }).click();
|
||||
await page.mouse.move(300, 0, { steps: 100 });
|
||||
await page.waitForTimeout(300);
|
||||
await page.locator('tr', { hasText: manualNodeName }).getByLabel('action-Action.Link-View-view-').click();
|
||||
await page.locator('.ant-list', { hasText: manualNodeName }).getByLabel('action-Action.Link-View-view-').click();
|
||||
const manualNodeRecord = manualNodeFieldDisplayName + dayjs().format('YYYYMMDDHHmmss.SSS').toString();
|
||||
await page.getByRole('textbox').fill(manualNodeRecord);
|
||||
await page.getByRole('button', { name: 'Continue the process' }).click();
|
||||
@ -265,7 +265,7 @@ test.describe('field data entry', () => {
|
||||
await page.getByRole('menuitem', { name: 'check-square Workflow todos' }).click();
|
||||
await page.mouse.move(300, 0, { steps: 100 });
|
||||
await page.waitForTimeout(300);
|
||||
await page.locator('tr', { hasText: manualNodeName }).getByLabel('action-Action.Link-View-view-').click();
|
||||
await page.locator('.ant-list', { hasText: manualNodeName }).getByLabel('action-Action.Link-View-view-').click();
|
||||
const manualNodeRecord = manualNodeFieldDisplayName + dayjs().format('YYYYMMDDHHmmss.SSS').toString();
|
||||
await page.getByRole('textbox').fill(manualNodeRecord);
|
||||
await page.getByRole('button', { name: 'Continue the process' }).click();
|
||||
@ -400,7 +400,7 @@ test.describe('field data entry', () => {
|
||||
await page.getByRole('menuitem', { name: 'check-square Workflow todos' }).click();
|
||||
await page.mouse.move(300, 0, { steps: 100 });
|
||||
await page.waitForTimeout(300);
|
||||
await page.locator('tr', { hasText: manualNodeName }).getByLabel('action-Action.Link-View-view-').click();
|
||||
await page.locator('.ant-list', { hasText: manualNodeName }).getByLabel('action-Action.Link-View-view-').click();
|
||||
const manualNodeRecord = manualNodeFieldDisplayName + dayjs().format('YYYYMMDDHHmmss.SSS').toString();
|
||||
await page.getByRole('textbox').fill(manualNodeRecord);
|
||||
await page.getByRole('button', { name: 'Continue the process' }).click();
|
||||
|
@ -130,7 +130,7 @@ test.describe('field data entry', () => {
|
||||
await page.getByRole('menuitem', { name: 'check-square Workflow todos' }).click();
|
||||
await page.mouse.move(300, 0, { steps: 100 });
|
||||
await page.waitForTimeout(300);
|
||||
await page.locator('tr', { hasText: manualNodeName }).getByLabel('action-Action.Link-View-view-').click();
|
||||
await page.locator('.ant-list', { hasText: manualNodeName }).getByLabel('action-Action.Link-View-view-').click();
|
||||
const manualNodeRecord = faker.internet.email();
|
||||
await page.getByRole('textbox').fill(manualNodeRecord);
|
||||
await page.getByRole('button', { name: 'Continue the process' }).click();
|
||||
@ -265,7 +265,7 @@ test.describe('field data entry', () => {
|
||||
await page.getByRole('menuitem', { name: 'check-square Workflow todos' }).click();
|
||||
await page.mouse.move(300, 0, { steps: 100 });
|
||||
await page.waitForTimeout(300);
|
||||
await page.locator('tr', { hasText: manualNodeName }).getByLabel('action-Action.Link-View-view-').click();
|
||||
await page.locator('.ant-list', { hasText: manualNodeName }).getByLabel('action-Action.Link-View-view-').click();
|
||||
const manualNodeRecord = faker.number.int();
|
||||
await page.getByRole('spinbutton').fill(manualNodeRecord.toString());
|
||||
await page.getByRole('button', { name: 'Continue the process' }).click();
|
||||
@ -400,7 +400,7 @@ test.describe('field data entry', () => {
|
||||
await page.getByRole('menuitem', { name: 'check-square Workflow todos' }).click();
|
||||
await page.mouse.move(300, 0, { steps: 100 });
|
||||
await page.waitForTimeout(300);
|
||||
await page.locator('tr', { hasText: manualNodeName }).getByLabel('action-Action.Link-View-view-').click();
|
||||
await page.locator('.ant-list', { hasText: manualNodeName }).getByLabel('action-Action.Link-View-view-').click();
|
||||
const manualNodeRecord = faker.number.float({ min: 0, max: 999999999, precision: 2 });
|
||||
await page.getByRole('spinbutton').fill(manualNodeRecord.toString());
|
||||
await page.getByRole('button', { name: 'Continue the process' }).click();
|
||||
|
@ -140,7 +140,7 @@ test.describe('field data entry', () => {
|
||||
await page.getByRole('menuitem', { name: 'check-square Workflow todos' }).click();
|
||||
await page.mouse.move(300, 0, { steps: 100 });
|
||||
await page.waitForTimeout(300);
|
||||
await page.locator('tr', { hasText: manualNodeName }).getByLabel('action-Action.Link-View-view-').click();
|
||||
await page.locator('.ant-list', { hasText: manualNodeName }).getByLabel('action-Action.Link-View-view-').click();
|
||||
const manualNodeRecord = faker.number.float({ min: 0, max: 100, precision: 2 });
|
||||
await page.getByRole('spinbutton').fill(manualNodeRecord.toString());
|
||||
await page.getByRole('button', { name: 'Continue the process' }).click();
|
||||
@ -279,7 +279,7 @@ test.describe('field data entry', () => {
|
||||
await page.getByRole('menuitem', { name: 'check-square Workflow todos' }).click();
|
||||
await page.mouse.move(300, 0, { steps: 100 });
|
||||
await page.waitForTimeout(300);
|
||||
await page.locator('tr', { hasText: manualNodeName }).getByLabel('action-Action.Link-View-view-').click();
|
||||
await page.locator('.ant-list', { hasText: manualNodeName }).getByLabel('action-Action.Link-View-view-').click();
|
||||
// const manualNodeRecord = faker.number.float({ min: 0, max: 100, precision: 2 });
|
||||
await page.getByRole('checkbox').check();
|
||||
await page.getByRole('button', { name: 'Continue the process' }).click();
|
||||
@ -424,7 +424,7 @@ test.describe('field data entry', () => {
|
||||
await page.getByRole('menuitem', { name: 'check-square Workflow todos' }).click();
|
||||
await page.mouse.move(300, 0, { steps: 100 });
|
||||
await page.waitForTimeout(300);
|
||||
await page.locator('tr', { hasText: manualNodeName }).getByLabel('action-Action.Link-View-view-').click();
|
||||
await page.locator('.ant-list', { hasText: manualNodeName }).getByLabel('action-Action.Link-View-view-').click();
|
||||
// const manualNodeRecord = faker.number.float({ min: 0, max: 100, precision: 2 });
|
||||
await page.getByTestId('select-single').click();
|
||||
await page.getByRole('option', { name: '存续' }).click();
|
||||
|
@ -142,7 +142,7 @@ test.describe('field data entry', () => {
|
||||
await page.getByRole('menuitem', { name: 'check-square Workflow todos' }).click();
|
||||
await page.mouse.move(300, 0, { steps: 100 });
|
||||
await page.waitForTimeout(300);
|
||||
await page.locator('tr', { hasText: manualNodeName }).getByLabel('action-Action.Link-View-view-').click();
|
||||
await page.locator('.ant-list', { hasText: manualNodeName }).getByLabel('action-Action.Link-View-view-').click();
|
||||
await page.getByTestId('select-multiple').click();
|
||||
await page.getByRole('option', { name: '软件销售', exact: true }).click();
|
||||
await page.getByRole('option', { name: '软件开发', exact: true }).click();
|
||||
@ -295,7 +295,7 @@ test.describe('field data entry', () => {
|
||||
await page.getByRole('menuitem', { name: 'check-square Workflow todos' }).click();
|
||||
await page.mouse.move(300, 0, { steps: 100 });
|
||||
await page.waitForTimeout(300);
|
||||
await page.locator('tr', { hasText: manualNodeName }).getByLabel('action-Action.Link-View-view-').click();
|
||||
await page.locator('.ant-list', { hasText: manualNodeName }).getByLabel('action-Action.Link-View-view-').click();
|
||||
// const manualNodeRecord = faker.number.float({ min: 0, max: 100, precision: 2 });
|
||||
await page.getByLabel('存续').check();
|
||||
await page.getByRole('button', { name: 'Continue the process' }).click();
|
||||
@ -440,7 +440,7 @@ test.describe('field data entry', () => {
|
||||
await page.getByRole('menuitem', { name: 'check-square Workflow todos' }).click();
|
||||
await page.mouse.move(300, 0, { steps: 100 });
|
||||
await page.waitForTimeout(300);
|
||||
await page.locator('tr', { hasText: manualNodeName }).getByLabel('action-Action.Link-View-view-').click();
|
||||
await page.locator('.ant-list', { hasText: manualNodeName }).getByLabel('action-Action.Link-View-view-').click();
|
||||
await page.getByLabel('软件销售', { exact: true }).check();
|
||||
await page.getByLabel('软件开发', { exact: true }).check();
|
||||
await page.getByRole('button', { name: 'Continue the process' }).click();
|
||||
@ -585,7 +585,7 @@ test.describe('field data entry', () => {
|
||||
await page.getByRole('menuitem', { name: 'check-square Workflow todos' }).click();
|
||||
await page.mouse.move(300, 0, { steps: 100 });
|
||||
await page.waitForTimeout(300);
|
||||
await page.locator('tr', { hasText: manualNodeName }).getByLabel('action-Action.Link-View-view-').click();
|
||||
await page.locator('.ant-list', { hasText: manualNodeName }).getByLabel('action-Action.Link-View-view-').click();
|
||||
const manualNodeRecord = dayjs().format('YYYY-MM-DD');
|
||||
await page.getByPlaceholder('Select date').click();
|
||||
await page.getByTitle(manualNodeRecord.toString()).click();
|
||||
|
@ -136,7 +136,7 @@ test.describe('field data entry', () => {
|
||||
await page.getByRole('menuitem', { name: 'check-square Workflow todos' }).click();
|
||||
await page.mouse.move(300, 0, { steps: 100 });
|
||||
await page.waitForTimeout(300);
|
||||
await page.locator('tr', { hasText: manualNodeName }).getByLabel('action-Action.Link-View-view-').click();
|
||||
await page.locator('.ant-list', { hasText: manualNodeName }).getByLabel('action-Action.Link-View-view-').click();
|
||||
const manualNodeRecord = manualNodeFieldDisplayName + dayjs().format('YYYYMMDDHHmmss.SSS').toString();
|
||||
await page.getByRole('textbox').fill(manualNodeRecord);
|
||||
await page.getByRole('button', { name: 'Continue the process' }).click();
|
||||
@ -275,7 +275,7 @@ test.describe('field data entry', () => {
|
||||
await page.getByRole('menuitem', { name: 'check-square Workflow todos' }).click();
|
||||
await page.mouse.move(300, 0, { steps: 100 });
|
||||
await page.waitForTimeout(300);
|
||||
await page.locator('tr', { hasText: manualNodeName }).getByLabel('action-Action.Link-View-view-').click();
|
||||
await page.locator('.ant-list', { hasText: manualNodeName }).getByLabel('action-Action.Link-View-view-').click();
|
||||
const manualNodeRecord = manualNodeFieldDisplayName + dayjs().format('YYYYMMDDHHmmss.SSS').toString();
|
||||
await page.getByRole('textbox').fill(manualNodeRecord);
|
||||
await page.getByRole('button', { name: 'Continue the process' }).click();
|
||||
@ -414,7 +414,7 @@ test.describe('field data entry', () => {
|
||||
await page.getByRole('menuitem', { name: 'check-square Workflow todos' }).click();
|
||||
await page.mouse.move(300, 0, { steps: 100 });
|
||||
await page.waitForTimeout(300);
|
||||
await page.locator('tr', { hasText: manualNodeName }).getByLabel('action-Action.Link-View-view-').click();
|
||||
await page.locator('.ant-list', { hasText: manualNodeName }).getByLabel('action-Action.Link-View-view-').click();
|
||||
const manualNodeRecord = manualNodeFieldDisplayName + dayjs().format('YYYYMMDDHHmmss.SSS').toString();
|
||||
await page.getByRole('textbox').fill(manualNodeRecord);
|
||||
await page.getByRole('button', { name: 'Continue the process' }).click();
|
||||
|
@ -136,7 +136,7 @@ test.describe('field data entry', () => {
|
||||
await page.getByRole('menuitem', { name: 'check-square Workflow todos' }).click();
|
||||
await page.mouse.move(300, 0, { steps: 100 });
|
||||
await page.waitForTimeout(300);
|
||||
await page.locator('tr', { hasText: manualNodeName }).getByLabel('action-Action.Link-View-view-').click();
|
||||
await page.locator('.ant-list', { hasText: manualNodeName }).getByLabel('action-Action.Link-View-view-').click();
|
||||
const manualNodeRecord = faker.internet.email();
|
||||
await page.getByRole('textbox').fill(manualNodeRecord);
|
||||
await page.getByRole('button', { name: 'Continue the process' }).click();
|
||||
@ -275,7 +275,7 @@ test.describe('field data entry', () => {
|
||||
await page.getByRole('menuitem', { name: 'check-square Workflow todos' }).click();
|
||||
await page.mouse.move(300, 0, { steps: 100 });
|
||||
await page.waitForTimeout(300);
|
||||
await page.locator('tr', { hasText: manualNodeName }).getByLabel('action-Action.Link-View-view-').click();
|
||||
await page.locator('.ant-list', { hasText: manualNodeName }).getByLabel('action-Action.Link-View-view-').click();
|
||||
const manualNodeRecord = faker.number.int();
|
||||
await page.getByRole('spinbutton').fill(manualNodeRecord.toString());
|
||||
await page.getByRole('button', { name: 'Continue the process' }).click();
|
||||
@ -414,7 +414,7 @@ test.describe('field data entry', () => {
|
||||
await page.getByRole('menuitem', { name: 'check-square Workflow todos' }).click();
|
||||
await page.mouse.move(300, 0, { steps: 100 });
|
||||
await page.waitForTimeout(300);
|
||||
await page.locator('tr', { hasText: manualNodeName }).getByLabel('action-Action.Link-View-view-').click();
|
||||
await page.locator('.ant-list', { hasText: manualNodeName }).getByLabel('action-Action.Link-View-view-').click();
|
||||
const manualNodeRecord = faker.number.float();
|
||||
await page.getByRole('spinbutton').fill(manualNodeRecord.toString());
|
||||
await page.getByRole('button', { name: 'Continue the process' }).click();
|
||||
|
@ -145,7 +145,7 @@ test.describe('field data', () => {
|
||||
await page.getByRole('menuitem', { name: 'check-square Workflow todos' }).click();
|
||||
await page.mouse.move(300, 0, { steps: 100 });
|
||||
await page.waitForTimeout(300);
|
||||
await page.locator('tr', { hasText: manualNodeName }).getByLabel('action-Action.Link-View-view-').click();
|
||||
await page.locator('.ant-list', { hasText: manualNodeName }).getByLabel('action-Action.Link-View-view-').click();
|
||||
await expect(page.getByText(triggerNodeCollectionRecordOne)).toBeAttached();
|
||||
// 4、后置处理:删除工作流
|
||||
await apiDeleteWorkflow(workflowId);
|
||||
@ -301,11 +301,11 @@ test.describe('field data', () => {
|
||||
await page.getByRole('menuitem', { name: 'check-square Workflow todos' }).click();
|
||||
await page.mouse.move(300, 0, { steps: 100 });
|
||||
await page.waitForTimeout(300);
|
||||
await page.locator('tr', { hasText: manualNodeName }).getByLabel('action-Action.Link-View-view-').click();
|
||||
await page.locator('.ant-list', { hasText: manualNodeName }).getByLabel('action-Action.Link-View-view-').click();
|
||||
// await expect(page.getByText('8')).toBeAttached();
|
||||
await expect(
|
||||
page
|
||||
.getByLabel(`block-item-CardItem-users_jobs-${preAggregateNodeTitle}`)
|
||||
.getByLabel(`block-item-CardItem-workflowManualTasks-${preAggregateNodeTitle}`)
|
||||
.locator('.ant-card-body')
|
||||
.getByText('8'),
|
||||
).toBeAttached();
|
||||
|
@ -174,18 +174,22 @@ test.describe('field data', () => {
|
||||
await page.getByRole('menuitem', { name: 'check-square Workflow todos' }).click();
|
||||
await page.mouse.move(300, 0, { steps: 100 });
|
||||
await page.waitForTimeout(300);
|
||||
await page.locator('tr', { hasText: preManualNodeTitle }).getByLabel('action-Action.Link-View-view-').click();
|
||||
await page
|
||||
.locator('.ant-list', { hasText: preManualNodeTitle })
|
||||
.getByLabel('action-Action.Link-View-view-')
|
||||
.click();
|
||||
const preManualNodeRecord = triggerNodeFieldDisplayName + dayjs().format('YYYYMMDDHHmmss.SSS').toString();
|
||||
await page.getByRole('textbox').fill(preManualNodeRecord);
|
||||
await page.getByRole('button', { name: 'Continue the process' }).click();
|
||||
await page.getByLabel('action-Filter.Action-Filter-filter-users_jobs-workflow-todo').click();
|
||||
await page.getByText('Add condition', { exact: true }).click();
|
||||
await page.getByTestId('select-filter-field').click();
|
||||
await page.getByRole('menuitemcheckbox', { name: 'Task right' }).click();
|
||||
await page.getByRole('menuitemcheckbox', { name: 'Title' }).click();
|
||||
await page.getByRole('textbox').fill(manualNodeName);
|
||||
await page.waitForTimeout(300);
|
||||
await page.getByLabel('action-Filter.Action-Filter-').click();
|
||||
// await page.getByText('Add condition', { exact: true }).click();
|
||||
// await page.getByRole('button', { name: 'Task title' }).click();
|
||||
// await page.getByRole('menuitemcheckbox', { name: 'Task title' }).click();
|
||||
await page.getByRole('textbox').first().fill(manualNodeName);
|
||||
await page.getByRole('button', { name: 'Submit' }).click();
|
||||
await page.locator('tr', { hasText: manualNodeName }).getByLabel('action-Action.Link-View-view-').click();
|
||||
await page.waitForTimeout(300);
|
||||
await page.locator('.ant-list', { hasText: manualNodeName }).getByLabel('action-Action.Link-View-view-').click();
|
||||
await expect(page.getByText(preManualNodeRecord)).toBeAttached();
|
||||
// 4、后置处理:删除工作流
|
||||
await apiDeleteWorkflow(workflowId);
|
||||
@ -330,18 +334,21 @@ test.describe('field data', () => {
|
||||
await page.getByRole('menuitem', { name: 'check-square Workflow todos' }).click();
|
||||
await page.mouse.move(300, 0, { steps: 100 });
|
||||
await page.waitForTimeout(300);
|
||||
await page.locator('tr', { hasText: preManualNodeTitle }).getByLabel('action-Action.Link-View-view-').click();
|
||||
await page
|
||||
.locator('.ant-list', { hasText: preManualNodeTitle })
|
||||
.getByLabel('action-Action.Link-View-view-')
|
||||
.click();
|
||||
const preManualNodeRecord = triggerNodeFieldDisplayName + dayjs().format('YYYYMMDDHHmmss.SSS').toString();
|
||||
await page.getByRole('textbox').fill(preManualNodeRecord);
|
||||
await page.getByRole('button', { name: 'Continue the process' }).click();
|
||||
await page.getByLabel('action-Filter.Action-Filter-filter-users_jobs-workflow-todo').click();
|
||||
await page.getByText('Add condition', { exact: true }).click();
|
||||
await page.getByTestId('select-filter-field').click();
|
||||
await page.getByRole('menuitemcheckbox', { name: 'Task right' }).click();
|
||||
await page.getByRole('menuitemcheckbox', { name: 'Title' }).click();
|
||||
await page.getByRole('textbox').fill(manualNodeName);
|
||||
await page.getByLabel('action-Filter.Action-Filter-').click();
|
||||
// await page.getByText('Add condition', { exact: true }).click();
|
||||
// await page.getByTestId('select-filter-field').click();
|
||||
// await page.getByRole('menuitemcheckbox', { name: 'Task title' }).click();
|
||||
await page.getByRole('textbox').first().fill(manualNodeName);
|
||||
await page.getByRole('button', { name: 'Submit' }).click();
|
||||
await page.locator('tr', { hasText: manualNodeName }).getByLabel('action-Action.Link-View-view-').click();
|
||||
await page.waitForTimeout(300);
|
||||
await page.locator('.ant-list', { hasText: manualNodeName }).getByLabel('action-Action.Link-View-view-').click();
|
||||
await expect(page.getByText(preManualNodeRecord)).toBeAttached();
|
||||
|
||||
const createNodeCollectionData = await apiGetList(preManualNodeCollectionName);
|
||||
@ -535,18 +542,21 @@ test.describe('field data', () => {
|
||||
await page.getByRole('menuitem', { name: 'check-square Workflow todos' }).click();
|
||||
await page.mouse.move(300, 0, { steps: 100 });
|
||||
await page.waitForTimeout(300);
|
||||
await page.locator('tr', { hasText: preManualNodeTitle }).getByLabel('action-Action.Link-View-view-').click();
|
||||
await page
|
||||
.locator('.ant-list', { hasText: preManualNodeTitle })
|
||||
.getByLabel('action-Action.Link-View-view-')
|
||||
.click();
|
||||
const preManualNodeRecord = triggerNodeFieldDisplayName + dayjs().format('YYYYMMDDHHmmss.SSS').toString();
|
||||
await page.getByRole('textbox').fill(preManualNodeRecord);
|
||||
await page.getByRole('button', { name: 'Continue the process' }).click();
|
||||
await page.getByLabel('action-Filter.Action-Filter-filter-users_jobs-workflow-todo').click();
|
||||
await page.getByText('Add condition', { exact: true }).click();
|
||||
await page.getByTestId('select-filter-field').click();
|
||||
await page.getByRole('menuitemcheckbox', { name: 'Task right' }).click();
|
||||
await page.getByRole('menuitemcheckbox', { name: 'Title' }).click();
|
||||
await page.getByRole('textbox').fill(manualNodeName);
|
||||
await page.getByLabel('action-Filter.Action-Filter-').click();
|
||||
// await page.getByText('Add condition', { exact: true }).click();
|
||||
// await page.getByTestId('select-filter-field').click();
|
||||
// await page.getByRole('menuitemcheckbox', { name: 'Task title' }).click();
|
||||
await page.getByRole('textbox').first().fill(manualNodeName);
|
||||
await page.getByRole('button', { name: 'Submit' }).click();
|
||||
await page.locator('tr', { hasText: manualNodeName }).getByLabel('action-Action.Link-View-view-').click();
|
||||
await page.waitForTimeout(300);
|
||||
await page.locator('.ant-list', { hasText: manualNodeName }).getByLabel('action-Action.Link-View-view-').click();
|
||||
await expect(page.getByText(preManualNodeRecord)).toBeAttached();
|
||||
const filter = `pageSize=20&page=1&filter={"$and":[{"orgname":{"$eq":"${preManualNodeRecord}"}}]}`;
|
||||
const createNodeCollectionData = await apiFilterList(preManualNodeCollectionName, filter);
|
||||
|
@ -114,7 +114,7 @@ test.describe('field data', () => {
|
||||
await page.getByRole('menuitem', { name: 'check-square Workflow todos' }).click();
|
||||
await page.mouse.move(300, 0, { steps: 100 });
|
||||
await page.waitForTimeout(300);
|
||||
await page.locator('tr', { hasText: manualNodeName }).getByLabel('action-Action.Link-View-view-').click();
|
||||
await page.locator('.ant-list', { hasText: manualNodeName }).getByLabel('action-Action.Link-View-view-').click();
|
||||
await expect(page.getByText(triggerNodeCollectionRecordOne)).toBeAttached();
|
||||
// 4、后置处理:删除工作流
|
||||
await apiDeleteWorkflow(workflowId);
|
||||
@ -222,7 +222,7 @@ test.describe('field data', () => {
|
||||
await page.getByRole('menuitem', { name: 'check-square Workflow todos' }).click();
|
||||
await page.mouse.move(300, 0, { steps: 100 });
|
||||
await page.waitForTimeout(300);
|
||||
await page.locator('tr', { hasText: manualNodeName }).getByLabel('action-Action.Link-View-view-').click();
|
||||
await page.locator('.ant-list', { hasText: manualNodeName }).getByLabel('action-Action.Link-View-view-').click();
|
||||
await expect(page.getByText(triggerNodeCollectionRecordOne)).toBeAttached();
|
||||
// 4、后置处理:删除工作流
|
||||
await apiDeleteWorkflow(workflowId);
|
||||
@ -341,7 +341,7 @@ test.describe('field data', () => {
|
||||
await page.getByRole('menuitem', { name: 'check-square Workflow todos' }).click();
|
||||
await page.mouse.move(300, 0, { steps: 100 });
|
||||
await page.waitForTimeout(300);
|
||||
await page.locator('tr', { hasText: manualNodeName }).getByLabel('action-Action.Link-View-view-').click();
|
||||
await page.locator('.ant-list', { hasText: manualNodeName }).getByLabel('action-Action.Link-View-view-').click();
|
||||
await expect(page.getByText(triggerNodeCollectionRecordOne)).toBeAttached();
|
||||
// 4、后置处理:删除工作流
|
||||
await apiDeleteWorkflow(workflowId);
|
||||
|
@ -181,7 +181,7 @@ test.describe('field data update', () => {
|
||||
await page.getByRole('menuitem', { name: 'check-square Workflow todos' }).click();
|
||||
await page.mouse.move(300, 0, { steps: 100 });
|
||||
await page.waitForTimeout(300);
|
||||
await page.locator('tr', { hasText: manualNodeName }).getByLabel('action-Action.Link-View-view-').click();
|
||||
await page.locator('.ant-list', { hasText: manualNodeName }).getByLabel('action-Action.Link-View-view-').click();
|
||||
const manualNodeRecord = faker.number.float({ min: 0, max: 100, precision: 2 });
|
||||
await page.getByRole('spinbutton').fill(manualNodeRecord.toString());
|
||||
await page.getByRole('button', { name: 'Continue the process' }).click();
|
||||
@ -360,7 +360,7 @@ test.describe('field data update', () => {
|
||||
await page.getByRole('menuitem', { name: 'check-square Workflow todos' }).click();
|
||||
await page.mouse.move(300, 0, { steps: 100 });
|
||||
await page.waitForTimeout(300);
|
||||
await page.locator('tr', { hasText: manualNodeName }).getByLabel('action-Action.Link-View-view-').click();
|
||||
await page.locator('.ant-list', { hasText: manualNodeName }).getByLabel('action-Action.Link-View-view-').click();
|
||||
// const manualNodeRecord = faker.number.float({ min: 0, max: 100, precision: 2 });
|
||||
await page.getByRole('checkbox').check();
|
||||
await page.getByRole('button', { name: 'Continue the process' }).click();
|
||||
@ -539,7 +539,7 @@ test.describe('field data update', () => {
|
||||
await page.getByRole('menuitem', { name: 'check-square Workflow todos' }).click();
|
||||
await page.mouse.move(300, 0, { steps: 100 });
|
||||
await page.waitForTimeout(300);
|
||||
await page.locator('tr', { hasText: manualNodeName }).getByLabel('action-Action.Link-View-view-').click();
|
||||
await page.locator('.ant-list', { hasText: manualNodeName }).getByLabel('action-Action.Link-View-view-').click();
|
||||
// const manualNodeRecord = faker.number.float({ min: 0, max: 100, precision: 2 });
|
||||
await page.getByTestId('select-single').click();
|
||||
await page.getByRole('option', { name: '存续' }).click();
|
||||
@ -719,7 +719,7 @@ test.describe('field data update', () => {
|
||||
await page.getByRole('menuitem', { name: 'check-square Workflow todos' }).click();
|
||||
await page.mouse.move(300, 0, { steps: 100 });
|
||||
await page.waitForTimeout(300);
|
||||
await page.locator('tr', { hasText: manualNodeName }).getByLabel('action-Action.Link-View-view-').click();
|
||||
await page.locator('.ant-list', { hasText: manualNodeName }).getByLabel('action-Action.Link-View-view-').click();
|
||||
// const manualNodeRecord = faker.number.float({ min: 0, max: 100, precision: 2 });
|
||||
await page.getByTestId('select-multiple').click();
|
||||
await page.getByRole('option', { name: '软件销售', exact: true }).click();
|
||||
|
@ -173,7 +173,7 @@ test.describe('field data update', () => {
|
||||
await page.getByRole('menuitem', { name: 'check-square Workflow todos' }).click();
|
||||
await page.mouse.move(300, 0, { steps: 100 });
|
||||
await page.waitForTimeout(300);
|
||||
await page.locator('tr', { hasText: manualNodeName }).getByLabel('action-Action.Link-View-view-').click();
|
||||
await page.locator('.ant-list', { hasText: manualNodeName }).getByLabel('action-Action.Link-View-view-').click();
|
||||
// const manualNodeRecord = faker.number.float({ min: 0, max: 100, precision: 2 });
|
||||
await page.getByLabel('存续').check();
|
||||
await page.getByRole('button', { name: 'Continue the process' }).click();
|
||||
@ -352,7 +352,7 @@ test.describe('field data update', () => {
|
||||
await page.getByRole('menuitem', { name: 'check-square Workflow todos' }).click();
|
||||
await page.mouse.move(300, 0, { steps: 100 });
|
||||
await page.waitForTimeout(300);
|
||||
await page.locator('tr', { hasText: manualNodeName }).getByLabel('action-Action.Link-View-view-').click();
|
||||
await page.locator('.ant-list', { hasText: manualNodeName }).getByLabel('action-Action.Link-View-view-').click();
|
||||
await page.getByLabel('软件销售', { exact: true }).check();
|
||||
await page.getByLabel('软件开发', { exact: true }).check();
|
||||
await page.getByRole('button', { name: 'Continue the process' }).click();
|
||||
@ -537,7 +537,7 @@ test.describe('field data update', () => {
|
||||
await page.getByRole('menuitem', { name: 'check-square Workflow todos' }).click();
|
||||
await page.mouse.move(300, 0, { steps: 100 });
|
||||
await page.waitForTimeout(300);
|
||||
await page.locator('tr', { hasText: manualNodeName }).getByLabel('action-Action.Link-View-view-').click();
|
||||
await page.locator('.ant-list', { hasText: manualNodeName }).getByLabel('action-Action.Link-View-view-').click();
|
||||
const manualNodeRecord = dayjs().format('YYYY-MM-DD');
|
||||
await page.getByPlaceholder('Select date').click();
|
||||
await page.getByTitle(manualNodeRecord.toString()).click();
|
||||
|
@ -157,7 +157,7 @@ test.describe('field data update', () => {
|
||||
await page.getByRole('menuitem', { name: 'check-square Workflow todos' }).click();
|
||||
await page.mouse.move(300, 0, { steps: 100 });
|
||||
await page.waitForTimeout(300);
|
||||
await page.locator('tr', { hasText: manualNodeName }).getByLabel('action-Action.Link-View-view-').click();
|
||||
await page.locator('.ant-list', { hasText: manualNodeName }).getByLabel('action-Action.Link-View-view-').click();
|
||||
const manualNodeRecord = manualNodeFieldDisplayName + dayjs().format('YYYYMMDDHHmmss.SSS').toString();
|
||||
await page.getByRole('textbox').fill(manualNodeRecord);
|
||||
await page.getByRole('button', { name: 'Continue the process' }).click();
|
||||
@ -320,7 +320,7 @@ test.describe('field data update', () => {
|
||||
await page.getByRole('menuitem', { name: 'check-square Workflow todos' }).click();
|
||||
await page.mouse.move(300, 0, { steps: 100 });
|
||||
await page.waitForTimeout(300);
|
||||
await page.locator('tr', { hasText: manualNodeName }).getByLabel('action-Action.Link-View-view-').click();
|
||||
await page.locator('.ant-list', { hasText: manualNodeName }).getByLabel('action-Action.Link-View-view-').click();
|
||||
const manualNodeRecord = manualNodeFieldDisplayName + dayjs().format('YYYYMMDDHHmmss.SSS').toString();
|
||||
await page.getByRole('textbox').fill(manualNodeRecord);
|
||||
await page.getByRole('button', { name: 'Continue the process' }).click();
|
||||
@ -483,7 +483,7 @@ test.describe('field data update', () => {
|
||||
await page.getByRole('menuitem', { name: 'check-square Workflow todos' }).click();
|
||||
await page.mouse.move(300, 0, { steps: 100 });
|
||||
await page.waitForTimeout(300);
|
||||
await page.locator('tr', { hasText: manualNodeName }).getByLabel('action-Action.Link-View-view-').click();
|
||||
await page.locator('.ant-list', { hasText: manualNodeName }).getByLabel('action-Action.Link-View-view-').click();
|
||||
const manualNodeRecord = manualNodeFieldDisplayName + dayjs().format('YYYYMMDDHHmmss.SSS').toString();
|
||||
await page.getByRole('textbox').fill(manualNodeRecord);
|
||||
await page.getByRole('button', { name: 'Continue the process' }).click();
|
||||
|
@ -157,7 +157,7 @@ test.describe('field data update', () => {
|
||||
await page.getByRole('menuitem', { name: 'check-square Workflow todos' }).click();
|
||||
await page.mouse.move(300, 0, { steps: 100 });
|
||||
await page.waitForTimeout(300);
|
||||
await page.locator('tr', { hasText: manualNodeName }).getByLabel('action-Action.Link-View-view-').click();
|
||||
await page.locator('.ant-list', { hasText: manualNodeName }).getByLabel('action-Action.Link-View-view-').click();
|
||||
const manualNodeRecord = faker.internet.email();
|
||||
await page.getByRole('textbox').fill(manualNodeRecord);
|
||||
await page.getByRole('button', { name: 'Continue the process' }).click();
|
||||
@ -320,7 +320,7 @@ test.describe('field data update', () => {
|
||||
await page.getByRole('menuitem', { name: 'check-square Workflow todos' }).click();
|
||||
await page.mouse.move(300, 0, { steps: 100 });
|
||||
await page.waitForTimeout(300);
|
||||
await page.locator('tr', { hasText: manualNodeName }).getByLabel('action-Action.Link-View-view-').click();
|
||||
await page.locator('.ant-list', { hasText: manualNodeName }).getByLabel('action-Action.Link-View-view-').click();
|
||||
const manualNodeRecord = faker.number.int();
|
||||
await page.getByRole('spinbutton').fill(manualNodeRecord.toString());
|
||||
await page.getByRole('button', { name: 'Continue the process' }).click();
|
||||
@ -483,7 +483,7 @@ test.describe('field data update', () => {
|
||||
await page.getByRole('menuitem', { name: 'check-square Workflow todos' }).click();
|
||||
await page.mouse.move(300, 0, { steps: 100 });
|
||||
await page.waitForTimeout(300);
|
||||
await page.locator('tr', { hasText: manualNodeName }).getByLabel('action-Action.Link-View-view-').click();
|
||||
await page.locator('.ant-list', { hasText: manualNodeName }).getByLabel('action-Action.Link-View-view-').click();
|
||||
const manualNodeRecord = faker.number.float();
|
||||
await page.getByRole('spinbutton').fill(manualNodeRecord.toString());
|
||||
await page.getByRole('button', { name: 'Continue the process' }).click();
|
||||
|
@ -121,12 +121,12 @@ test('filter task node', async ({ page, mockPage, mockCollections, mockRecords }
|
||||
await page.getByRole('menuitem', { name: 'check-square Workflow todos' }).click();
|
||||
await page.mouse.move(300, 0, { steps: 100 });
|
||||
await page.waitForTimeout(300);
|
||||
await page.getByLabel('action-Filter.Action-Filter-filter-users_jobs-workflow-todo').click();
|
||||
await page.getByText('Add condition', { exact: true }).click();
|
||||
await page.getByTestId('select-filter-field').click();
|
||||
await page.getByRole('menuitemcheckbox', { name: 'Task right' }).click();
|
||||
await page.getByRole('menuitemcheckbox', { name: 'Title' }).click();
|
||||
await page.getByRole('textbox').fill(manualNodeName);
|
||||
await page.getByLabel('action-Filter.Action-Filter-filter-').click();
|
||||
// await page.getByText('Add condition', { exact: true }).click();
|
||||
// await page.getByTestId('select-filter-field').click();
|
||||
// await page.getByRole('menuitemcheckbox', { name: 'Task right' }).click();
|
||||
// await page.getByRole('menuitemcheckbox', { name: 'Title' }).click();
|
||||
await page.getByRole('textbox').first().fill(manualNodeName);
|
||||
await page.getByRole('button', { name: 'Submit' }).click();
|
||||
|
||||
// 3、预期结果:列表中出现筛选的工作流
|
||||
@ -235,12 +235,12 @@ test('filter workflow name', async ({ page, mockPage, mockCollections, mockRecor
|
||||
await page.getByRole('menuitem', { name: 'check-square Workflow todos' }).click();
|
||||
await page.mouse.move(300, 0, { steps: 100 });
|
||||
await page.waitForTimeout(300);
|
||||
await page.getByLabel('action-Filter.Action-Filter-filter-users_jobs-workflow-todo').click();
|
||||
await page.getByText('Add condition', { exact: true }).click();
|
||||
await page.getByTestId('select-filter-field').click();
|
||||
await page.getByRole('menuitemcheckbox', { name: 'Workflow right' }).click();
|
||||
await page.getByRole('menuitemcheckbox', { name: 'Name' }).click();
|
||||
await page.getByRole('textbox').fill(workFlowName);
|
||||
await page.getByLabel('action-Filter.Action-Filter-filter-').click();
|
||||
// await page.getByText('Add condition', { exact: true }).click();
|
||||
// await page.getByTestId('select-filter-field').click();
|
||||
// await page.getByRole('menuitemcheckbox', { name: 'Workflow right' }).click();
|
||||
// await page.getByRole('menuitemcheckbox', { name: 'Name' }).click();
|
||||
await page.getByRole('textbox').last().fill(workFlowName);
|
||||
await page.getByRole('button', { name: 'Submit' }).click();
|
||||
|
||||
// 3、预期结果:列表中出现筛选的工作流
|
||||
|
@ -13,8 +13,8 @@ import WorkflowPlugin from '@nocobase/plugin-workflow/client';
|
||||
import Manual from './instruction';
|
||||
|
||||
import { NAMESPACE } from '../locale';
|
||||
import { useCountRequest, WorkflowManualProvider } from './WorkflowManualProvider';
|
||||
import { WorkflowTodo } from './WorkflowTodo';
|
||||
import { WorkflowManualProvider } from './WorkflowManualProvider';
|
||||
import { manualTodo, WorkflowTodo } from './WorkflowTodo';
|
||||
import {
|
||||
addActionButton,
|
||||
addActionButton_deprecated,
|
||||
@ -22,6 +22,7 @@ import {
|
||||
addBlockButton_deprecated,
|
||||
} from './instruction/SchemaConfig';
|
||||
import { addCustomFormField, addCustomFormField_deprecated } from './instruction/forms/custom';
|
||||
import { MANUAL_TASK_TYPE } from '../common/constants';
|
||||
|
||||
export default class extends Plugin {
|
||||
async afterAdd() {
|
||||
@ -37,11 +38,7 @@ export default class extends Plugin {
|
||||
const workflow = this.app.pm.get('workflow') as WorkflowPlugin;
|
||||
workflow.registerInstruction('manual', Manual);
|
||||
|
||||
workflow.registerTaskType('manual', {
|
||||
title: `{{t("My manual tasks", { ns: "${NAMESPACE}" })}}`,
|
||||
useCountRequest,
|
||||
component: WorkflowTodo.TaskBlock,
|
||||
});
|
||||
workflow.registerTaskType(MANUAL_TASK_TYPE, manualTodo);
|
||||
|
||||
this.app.schemaInitializerManager.add(addBlockButton_deprecated);
|
||||
this.app.schemaInitializerManager.add(addBlockButton);
|
||||
|
@ -445,13 +445,13 @@ export function SchemaConfig({ value, onChange }) {
|
||||
type: 'void',
|
||||
title: `{{t("User interface", { ns: "${NAMESPACE}" })}}`,
|
||||
'x-decorator': 'Form',
|
||||
'x-component': 'Action.Drawer',
|
||||
'x-component': 'Action.Container',
|
||||
'x-component-props': {
|
||||
className: css`
|
||||
.ant-drawer-body {
|
||||
background: var(--nb-box-bg);
|
||||
}
|
||||
`,
|
||||
// className: css`
|
||||
// .ant-drawer-body {
|
||||
// background: var(--nb-box-bg);
|
||||
// }
|
||||
// `,
|
||||
// Using ref to call refresh ensures accessing the latest refresh function
|
||||
onClose: () => refreshRef.current(),
|
||||
},
|
||||
|
@ -18,6 +18,7 @@ import {
|
||||
CollectionBlockInitializer,
|
||||
Instruction,
|
||||
WorkflowVariableTextArea,
|
||||
useNodeContext,
|
||||
} from '@nocobase/plugin-workflow/client';
|
||||
|
||||
import { SchemaConfig, SchemaConfigButton } from './SchemaConfig';
|
||||
@ -149,6 +150,7 @@ export default class extends Instruction {
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'WorkflowVariableTextArea',
|
||||
description: `{{t("Title of each task item. Default to node title.", { ns: "${NAMESPACE}" })}}`,
|
||||
default: '{{useNodeContext().title}}',
|
||||
},
|
||||
schema: {
|
||||
type: 'void',
|
||||
@ -168,6 +170,9 @@ export default class extends Instruction {
|
||||
default: {},
|
||||
},
|
||||
};
|
||||
scope = {
|
||||
useNodeContext,
|
||||
};
|
||||
components = {
|
||||
SchemaConfigButton,
|
||||
SchemaConfig,
|
||||
|
@ -0,0 +1,41 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export const NAMESPACE = 'workflow-manual';
|
||||
|
||||
export const MANUAL_TASK_TYPE = 'manual';
|
||||
|
||||
export const TASK_STATUS = {
|
||||
PENDING: 0,
|
||||
RESOLVED: 1,
|
||||
REJECTED: -1,
|
||||
};
|
||||
|
||||
export const TaskStatusOptions = [
|
||||
{
|
||||
value: TASK_STATUS.PENDING,
|
||||
label: `{{t("Pending", { ns: "workflow" })}}`,
|
||||
color: 'gold',
|
||||
},
|
||||
{
|
||||
value: TASK_STATUS.RESOLVED,
|
||||
label: `{{t("Resolved", { ns: "workflow" })}}`,
|
||||
color: 'green',
|
||||
},
|
||||
{
|
||||
value: TASK_STATUS.REJECTED,
|
||||
label: `{{t("Rejected", { ns: "workflow" })}}`,
|
||||
color: 'red',
|
||||
},
|
||||
];
|
||||
|
||||
export const TaskStatusOptionsMap = TaskStatusOptions.reduce(
|
||||
(map, item) => Object.assign(map, { [item.value]: item }),
|
||||
{},
|
||||
);
|
@ -8,8 +8,9 @@
|
||||
*/
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { NAMESPACE } from '../common/constants';
|
||||
|
||||
export const NAMESPACE = 'workflow-manual';
|
||||
export { NAMESPACE };
|
||||
|
||||
export function useLang(key: string, options = {}) {
|
||||
const { t } = usePluginTranslation(options);
|
||||
|
@ -118,9 +118,9 @@ export default class extends Instruction {
|
||||
}
|
||||
const title = config.title ? processor.getParsedValue(config.title, node.id) : node.title;
|
||||
// NOTE: batch create users jobs
|
||||
const UserJobModel = this.workflow.app.db.getModel('users_jobs');
|
||||
await UserJobModel.bulkCreate(
|
||||
assignees.map((userId) => ({
|
||||
const TaskRepo = this.workflow.app.db.getRepository('workflowManualTasks');
|
||||
await TaskRepo.createMany({
|
||||
records: assignees.map((userId) => ({
|
||||
userId,
|
||||
jobId: job.id,
|
||||
nodeId: node.id,
|
||||
@ -129,10 +129,8 @@ export default class extends Instruction {
|
||||
status: JOB_STATUS.PENDING,
|
||||
title,
|
||||
})),
|
||||
{
|
||||
transaction: processor.mainTransaction,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
return job;
|
||||
}
|
||||
@ -141,15 +139,15 @@ export default class extends Instruction {
|
||||
// NOTE: check all users jobs related if all done then continue as parallel
|
||||
const { mode } = node.config as ManualConfig;
|
||||
|
||||
const UserJobRepo = this.workflow.app.db.getRepository('users_jobs');
|
||||
const jobs = await UserJobRepo.find({
|
||||
const TaskRepo = this.workflow.app.db.getRepository('workflowManualTasks');
|
||||
const tasks = await TaskRepo.find({
|
||||
where: {
|
||||
jobId: job.id,
|
||||
},
|
||||
transaction: processor.mainTransaction,
|
||||
});
|
||||
const assignees = [];
|
||||
const distributionMap = jobs.reduce((result, item) => {
|
||||
const distributionMap = tasks.reduce((result, item) => {
|
||||
if (result[item.status] == null) {
|
||||
result[item.status] = 0;
|
||||
}
|
||||
@ -162,9 +160,9 @@ export default class extends Instruction {
|
||||
count: distributionMap[status],
|
||||
}));
|
||||
|
||||
const submitted = jobs.reduce((count, item) => (item.status !== JOB_STATUS.PENDING ? count + 1 : count), 0);
|
||||
const submitted = tasks.reduce((count, item) => (item.status !== JOB_STATUS.PENDING ? count + 1 : count), 0);
|
||||
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.latestTask?.result ?? job.result;
|
||||
processor.logger.debug(`manual resume job and next status: ${status}`);
|
||||
job.set({
|
||||
status,
|
||||
|
@ -15,11 +15,20 @@ import WorkflowPlugin, { JOB_STATUS } from '@nocobase/plugin-workflow';
|
||||
import * as jobActions from './actions';
|
||||
|
||||
import ManualInstruction from './ManualInstruction';
|
||||
import { MANUAL_TASK_TYPE } from '../common/constants';
|
||||
|
||||
interface WorkflowManualTaskModel {
|
||||
id: number;
|
||||
userId: number;
|
||||
workflowId: number;
|
||||
executionId: number;
|
||||
status: number;
|
||||
}
|
||||
|
||||
export default class extends Plugin {
|
||||
async load() {
|
||||
this.app.resourceManager.define({
|
||||
name: 'users_jobs',
|
||||
name: 'workflowManualTasks',
|
||||
actions: {
|
||||
list: {
|
||||
filter: {
|
||||
@ -41,9 +50,22 @@ export default class extends Plugin {
|
||||
},
|
||||
});
|
||||
|
||||
this.app.acl.allow('users_jobs', ['list', 'get', 'submit', 'countMine'], 'loggedIn');
|
||||
this.app.acl.allow('workflowManualTasks', ['list', 'get', 'submit'], 'loggedIn');
|
||||
|
||||
const workflowPlugin = this.app.pm.get(WorkflowPlugin) as WorkflowPlugin;
|
||||
workflowPlugin.registerInstruction('manual', ManualInstruction);
|
||||
|
||||
this.db.on('workflowManualTasks.afterSave', async (task: WorkflowManualTaskModel, options) => {
|
||||
await workflowPlugin.toggleTaskStatus(
|
||||
{
|
||||
type: MANUAL_TASK_TYPE,
|
||||
key: `${task.id}`,
|
||||
userId: task.userId,
|
||||
workflowId: task.workflowId,
|
||||
},
|
||||
Boolean(task.status),
|
||||
options,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -37,7 +37,7 @@ describe('workflow > instructions > manual > assignees', () => {
|
||||
PostRepo = db.getCollection('posts').repository;
|
||||
CommentRepo = db.getCollection('comments').repository;
|
||||
UserModel = db.getCollection('users').model;
|
||||
UserJobModel = db.getModel('users_jobs');
|
||||
UserJobModel = db.getModel('workflowManualTasks');
|
||||
|
||||
users = await UserModel.bulkCreate([
|
||||
{ id: 2, nickname: 'a' },
|
||||
@ -117,13 +117,13 @@ describe('workflow > instructions > manual > assignees', () => {
|
||||
expect(usersJobs[0].userId).toBe(users[0].id);
|
||||
expect(usersJobs[0].jobId).toBe(j1.id);
|
||||
|
||||
const res1 = await agent.resource('users_jobs').submit({
|
||||
const res1 = await agent.resource('workflowManualTasks').submit({
|
||||
filterByTk: usersJobs[0].id,
|
||||
values: { result: { f1: {}, _: 'resolve' } },
|
||||
});
|
||||
expect(res1.status).toBe(401);
|
||||
|
||||
const res2 = await userAgents[1].resource('users_jobs').submit({
|
||||
const res2 = await userAgents[1].resource('workflowManualTasks').submit({
|
||||
filterByTk: usersJobs[0].id,
|
||||
values: {
|
||||
result: { f1: {}, _: 'resolve' },
|
||||
@ -131,7 +131,7 @@ describe('workflow > instructions > manual > assignees', () => {
|
||||
});
|
||||
expect(res2.status).toBe(403);
|
||||
|
||||
const res3 = await userAgents[0].resource('users_jobs').submit({
|
||||
const res3 = await userAgents[0].resource('workflowManualTasks').submit({
|
||||
filterByTk: usersJobs[0].id,
|
||||
values: {
|
||||
result: { f1: { a: 1 }, _: 'resolve' },
|
||||
@ -150,7 +150,7 @@ describe('workflow > instructions > manual > assignees', () => {
|
||||
expect(usersJobsAfter[0].status).toBe(JOB_STATUS.RESOLVED);
|
||||
expect(usersJobsAfter[0].result).toEqual({ f1: { a: 1 }, _: 'resolve' });
|
||||
|
||||
const res4 = await userAgents[0].resource('users_jobs').submit({
|
||||
const res4 = await userAgents[0].resource('workflowManualTasks').submit({
|
||||
filterByTk: usersJobs[0].id,
|
||||
values: {
|
||||
result: { f1: { a: 2 }, _: 'resolve' },
|
||||
|
@ -37,7 +37,7 @@ describe('workflow > instructions > manual', () => {
|
||||
PostRepo = db.getCollection('posts').repository;
|
||||
AnotherPostRepo = app.dataSourceManager.dataSources.get('another').collectionManager.getRepository('posts');
|
||||
UserModel = db.getCollection('users').model;
|
||||
UserJobModel = db.getModel('users_jobs');
|
||||
UserJobModel = db.getModel('workflowManualTasks');
|
||||
|
||||
users = await UserModel.bulkCreate([
|
||||
{ id: 2, nickname: 'a' },
|
||||
@ -80,13 +80,13 @@ describe('workflow > instructions > manual', () => {
|
||||
|
||||
await sleep(500);
|
||||
|
||||
const UserJobModel = db.getModel('users_jobs');
|
||||
const UserJobModel = db.getModel('workflowManualTasks');
|
||||
const pendingJobs = await UserJobModel.findAll({
|
||||
order: [['userId', 'ASC']],
|
||||
});
|
||||
expect(pendingJobs.length).toBe(1);
|
||||
|
||||
const res1 = await userAgents[0].resource('users_jobs').submit({
|
||||
const res1 = await userAgents[0].resource('workflowManualTasks').submit({
|
||||
filterByTk: pendingJobs[0].get('id'),
|
||||
values: {
|
||||
result: { f1: { title: 't1' }, _: 'resolve' },
|
||||
@ -130,13 +130,13 @@ describe('workflow > instructions > manual', () => {
|
||||
|
||||
await sleep(500);
|
||||
|
||||
const UserJobModel = db.getModel('users_jobs');
|
||||
const UserJobModel = db.getModel('workflowManualTasks');
|
||||
const pendingJobs = await UserJobModel.findAll({
|
||||
order: [['userId', 'ASC']],
|
||||
});
|
||||
expect(pendingJobs.length).toBe(1);
|
||||
|
||||
const res1 = await userAgents[0].resource('users_jobs').submit({
|
||||
const res1 = await userAgents[0].resource('workflowManualTasks').submit({
|
||||
filterByTk: pendingJobs[0].get('id'),
|
||||
values: {
|
||||
result: { f1: { title: 't1' }, _: 'pending' },
|
||||
@ -155,7 +155,7 @@ describe('workflow > instructions > manual', () => {
|
||||
const c1 = await AnotherPostRepo.find();
|
||||
expect(c1.length).toBe(0);
|
||||
|
||||
const res2 = await userAgents[0].resource('users_jobs').submit({
|
||||
const res2 = await userAgents[0].resource('workflowManualTasks').submit({
|
||||
filterByTk: pendingJobs[0].get('id'),
|
||||
values: {
|
||||
result: { f1: { title: 't2' }, _: 'resolve' },
|
||||
@ -201,13 +201,13 @@ describe('workflow > instructions > manual', () => {
|
||||
|
||||
await sleep(500);
|
||||
|
||||
const UserJobModel = db.getModel('users_jobs');
|
||||
const UserJobModel = db.getModel('workflowManualTasks');
|
||||
const pendingJobs = await UserJobModel.findAll({
|
||||
order: [['userId', 'ASC']],
|
||||
});
|
||||
expect(pendingJobs.length).toBe(1);
|
||||
|
||||
const res1 = await userAgents[0].resource('users_jobs').submit({
|
||||
const res1 = await userAgents[0].resource('workflowManualTasks').submit({
|
||||
filterByTk: pendingJobs[0].get('id'),
|
||||
values: {
|
||||
result: { f1: { title: 't2' }, _: 'resolve' },
|
||||
|
@ -37,7 +37,7 @@ describe('workflow > instructions > manual', () => {
|
||||
PostRepo = db.getCollection('posts').repository;
|
||||
CommentRepo = db.getCollection('comments').repository;
|
||||
UserModel = db.getCollection('users').model;
|
||||
UserJobModel = db.getModel('users_jobs');
|
||||
UserJobModel = db.getModel('workflowManualTasks');
|
||||
|
||||
users = await UserModel.bulkCreate([
|
||||
{ id: 2, nickname: 'a' },
|
||||
@ -85,7 +85,7 @@ describe('workflow > instructions > manual', () => {
|
||||
expect(usersJobs[0].userId).toBe(users[0].id);
|
||||
expect(usersJobs[0].jobId).toBe(j1.id);
|
||||
|
||||
const res1 = await userAgents[0].resource('users_jobs').submit({
|
||||
const res1 = await userAgents[0].resource('workflowManualTasks').submit({
|
||||
filterByTk: usersJobs[0].id,
|
||||
values: {
|
||||
result: { f1: { a: 1 }, _: 'resolve' },
|
||||
@ -123,7 +123,7 @@ describe('workflow > instructions > manual', () => {
|
||||
expect(usersJobs[0].userId).toBe(users[0].id);
|
||||
expect(usersJobs[0].jobId).toBe(j1.id);
|
||||
|
||||
const res1 = await userAgents[0].resource('users_jobs').submit({
|
||||
const res1 = await userAgents[0].resource('workflowManualTasks').submit({
|
||||
filterByTk: usersJobs[0].id,
|
||||
values: {
|
||||
result: { f1: { a: 1 } },
|
||||
@ -167,7 +167,7 @@ describe('workflow > instructions > manual', () => {
|
||||
expect(usersJobs[0].userId).toBe(users[0].id);
|
||||
expect(usersJobs[0].jobId).toBe(j1.id);
|
||||
|
||||
const res1 = await userAgents[0].resource('users_jobs').submit({
|
||||
const res1 = await userAgents[0].resource('workflowManualTasks').submit({
|
||||
filterByTk: usersJobs[0].id,
|
||||
values: {
|
||||
result: { f1: { a: 1 }, _: 'resolve' },
|
||||
@ -217,7 +217,7 @@ describe('workflow > instructions > manual', () => {
|
||||
expect(usersJobs[0].userId).toBe(users[0].id);
|
||||
expect(usersJobs[0].jobId).toBe(j1.id);
|
||||
|
||||
const res1 = await userAgents[0].resource('users_jobs').submit({
|
||||
const res1 = await userAgents[0].resource('workflowManualTasks').submit({
|
||||
filterByTk: usersJobs[0].id,
|
||||
values: {
|
||||
result: { f1: { a: 1 }, _: 'reject' },
|
||||
@ -267,7 +267,7 @@ describe('workflow > instructions > manual', () => {
|
||||
expect(usersJobs[0].userId).toBe(users[0].id);
|
||||
expect(usersJobs[0].jobId).toBe(j1.id);
|
||||
|
||||
const res1 = await userAgents[0].resource('users_jobs').submit({
|
||||
const res1 = await userAgents[0].resource('workflowManualTasks').submit({
|
||||
filterByTk: usersJobs[0].id,
|
||||
values: {
|
||||
result: { f1: { a: 1 }, _: 'save' },
|
||||
@ -323,7 +323,7 @@ describe('workflow > instructions > manual', () => {
|
||||
expect(usersJobs[0].jobId).toBe(j1.id);
|
||||
|
||||
const now = new Date();
|
||||
const res1 = await userAgents[0].resource('users_jobs').submit({
|
||||
const res1 = await userAgents[0].resource('workflowManualTasks').submit({
|
||||
filterByTk: usersJobs[0].id,
|
||||
values: {
|
||||
result: { f1: { a: 2, id: 3 }, _: 'resolve' },
|
||||
@ -371,13 +371,13 @@ describe('workflow > instructions > manual', () => {
|
||||
|
||||
await sleep(500);
|
||||
|
||||
const UserJobModel = db.getModel('users_jobs');
|
||||
const UserJobModel = db.getModel('workflowManualTasks');
|
||||
const pendingJobs = await UserJobModel.findAll({
|
||||
order: [['userId', 'ASC']],
|
||||
});
|
||||
expect(pendingJobs.length).toBe(2);
|
||||
|
||||
const res1 = await userAgents[0].resource('users_jobs').submit({
|
||||
const res1 = await userAgents[0].resource('workflowManualTasks').submit({
|
||||
filterByTk: pendingJobs[0].get('id'),
|
||||
values: {
|
||||
result: { f1: { number: 1 }, _: 'resolve' },
|
||||
@ -420,13 +420,13 @@ describe('workflow > instructions > manual', () => {
|
||||
|
||||
await sleep(500);
|
||||
|
||||
const UserJobModel = db.getModel('users_jobs');
|
||||
const UserJobModel = db.getModel('workflowManualTasks');
|
||||
const pendingJobs = await UserJobModel.findAll({
|
||||
order: [['userId', 'ASC']],
|
||||
});
|
||||
expect(pendingJobs.length).toBe(2);
|
||||
|
||||
const res1 = await userAgents[0].resource('users_jobs').submit({
|
||||
const res1 = await userAgents[0].resource('workflowManualTasks').submit({
|
||||
filterByTk: pendingJobs[0].get('id'),
|
||||
values: {
|
||||
result: { f1: { number: 1 }, _: 'pending' },
|
||||
@ -442,7 +442,7 @@ describe('workflow > instructions > manual', () => {
|
||||
expect(j1.status).toBe(JOB_STATUS.PENDING);
|
||||
expect(j1.result).toMatchObject({ f1: { number: 1 } });
|
||||
|
||||
const res2 = await userAgents[0].resource('users_jobs').submit({
|
||||
const res2 = await userAgents[0].resource('workflowManualTasks').submit({
|
||||
filterByTk: pendingJobs[0].get('id'),
|
||||
values: {
|
||||
result: { f2: { number: 2 }, _: 'pending' },
|
||||
@ -461,7 +461,7 @@ describe('workflow > instructions > manual', () => {
|
||||
f2: { number: 2 },
|
||||
});
|
||||
|
||||
const res3 = await userAgents[0].resource('users_jobs').submit({
|
||||
const res3 = await userAgents[0].resource('workflowManualTasks').submit({
|
||||
filterByTk: pendingJobs[0].get('id'),
|
||||
values: {
|
||||
result: { f2: { number: 3 }, _: 'resolve' },
|
||||
@ -500,13 +500,13 @@ describe('workflow > instructions > manual', () => {
|
||||
|
||||
await sleep(500);
|
||||
|
||||
const UserJobModel = db.getModel('users_jobs');
|
||||
const UserJobModel = db.getModel('workflowManualTasks');
|
||||
const pendingJobs = await UserJobModel.findAll({
|
||||
order: [['userId', 'ASC']],
|
||||
});
|
||||
expect(pendingJobs.length).toBe(1);
|
||||
|
||||
const res1 = await userAgents[0].resource('users_jobs').submit({
|
||||
const res1 = await userAgents[0].resource('workflowManualTasks').submit({
|
||||
filterByTk: pendingJobs[0].get('id'),
|
||||
values: {
|
||||
result: { f1: { status: 1 }, _: 'resolve' },
|
||||
@ -549,13 +549,13 @@ describe('workflow > instructions > manual', () => {
|
||||
|
||||
await sleep(500);
|
||||
|
||||
const UserJobModel = db.getModel('users_jobs');
|
||||
const UserJobModel = db.getModel('workflowManualTasks');
|
||||
const pendingJobs = await UserJobModel.findAll({
|
||||
order: [['userId', 'ASC']],
|
||||
});
|
||||
expect(pendingJobs.length).toBe(1);
|
||||
|
||||
const res1 = await userAgents[0].resource('users_jobs').submit({
|
||||
const res1 = await userAgents[0].resource('workflowManualTasks').submit({
|
||||
filterByTk: pendingJobs[0].get('id'),
|
||||
values: {
|
||||
result: { f1: { status: 1 }, _: 'pending' },
|
||||
@ -574,7 +574,7 @@ describe('workflow > instructions > manual', () => {
|
||||
const c1 = await CommentRepo.find();
|
||||
expect(c1.length).toBe(0);
|
||||
|
||||
const res2 = await userAgents[0].resource('users_jobs').submit({
|
||||
const res2 = await userAgents[0].resource('workflowManualTasks').submit({
|
||||
filterByTk: pendingJobs[0].get('id'),
|
||||
values: {
|
||||
result: { f1: { status: 1 }, _: 'resolve' },
|
||||
@ -615,13 +615,13 @@ describe('workflow > instructions > manual', () => {
|
||||
|
||||
await sleep(500);
|
||||
|
||||
const UserJobModel = db.getModel('users_jobs');
|
||||
const UserJobModel = db.getModel('workflowManualTasks');
|
||||
const pendingJobs = await UserJobModel.findAll({
|
||||
order: [['userId', 'ASC']],
|
||||
});
|
||||
expect(pendingJobs.length).toBe(1);
|
||||
|
||||
const res1 = await userAgents[0].resource('users_jobs').submit({
|
||||
const res1 = await userAgents[0].resource('workflowManualTasks').submit({
|
||||
filterByTk: pendingJobs[0].get('id'),
|
||||
values: {
|
||||
result: { f1: { title: 't2' }, _: 'resolve' },
|
||||
|
@ -37,7 +37,7 @@ describe('workflow > instructions > manual', () => {
|
||||
PostRepo = db.getCollection('posts').repository;
|
||||
CommentRepo = db.getCollection('comments').repository;
|
||||
UserModel = db.getCollection('users').model;
|
||||
UserJobModel = db.getModel('users_jobs');
|
||||
UserJobModel = db.getModel('workflowManualTasks');
|
||||
|
||||
users = await UserModel.bulkCreate([
|
||||
{ id: 2, nickname: 'a' },
|
||||
@ -87,13 +87,13 @@ describe('workflow > instructions > manual', () => {
|
||||
expect(usersJobs[0].userId).toBe(users[0].id);
|
||||
expect(usersJobs[0].jobId).toBe(j1.id);
|
||||
|
||||
const res1 = await agent.resource('users_jobs').submit({
|
||||
const res1 = await agent.resource('workflowManualTasks').submit({
|
||||
filterByTk: usersJobs[0].id,
|
||||
values: { result: { f1: {}, _: 'resolve' } },
|
||||
});
|
||||
expect(res1.status).toBe(401);
|
||||
|
||||
const res2 = await userAgents[1].resource('users_jobs').submit({
|
||||
const res2 = await userAgents[1].resource('workflowManualTasks').submit({
|
||||
filterByTk: usersJobs[0].id,
|
||||
values: {
|
||||
result: { f1: {}, _: 'resolve' },
|
||||
@ -101,7 +101,7 @@ describe('workflow > instructions > manual', () => {
|
||||
});
|
||||
expect(res2.status).toBe(403);
|
||||
|
||||
const res3 = await userAgents[0].resource('users_jobs').submit({
|
||||
const res3 = await userAgents[0].resource('workflowManualTasks').submit({
|
||||
filterByTk: usersJobs[0].id,
|
||||
values: {
|
||||
result: { f1: { a: 1 }, _: 'resolve' },
|
||||
@ -120,7 +120,7 @@ describe('workflow > instructions > manual', () => {
|
||||
expect(usersJobsAfter[0].status).toBe(JOB_STATUS.RESOLVED);
|
||||
expect(usersJobsAfter[0].result).toEqual({ f1: { a: 1 }, _: 'resolve' });
|
||||
|
||||
const res4 = await userAgents[0].resource('users_jobs').submit({
|
||||
const res4 = await userAgents[0].resource('workflowManualTasks').submit({
|
||||
filterByTk: usersJobs[0].id,
|
||||
values: {
|
||||
result: { f1: { a: 2 }, _: 'resolve' },
|
||||
@ -151,9 +151,13 @@ describe('workflow > instructions > manual', () => {
|
||||
const [j1] = await pending.getJobs();
|
||||
expect(j1.status).toBe(JOB_STATUS.PENDING);
|
||||
|
||||
const usersJobs = await j1.getUsersJobs();
|
||||
const usersJobs = await UserJobModel.findAll({
|
||||
where: {
|
||||
jobId: j1.id,
|
||||
},
|
||||
});
|
||||
|
||||
const res1 = await userAgents[1].resource('users_jobs').submit({
|
||||
const res1 = await userAgents[1].resource('workflowManualTasks').submit({
|
||||
filterByTk: usersJobs.find((item) => item.userId === users[1].id).id,
|
||||
values: {
|
||||
result: { f1: { a: 1 }, _: 'resolve' },
|
||||
@ -167,7 +171,7 @@ describe('workflow > instructions > manual', () => {
|
||||
expect(j2.status).toBe(JOB_STATUS.RESOLVED);
|
||||
expect(j2.result).toEqual({ f1: { a: 1 }, _: 'resolve' });
|
||||
|
||||
const res2 = await userAgents[0].resource('users_jobs').submit({
|
||||
const res2 = await userAgents[0].resource('workflowManualTasks').submit({
|
||||
filterByTk: usersJobs.find((item) => item.userId === users[0].id).id,
|
||||
values: {
|
||||
result: { f1: { a: 1 }, _: 'resolve' },
|
||||
@ -176,7 +180,7 @@ describe('workflow > instructions > manual', () => {
|
||||
expect(res2.status).toBe(400);
|
||||
});
|
||||
|
||||
it('also could submit to users_jobs api', async () => {
|
||||
it('also could submit to workflowManualTasks api', async () => {
|
||||
const n1 = await workflow.createNode({
|
||||
type: 'manual',
|
||||
config: {
|
||||
@ -193,13 +197,13 @@ describe('workflow > instructions > manual', () => {
|
||||
|
||||
await sleep(500);
|
||||
|
||||
const UserJobModel = db.getModel('users_jobs');
|
||||
const UserJobModel = db.getModel('workflowManualTasks');
|
||||
const usersJobs = await UserJobModel.findAll();
|
||||
expect(usersJobs.length).toBe(1);
|
||||
expect(usersJobs[0].get('status')).toBe(JOB_STATUS.PENDING);
|
||||
expect(usersJobs[0].get('userId')).toBe(users[0].id);
|
||||
|
||||
const res = await userAgents[0].resource('users_jobs').submit({
|
||||
const res = await userAgents[0].resource('workflowManualTasks').submit({
|
||||
filterByTk: usersJobs[0].get('id'),
|
||||
values: {
|
||||
result: { f1: { a: 1 }, _: 'resolve' },
|
||||
@ -236,13 +240,13 @@ describe('workflow > instructions > manual', () => {
|
||||
|
||||
await sleep(500);
|
||||
|
||||
const UserJobModel = db.getModel('users_jobs');
|
||||
const UserJobModel = db.getModel('workflowManualTasks');
|
||||
const pendingJobs = await UserJobModel.findAll({
|
||||
order: [['userId', 'ASC']],
|
||||
});
|
||||
expect(pendingJobs.length).toBe(2);
|
||||
|
||||
const res1 = await userAgents[0].resource('users_jobs').submit({
|
||||
const res1 = await userAgents[0].resource('workflowManualTasks').submit({
|
||||
filterByTk: pendingJobs[0].get('id'),
|
||||
values: {
|
||||
result: { f1: { a: 1 }, _: 'resolve' },
|
||||
@ -262,7 +266,7 @@ describe('workflow > instructions > manual', () => {
|
||||
});
|
||||
expect(usersJobs1.length).toBe(2);
|
||||
|
||||
const res2 = await userAgents[1].resource('users_jobs').submit({
|
||||
const res2 = await userAgents[1].resource('workflowManualTasks').submit({
|
||||
filterByTk: pendingJobs[1].get('id'),
|
||||
values: {
|
||||
result: { f1: { a: 2 }, _: 'resolve' },
|
||||
@ -297,13 +301,13 @@ describe('workflow > instructions > manual', () => {
|
||||
|
||||
await sleep(500);
|
||||
|
||||
const UserJobModel = db.getModel('users_jobs');
|
||||
const UserJobModel = db.getModel('workflowManualTasks');
|
||||
const pendingJobs = await UserJobModel.findAll({
|
||||
order: [['userId', 'ASC']],
|
||||
});
|
||||
expect(pendingJobs.length).toBe(2);
|
||||
|
||||
const res1 = await userAgents[0].resource('users_jobs').submit({
|
||||
const res1 = await userAgents[0].resource('workflowManualTasks').submit({
|
||||
filterByTk: pendingJobs[0].get('id'),
|
||||
values: {
|
||||
result: { f1: { a: 0 }, _: 'reject' },
|
||||
@ -323,7 +327,7 @@ describe('workflow > instructions > manual', () => {
|
||||
});
|
||||
expect(usersJobs1.length).toBe(2);
|
||||
|
||||
const res2 = await userAgents[1].resource('users_jobs').submit({
|
||||
const res2 = await userAgents[1].resource('workflowManualTasks').submit({
|
||||
filterByTk: pendingJobs[1].get('id'),
|
||||
values: {
|
||||
result: { f1: { a: 0 }, _: 'reject' },
|
||||
@ -353,13 +357,13 @@ describe('workflow > instructions > manual', () => {
|
||||
|
||||
await sleep(500);
|
||||
|
||||
const UserJobModel = db.getModel('users_jobs');
|
||||
const UserJobModel = db.getModel('workflowManualTasks');
|
||||
const pendingJobs = await UserJobModel.findAll({
|
||||
order: [['userId', 'ASC']],
|
||||
});
|
||||
expect(pendingJobs.length).toBe(2);
|
||||
|
||||
const res1 = await userAgents[0].resource('users_jobs').submit({
|
||||
const res1 = await userAgents[0].resource('workflowManualTasks').submit({
|
||||
filterByTk: pendingJobs[0].get('id'),
|
||||
values: {
|
||||
result: { f1: { a: 1 }, _: 'resolve' },
|
||||
@ -379,7 +383,7 @@ describe('workflow > instructions > manual', () => {
|
||||
});
|
||||
expect(usersJobs1.length).toBe(2);
|
||||
|
||||
const res2 = await userAgents[1].resource('users_jobs').submit({
|
||||
const res2 = await userAgents[1].resource('workflowManualTasks').submit({
|
||||
filterByTk: pendingJobs[1].get('id'),
|
||||
values: {
|
||||
result: { f1: { a: 0 }, _: 'reject' },
|
||||
@ -419,13 +423,13 @@ describe('workflow > instructions > manual', () => {
|
||||
|
||||
await sleep(500);
|
||||
|
||||
const UserJobModel = db.getModel('users_jobs');
|
||||
const UserJobModel = db.getModel('workflowManualTasks');
|
||||
const pendingJobs = await UserJobModel.findAll({
|
||||
order: [['userId', 'ASC']],
|
||||
});
|
||||
expect(pendingJobs.length).toBe(2);
|
||||
|
||||
const res1 = await userAgents[0].resource('users_jobs').submit({
|
||||
const res1 = await userAgents[0].resource('workflowManualTasks').submit({
|
||||
filterByTk: pendingJobs[0].get('id'),
|
||||
values: {
|
||||
result: { f1: { a: 1 }, _: 'resolve' },
|
||||
@ -441,7 +445,7 @@ describe('workflow > instructions > manual', () => {
|
||||
expect(j1.status).toBe(JOB_STATUS.RESOLVED);
|
||||
expect(j1.result).toBe(0.5);
|
||||
|
||||
const res2 = await userAgents[1].resource('users_jobs').submit({
|
||||
const res2 = await userAgents[1].resource('workflowManualTasks').submit({
|
||||
filterByTk: pendingJobs[1].get('id'),
|
||||
values: {
|
||||
result: { f1: { a: 0 }, _: 'reject' },
|
||||
@ -471,13 +475,13 @@ describe('workflow > instructions > manual', () => {
|
||||
|
||||
await sleep(500);
|
||||
|
||||
const UserJobModel = db.getModel('users_jobs');
|
||||
const UserJobModel = db.getModel('workflowManualTasks');
|
||||
const pendingJobs = await UserJobModel.findAll({
|
||||
order: [['userId', 'ASC']],
|
||||
});
|
||||
expect(pendingJobs.length).toBe(2);
|
||||
|
||||
const res1 = await userAgents[0].resource('users_jobs').submit({
|
||||
const res1 = await userAgents[0].resource('workflowManualTasks').submit({
|
||||
filterByTk: pendingJobs[0].get('id'),
|
||||
values: {
|
||||
result: { f1: { a: 0 }, _: 'reject' },
|
||||
@ -493,7 +497,7 @@ describe('workflow > instructions > manual', () => {
|
||||
expect(j1.status).toBe(JOB_STATUS.PENDING);
|
||||
expect(j1.result).toBe(0.5);
|
||||
|
||||
const res2 = await userAgents[1].resource('users_jobs').submit({
|
||||
const res2 = await userAgents[1].resource('workflowManualTasks').submit({
|
||||
filterByTk: pendingJobs[1].get('id'),
|
||||
values: {
|
||||
result: { f1: { a: 1 }, _: 'resolve' },
|
||||
@ -528,13 +532,13 @@ describe('workflow > instructions > manual', () => {
|
||||
|
||||
await sleep(500);
|
||||
|
||||
const UserJobModel = db.getModel('users_jobs');
|
||||
const UserJobModel = db.getModel('workflowManualTasks');
|
||||
const pendingJobs = await UserJobModel.findAll({
|
||||
order: [['userId', 'ASC']],
|
||||
});
|
||||
expect(pendingJobs.length).toBe(2);
|
||||
|
||||
const res1 = await userAgents[0].resource('users_jobs').submit({
|
||||
const res1 = await userAgents[0].resource('workflowManualTasks').submit({
|
||||
filterByTk: pendingJobs[0].get('id'),
|
||||
values: {
|
||||
result: { f1: { a: 0 }, _: 'reject' },
|
||||
@ -550,7 +554,7 @@ describe('workflow > instructions > manual', () => {
|
||||
expect(j1.status).toBe(JOB_STATUS.PENDING);
|
||||
expect(j1.result).toBe(0.5);
|
||||
|
||||
const res2 = await userAgents[1].resource('users_jobs').submit({
|
||||
const res2 = await userAgents[1].resource('workflowManualTasks').submit({
|
||||
filterByTk: pendingJobs[1].get('id'),
|
||||
values: {
|
||||
result: { f1: { a: 0 }, _: 'reject' },
|
||||
|
@ -24,7 +24,7 @@ export async function submit(context: Context, next) {
|
||||
const plugin: WorkflowPlugin = context.app.getPlugin(WorkflowPlugin);
|
||||
const instruction = plugin.instructions.get('manual') as ManualInstruction;
|
||||
|
||||
const userJob = await repository.findOne({
|
||||
const task = await repository.findOne({
|
||||
filterByTk,
|
||||
// filter: {
|
||||
// userId: currentUser?.id
|
||||
@ -33,40 +33,40 @@ export async function submit(context: Context, next) {
|
||||
context,
|
||||
});
|
||||
|
||||
if (!userJob) {
|
||||
if (!task) {
|
||||
return context.throw(404);
|
||||
}
|
||||
|
||||
const { forms = {} } = userJob.node.config;
|
||||
const { forms = {} } = task.node.config;
|
||||
const [formKey] = Object.keys(values.result ?? {}).filter((key) => key !== '_');
|
||||
const actionKey = values.result?._;
|
||||
|
||||
const actionItem = forms[formKey]?.actions?.find((item) => item.key === actionKey);
|
||||
// NOTE: validate status
|
||||
if (
|
||||
userJob.status !== JOB_STATUS.PENDING ||
|
||||
userJob.job.status !== JOB_STATUS.PENDING ||
|
||||
userJob.execution.status !== EXECUTION_STATUS.STARTED ||
|
||||
!userJob.workflow.enabled ||
|
||||
task.status !== JOB_STATUS.PENDING ||
|
||||
task.job.status !== JOB_STATUS.PENDING ||
|
||||
task.execution.status !== EXECUTION_STATUS.STARTED ||
|
||||
!task.workflow.enabled ||
|
||||
!actionKey ||
|
||||
actionItem?.status == null
|
||||
) {
|
||||
return context.throw(400);
|
||||
}
|
||||
|
||||
userJob.execution.workflow = userJob.workflow;
|
||||
const processor = plugin.createProcessor(userJob.execution);
|
||||
task.execution.workflow = task.workflow;
|
||||
const processor = plugin.createProcessor(task.execution);
|
||||
await processor.prepare();
|
||||
|
||||
// NOTE: validate assignee
|
||||
const assignees = processor
|
||||
.getParsedValue(userJob.node.config.assignees ?? [], userJob.nodeId)
|
||||
.getParsedValue(task.node.config.assignees ?? [], task.nodeId)
|
||||
.flat()
|
||||
.filter(Boolean);
|
||||
if (!assignees.includes(currentUser.id) || userJob.userId !== currentUser.id) {
|
||||
if (!assignees.includes(currentUser.id) || task.userId !== currentUser.id) {
|
||||
return context.throw(403);
|
||||
}
|
||||
const presetValues = processor.getParsedValue(actionItem.values ?? {}, userJob.nodeId, {
|
||||
const presetValues = processor.getParsedValue(actionItem.values ?? {}, task.nodeId, {
|
||||
additionalScope: {
|
||||
// @deprecated
|
||||
currentUser: currentUser,
|
||||
@ -82,56 +82,32 @@ export async function submit(context: Context, next) {
|
||||
},
|
||||
});
|
||||
|
||||
userJob.set({
|
||||
task.set({
|
||||
status: actionItem.status,
|
||||
result: actionItem.status
|
||||
? { [formKey]: Object.assign(values.result[formKey], presetValues), _: actionKey }
|
||||
: Object.assign(userJob.result ?? {}, values.result),
|
||||
: Object.assign(task.result ?? {}, values.result),
|
||||
});
|
||||
|
||||
const handler = instruction.formTypes.get(forms[formKey].type);
|
||||
if (handler && userJob.status) {
|
||||
await handler.call(instruction, userJob, forms[formKey], processor);
|
||||
if (handler && task.status) {
|
||||
await handler.call(instruction, task, forms[formKey], processor);
|
||||
}
|
||||
|
||||
await userJob.save();
|
||||
await task.save();
|
||||
|
||||
await processor.exit();
|
||||
|
||||
context.body = userJob;
|
||||
context.body = task;
|
||||
context.status = 202;
|
||||
|
||||
await next();
|
||||
|
||||
userJob.job.execution = userJob.execution;
|
||||
userJob.job.latestUserJob = userJob;
|
||||
task.job.execution = task.execution;
|
||||
task.job.latestTask = task;
|
||||
|
||||
// NOTE: resume the process and no `await` for quick returning
|
||||
processor.logger.info(`manual node (${userJob.nodeId}) action trigger execution (${userJob.execution.id}) to resume`);
|
||||
processor.logger.info(`manual node (${task.nodeId}) action trigger execution (${task.execution.id}) to resume`);
|
||||
|
||||
plugin.resume(userJob.job);
|
||||
}
|
||||
|
||||
export async function countMine(context: Context, next) {
|
||||
const repository = utils.getRepositoryFromParams(context);
|
||||
const { currentUser } = context.state;
|
||||
|
||||
const count = await repository.count({
|
||||
filter: {
|
||||
$and: [
|
||||
{
|
||||
'workflow.enabled': true,
|
||||
},
|
||||
context.action.params.filter ?? {},
|
||||
{
|
||||
userId: currentUser.id,
|
||||
},
|
||||
],
|
||||
},
|
||||
context,
|
||||
});
|
||||
|
||||
context.body = count;
|
||||
|
||||
await next();
|
||||
plugin.resume(task.job);
|
||||
}
|
||||
|
@ -1,28 +0,0 @@
|
||||
/**
|
||||
* 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 { extendCollection } from '@nocobase/database';
|
||||
|
||||
export default extendCollection({
|
||||
name: 'jobs',
|
||||
fields: [
|
||||
{
|
||||
type: 'belongsToMany',
|
||||
name: 'users',
|
||||
through: 'users_jobs',
|
||||
},
|
||||
{
|
||||
type: 'hasMany',
|
||||
name: 'usersJobs',
|
||||
target: 'users_jobs',
|
||||
foreignKey: 'jobId',
|
||||
onDelete: 'CASCADE',
|
||||
},
|
||||
],
|
||||
});
|
@ -1,26 +0,0 @@
|
||||
/**
|
||||
* 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 { extendCollection } from '@nocobase/database';
|
||||
|
||||
export default extendCollection({
|
||||
name: 'users',
|
||||
fields: [
|
||||
{
|
||||
type: 'belongsToMany',
|
||||
name: 'jobs',
|
||||
through: 'users_jobs',
|
||||
},
|
||||
{
|
||||
type: 'hasMany',
|
||||
name: 'usersJobs',
|
||||
target: 'users_jobs',
|
||||
},
|
||||
],
|
||||
});
|
@ -8,9 +8,10 @@
|
||||
*/
|
||||
|
||||
import { defineCollection } from '@nocobase/database';
|
||||
import { NAMESPACE } from '../../common/constants';
|
||||
|
||||
export default defineCollection({
|
||||
name: 'users_jobs',
|
||||
name: 'workflowManualTasks',
|
||||
dumpRules: {
|
||||
group: 'log',
|
||||
},
|
||||
@ -40,6 +41,12 @@ export default defineCollection({
|
||||
{
|
||||
type: 'string',
|
||||
name: 'title',
|
||||
interface: 'input',
|
||||
uiSchema: {
|
||||
type: 'string',
|
||||
title: `{{t("Task title", { ns: "${NAMESPACE}" })}}`,
|
||||
'x-component': 'Input',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'belongsTo',
|
||||
@ -53,6 +60,20 @@ export default defineCollection({
|
||||
{
|
||||
type: 'belongsTo',
|
||||
name: 'workflow',
|
||||
target: 'workflows',
|
||||
foreignKey: 'workflowId',
|
||||
interface: 'm2o',
|
||||
uiSchema: {
|
||||
type: 'object',
|
||||
title: `{{t("Workflow", { ns: "workflow" })}}`,
|
||||
'x-component': 'AssociationField',
|
||||
'x-component-props': {
|
||||
fieldNames: {
|
||||
label: 'title',
|
||||
value: 'id',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'integer',
|
@ -14,7 +14,7 @@ export default class extends Migration {
|
||||
async up() {
|
||||
const { db } = this.context;
|
||||
const NodeRepo = db.getRepository('flow_nodes');
|
||||
const TaskRepo = db.getRepository('users_jobs');
|
||||
const TaskRepo = db.getRepository('workflowManualTasks');
|
||||
await db.sequelize.transaction(async (transaction) => {
|
||||
const nodes = await NodeRepo.find({
|
||||
filter: {
|
||||
|
@ -0,0 +1,48 @@
|
||||
/**
|
||||
* 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';
|
||||
on = 'beforeLoad';
|
||||
async up() {
|
||||
const { db } = this.context;
|
||||
const queryInterface = db.sequelize.getQueryInterface();
|
||||
await db.sequelize.transaction(async (transaction) => {
|
||||
const exists = await queryInterface.tableExists('users_jobs', { transaction });
|
||||
if (exists) {
|
||||
const newTableName = db.options.underscored ? 'workflow_manual_tasks' : 'workflowManualTasks';
|
||||
|
||||
await queryInterface.renameTable('users_jobs', newTableName, { transaction });
|
||||
|
||||
const indexes: any = await queryInterface.showIndex(newTableName, { transaction });
|
||||
|
||||
for (const item of indexes) {
|
||||
if (item.name.startsWith('users_jobs')) {
|
||||
if (this.db.isPostgresCompatibleDialect()) {
|
||||
await db.sequelize.query(
|
||||
`ALTER INDEX "${item.name}" RENAME TO "${item.name.replace('users_jobs', 'workflow_manual_tasks')}";`,
|
||||
{ transaction },
|
||||
);
|
||||
} else if (this.db.isMySQLCompatibleDialect()) {
|
||||
await db.sequelize.query(
|
||||
`ALTER TABLE ${newTableName} RENAME INDEX ${item.name} TO ${item.name.replace(
|
||||
'users_jobs',
|
||||
'workflow_manual_tasks',
|
||||
)};`,
|
||||
{ transaction },
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
/**
|
||||
* 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 { EXECUTION_STATUS, JOB_STATUS } from '@nocobase/plugin-workflow';
|
||||
import { Migration } from '@nocobase/server';
|
||||
import { MANUAL_TASK_TYPE } from '../../common/constants';
|
||||
|
||||
export default class extends Migration {
|
||||
appVersion = '<1.6.0';
|
||||
async up() {
|
||||
const { db } = this.context;
|
||||
const WorkflowTaskModel = db.getModel('workflowTasks');
|
||||
const WorkflowManualTaskRepo = db.getRepository('workflowManualTasks');
|
||||
await db.sequelize.transaction(async (transaction) => {
|
||||
const tasks = await WorkflowManualTaskRepo.find({
|
||||
filter: {
|
||||
status: JOB_STATUS.PENDING,
|
||||
'execution.status': EXECUTION_STATUS.STARTED,
|
||||
'workflow.enabled': true,
|
||||
},
|
||||
transaction,
|
||||
});
|
||||
await WorkflowTaskModel.bulkCreate(
|
||||
tasks.map((item) => ({
|
||||
type: MANUAL_TASK_TYPE,
|
||||
key: `${item.id}`,
|
||||
userId: item.userId,
|
||||
workflowId: item.workflowId,
|
||||
})),
|
||||
{
|
||||
transaction,
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
@ -169,7 +169,9 @@ export class ApprovalPassthroughModeNode {
|
||||
this.node = page.getByLabel(`Approval-${nodeName}`, { exact: true });
|
||||
this.nodeTitle = page.getByLabel(`Approval-${nodeName}`, { exact: true }).getByRole('textbox');
|
||||
this.nodeConfigure = this.node.locator('>div').first();
|
||||
this.addAssigneesButton = page.getByRole('button', { name: 'plus Add' });
|
||||
this.addAssigneesButton = page
|
||||
.getByLabel('block-item-ArrayItems-workflows-Assignees')
|
||||
.getByRole('button', { name: 'plus Add' });
|
||||
this.addSelectAssigneesMenu = page.getByRole('button', { name: 'Select assignees' });
|
||||
this.addQueryAssigneesMenu = page.getByRole('button', { name: 'Query assignees' });
|
||||
this.assigneesDropDown = page.getByTestId('select-single');
|
||||
@ -183,14 +185,14 @@ export class ApprovalPassthroughModeNode {
|
||||
this.sequentiallyRadio = page.getByLabel('Sequentially', { exact: true });
|
||||
this.goToconfigureButton = page.getByRole('button', { name: 'Go to configure' });
|
||||
this.addBlockButton = page.getByLabel('schema-initializer-Grid-ApprovalProcessAddBlockButton-workflows');
|
||||
this.addDetailsMenu = page.getByRole('menuitem', { name: 'Details' });
|
||||
this.addDetailsMenu = page.getByRole('menuitem', { name: 'Original application content' });
|
||||
this.detailsConfigureFieldsButton = page.getByLabel(
|
||||
`schema-initializer-Grid-details:configureFields-${collectionName}`,
|
||||
);
|
||||
this.addActionsMenu = page.getByRole('menuitem', { name: 'Actions' }).getByRole('switch');
|
||||
this.addActionsMenu = page.getByRole('menuitem', { name: 'Process form' }).getByRole('switch');
|
||||
this.actionsConfigureFieldsButton = page.getByLabel('schema-initializer-Grid-FormItemInitializers-approvalRecords');
|
||||
this.actionsConfigureActionsButton = page.getByLabel(
|
||||
'schema-initializer-ActionBar-ApprovalProcessAddActionButton-approvalRecords',
|
||||
'schema-initializer-ActionBar-ApprovalProcessAddActionButton-',
|
||||
);
|
||||
this.addApproveButton = page.getByRole('menuitem', { name: 'Approve' }).getByRole('switch');
|
||||
this.addRejectButton = page.getByRole('menuitem', { name: 'Reject' }).getByRole('switch');
|
||||
@ -240,7 +242,9 @@ export class ApprovalBranchModeNode {
|
||||
this.node = page.getByLabel(`Approval-${nodeName}`, { exact: true });
|
||||
this.nodeTitle = page.getByLabel(`Approval-${nodeName}`, { exact: true }).getByRole('textbox');
|
||||
this.nodeConfigure = this.node.locator('>div').first();
|
||||
this.addAssigneesButton = page.getByRole('button', { name: 'plus Add' });
|
||||
this.addAssigneesButton = page
|
||||
.getByLabel('block-item-ArrayItems-workflows-Assignees')
|
||||
.getByRole('button', { name: 'plus Add' });
|
||||
this.addSelectAssigneesMenu = page.getByRole('button', { name: 'Select assignees' });
|
||||
this.addQueryAssigneesMenu = page.getByRole('button', { name: 'Query assignees' });
|
||||
this.assigneesDropDown = page.getByTestId('select-single');
|
||||
@ -260,9 +264,7 @@ export class ApprovalBranchModeNode {
|
||||
);
|
||||
this.addActionsMenu = page.getByRole('menuitem', { name: 'Process form' }).getByRole('switch');
|
||||
this.actionsConfigureFieldsButton = page.getByLabel('schema-initializer-Grid-FormItemInitializers-approvalRecords');
|
||||
this.actionsConfigureActionsButton = page.getByLabel(
|
||||
'schema-initializer-ActionBar-ApprovalProcessAddActionButton-approvalRecords',
|
||||
);
|
||||
this.actionsConfigureActionsButton = page.getByLabel('schema-initializer-ActionBar-');
|
||||
this.addApproveButton = page.getByRole('menuitem', { name: 'Approve' }).getByRole('switch');
|
||||
this.addRejectButton = page.getByRole('menuitem', { name: 'Reject' }).getByRole('switch');
|
||||
this.addReturnButton = page.getByRole('menuitem', { name: 'Return' }).getByRole('switch');
|
||||
|
@ -415,7 +415,7 @@ export const apiGetWorkflowNodeExecutions = async (id: number) => {
|
||||
|
||||
const state = await api.storageState();
|
||||
const headers = getHeaders(state);
|
||||
const url = `/api/executions:list?appends[]=jobs&filter[workflowId]=${id}&fields=id,createdAt,updatedAt,key,status,workflowId,jobs`;
|
||||
const url = `/api/executions:list?appends[]=jobs&filter[workflowId]=${id}&fields=id,createdAt,updatedAt,key,status,workflowId`;
|
||||
const result = await api.get(url, {
|
||||
headers,
|
||||
});
|
||||
|
@ -6,9 +6,9 @@
|
||||
* 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 React, { useEffect, useMemo } from 'react';
|
||||
import React, { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
|
||||
import { Link, Outlet, useNavigate, useParams } from 'react-router-dom';
|
||||
import { Button, Layout, Menu, Spin, Badge, theme, Tooltip } from 'antd';
|
||||
import { Button, Layout, Menu, Badge, Tooltip, Tabs } from 'antd';
|
||||
import { PageHeader } from '@ant-design/pro-layout';
|
||||
import { CheckCircleOutlined } from '@ant-design/icons';
|
||||
import classnames from 'classnames';
|
||||
@ -16,17 +16,31 @@ import classnames from 'classnames';
|
||||
import {
|
||||
css,
|
||||
PinnedPluginListProvider,
|
||||
SchemaComponent,
|
||||
SchemaComponentContext,
|
||||
SchemaComponentOptions,
|
||||
useApp,
|
||||
useCompile,
|
||||
useDocumentTitle,
|
||||
usePlugin,
|
||||
useRequest,
|
||||
useToken,
|
||||
} from '@nocobase/client';
|
||||
|
||||
import PluginWorkflowClient from '.';
|
||||
import { lang } from './locale';
|
||||
import { lang, NAMESPACE } from './locale';
|
||||
|
||||
const layoutClass = css`
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
||||
const sideClass = css`
|
||||
height: calc(100vh - 46px);
|
||||
overflow: auto;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
height: 100%;
|
||||
|
||||
.ant-layout-sider-children {
|
||||
width: 200px;
|
||||
@ -34,21 +48,29 @@ const sideClass = css`
|
||||
}
|
||||
`;
|
||||
|
||||
const contentClass = css`
|
||||
padding: 24px;
|
||||
min-height: 280px;
|
||||
overflow: auto;
|
||||
`;
|
||||
|
||||
export interface TaskTypeOptions {
|
||||
title: string;
|
||||
useCountRequest?: Function;
|
||||
component?: React.ComponentType;
|
||||
children?: TaskTypeOptions[];
|
||||
collection: string;
|
||||
useActionParams: Function;
|
||||
component: React.ComponentType;
|
||||
extraActions?: React.ComponentType;
|
||||
// children?: TaskTypeOptions[];
|
||||
}
|
||||
|
||||
const TasksCountsContext = createContext<{ counts: Record<string, number>; total: number }>({ counts: {}, total: 0 });
|
||||
|
||||
function MenuLink({ type }: any) {
|
||||
const workflowPlugin = usePlugin(PluginWorkflowClient);
|
||||
const compile = useCompile();
|
||||
const { title, useCountRequest } = workflowPlugin.taskTypes.get(type);
|
||||
const { data, loading, run } = useCountRequest?.() || { loading: false };
|
||||
useEffect(() => {
|
||||
run?.();
|
||||
}, [run]);
|
||||
const { title } = workflowPlugin.taskTypes.get(type);
|
||||
const { counts } = useContext(TasksCountsContext);
|
||||
const typeTitle = compile(title);
|
||||
|
||||
return (
|
||||
<Link
|
||||
@ -57,24 +79,72 @@ function MenuLink({ type }: any) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
|
||||
> span:first-child {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
`}
|
||||
>
|
||||
<span>{compile(title)}</span>
|
||||
{loading ? <Spin /> : <Badge count={data?.data || 0} />}
|
||||
<span>{typeTitle}</span>
|
||||
<Badge count={counts[type] || 0} size="small" />
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
export function WorkflowTasks() {
|
||||
const workflowPlugin = usePlugin(PluginWorkflowClient);
|
||||
const navigate = useNavigate();
|
||||
const { taskType } = useParams();
|
||||
const compile = useCompile();
|
||||
const {
|
||||
token: { colorBgContainer },
|
||||
} = theme.useToken();
|
||||
const TASK_STATUS = {
|
||||
ALL: 'all',
|
||||
PENDING: 'pending',
|
||||
COMPLETED: 'completed',
|
||||
};
|
||||
|
||||
const items = useMemo(
|
||||
function StatusTabs() {
|
||||
const navigate = useNavigate();
|
||||
const { taskType, status = TASK_STATUS.PENDING } = useParams();
|
||||
const type = useCurrentTaskType();
|
||||
const { extraActions: ExtraActions } = type;
|
||||
return (
|
||||
<Tabs
|
||||
activeKey={status}
|
||||
onChange={(activeKey) => {
|
||||
navigate(`/admin/workflow/tasks/${taskType}/${activeKey}`);
|
||||
}}
|
||||
className={css`
|
||||
&.ant-tabs-top > .ant-tabs-nav {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
`}
|
||||
items={[
|
||||
{
|
||||
key: TASK_STATUS.PENDING,
|
||||
label: lang('Pending'),
|
||||
},
|
||||
{
|
||||
key: TASK_STATUS.COMPLETED,
|
||||
label: lang('Completed'),
|
||||
},
|
||||
{
|
||||
key: TASK_STATUS.ALL,
|
||||
label: lang('All'),
|
||||
},
|
||||
]}
|
||||
tabBarExtraContent={
|
||||
ExtraActions
|
||||
? {
|
||||
right: <ExtraActions />,
|
||||
}
|
||||
: {}
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function useTaskTypeItems() {
|
||||
const workflowPlugin = usePlugin(PluginWorkflowClient);
|
||||
|
||||
return useMemo(
|
||||
() =>
|
||||
Array.from(workflowPlugin.taskTypes.getKeys()).map((key: string) => {
|
||||
return {
|
||||
@ -84,37 +154,143 @@ export function WorkflowTasks() {
|
||||
}),
|
||||
[workflowPlugin.taskTypes],
|
||||
);
|
||||
}
|
||||
|
||||
const { title, component: Component } = useMemo<any>(
|
||||
function useCurrentTaskType() {
|
||||
const workflowPlugin = usePlugin(PluginWorkflowClient);
|
||||
const { taskType } = useParams();
|
||||
const items = useTaskTypeItems();
|
||||
return useMemo<any>(
|
||||
() => workflowPlugin.taskTypes.get(taskType ?? items[0]?.key) ?? {},
|
||||
[items, taskType, workflowPlugin.taskTypes],
|
||||
);
|
||||
}
|
||||
|
||||
export function WorkflowTasks() {
|
||||
const compile = useCompile();
|
||||
const { setTitle } = useDocumentTitle();
|
||||
const navigate = useNavigate();
|
||||
const { taskType, status = TASK_STATUS.PENDING } = useParams();
|
||||
const {
|
||||
token: { colorBgContainer },
|
||||
} = useToken();
|
||||
|
||||
const items = useTaskTypeItems();
|
||||
|
||||
const { title, collection, useActionParams, component: Component } = useCurrentTaskType();
|
||||
|
||||
const params = useActionParams(status);
|
||||
|
||||
useEffect(() => {
|
||||
if (!taskType && items[0].key) {
|
||||
navigate(`/admin/workflow/tasks/${items[0].key}`, { replace: true });
|
||||
}
|
||||
}, [items, navigate, taskType]);
|
||||
setTitle?.(`${lang('Workflow todo')}${title ? `: ${compile(title)}` : ''}`);
|
||||
}, [taskType, status, setTitle, title, compile]);
|
||||
|
||||
const key = taskType ?? items[0].key;
|
||||
useEffect(() => {
|
||||
if (!taskType) {
|
||||
navigate(`/admin/workflow/tasks/${items[0].key}/${status}`, { replace: true });
|
||||
}
|
||||
}, [items, navigate, status, taskType]);
|
||||
|
||||
const typeKey = taskType ?? items[0].key;
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<Layout className={layoutClass}>
|
||||
<Layout.Sider className={sideClass} theme="light">
|
||||
<Menu mode="inline" selectedKeys={[key]} items={items} style={{ height: '100%' }} />
|
||||
<Menu mode="inline" selectedKeys={[typeKey]} items={items} style={{ height: '100%' }} />
|
||||
</Layout.Sider>
|
||||
<Layout>
|
||||
<PageHeader
|
||||
className={classnames('pageHeaderCss', 'height0')}
|
||||
style={{ background: colorBgContainer, padding: '12px 24px 0 24px' }}
|
||||
title={compile(title)}
|
||||
/>
|
||||
<Layout.Content style={{ padding: '24px', minHeight: 280 }}>
|
||||
<Layout
|
||||
className={css`
|
||||
> div {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
|
||||
> .ant-formily-layout {
|
||||
height: 100%;
|
||||
|
||||
> div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
`}
|
||||
>
|
||||
<SchemaComponentContext.Provider value={{ designable: false }}>
|
||||
{Component ? <Component /> : null}
|
||||
<SchemaComponent
|
||||
components={{
|
||||
Layout,
|
||||
PageHeader,
|
||||
StatusTabs,
|
||||
}}
|
||||
schema={{
|
||||
name: `${taskType}-${status}`,
|
||||
type: 'void',
|
||||
'x-decorator': 'List.Decorator',
|
||||
'x-decorator-props': {
|
||||
collection,
|
||||
action: 'list',
|
||||
params: {
|
||||
pageSize: 20,
|
||||
sort: ['-createdAt'],
|
||||
...params,
|
||||
},
|
||||
},
|
||||
properties: {
|
||||
header: {
|
||||
type: 'void',
|
||||
'x-component': 'PageHeader',
|
||||
'x-component-props': {
|
||||
className: classnames('pageHeaderCss'),
|
||||
style: {
|
||||
background: colorBgContainer,
|
||||
padding: '12px 24px 0 24px',
|
||||
},
|
||||
title,
|
||||
},
|
||||
properties: {
|
||||
tabs: {
|
||||
type: 'void',
|
||||
'x-component': 'StatusTabs',
|
||||
},
|
||||
},
|
||||
},
|
||||
content: {
|
||||
type: 'void',
|
||||
'x-component': 'Layout.Content',
|
||||
'x-component-props': {
|
||||
className: contentClass,
|
||||
},
|
||||
properties: {
|
||||
list: {
|
||||
type: 'array',
|
||||
'x-component': 'List',
|
||||
'x-component-props': {
|
||||
className: css`
|
||||
> .itemCss:not(:last-child) {
|
||||
border-bottom: none;
|
||||
}
|
||||
`,
|
||||
locale: {
|
||||
emptyText: `{{ t("No data yet", { ns: "${NAMESPACE}" }) }}`,
|
||||
},
|
||||
},
|
||||
properties: {
|
||||
item: {
|
||||
type: 'object',
|
||||
'x-decorator': 'List.Item',
|
||||
'x-component': Component,
|
||||
'x-read-pretty': true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<Outlet />
|
||||
</SchemaComponentContext.Provider>
|
||||
</Layout.Content>
|
||||
</Layout>
|
||||
</Layout>
|
||||
);
|
||||
@ -122,40 +298,78 @@ export function WorkflowTasks() {
|
||||
|
||||
function WorkflowTasksLink() {
|
||||
const workflowPlugin = usePlugin(PluginWorkflowClient);
|
||||
const { total } = useContext(TasksCountsContext);
|
||||
|
||||
const types = Array.from(workflowPlugin.taskTypes.getKeys());
|
||||
return types.length ? (
|
||||
<Tooltip title={lang('Workflow todos')}>
|
||||
<Button
|
||||
className={css`
|
||||
padding: 0;
|
||||
display: inline-flex;
|
||||
vertical-align: middle;
|
||||
a {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
.anticon {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
line-height: 1em;
|
||||
}
|
||||
}
|
||||
`}
|
||||
>
|
||||
<Link to="/admin/workflow/tasks">
|
||||
<Button>
|
||||
<Link to={`/admin/workflow/tasks/${types[0]}`}>
|
||||
<Badge count={total} size="small">
|
||||
<CheckCircleOutlined />
|
||||
</Badge>
|
||||
</Link>
|
||||
</Button>
|
||||
</Tooltip>
|
||||
) : null;
|
||||
}
|
||||
|
||||
function transform(detail) {
|
||||
return detail.reduce((result, stats) => {
|
||||
result[stats.type] = stats.count;
|
||||
return result;
|
||||
}, {});
|
||||
}
|
||||
|
||||
export const TasksProvider = (props: any) => {
|
||||
const app = useApp();
|
||||
const [counts, setCounts] = useState<Record<string, number>>({});
|
||||
const onTaskUpdate = useCallback(({ detail = [] }: CustomEvent) => {
|
||||
setCounts((prev) => {
|
||||
return {
|
||||
...prev,
|
||||
...transform(detail),
|
||||
};
|
||||
});
|
||||
}, []);
|
||||
|
||||
const { runAsync } = useRequest(
|
||||
{
|
||||
resource: 'workflowTasks',
|
||||
action: 'countMine',
|
||||
},
|
||||
{
|
||||
manual: true,
|
||||
},
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
runAsync()
|
||||
.then((res) => {
|
||||
setCounts((prev) => {
|
||||
return {
|
||||
...prev,
|
||||
...transform(res['data']),
|
||||
};
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
});
|
||||
}, [runAsync]);
|
||||
|
||||
useEffect(() => {
|
||||
app.eventBus.addEventListener('ws:message:workflow:tasks:updated', onTaskUpdate);
|
||||
|
||||
return () => {
|
||||
app.eventBus.removeEventListener('ws:message:workflow:tasks:updated', onTaskUpdate);
|
||||
};
|
||||
}, [app.eventBus, onTaskUpdate]);
|
||||
|
||||
const total = Object.values(counts).reduce((a, b) => a + b, 0) || 0;
|
||||
|
||||
return (
|
||||
<TasksCountsContext.Provider value={{ total, counts }}>
|
||||
<PinnedPluginListProvider
|
||||
items={{
|
||||
todo: { component: 'WorkflowTasksLink', pin: true, snippet: '*' },
|
||||
@ -169,5 +383,6 @@ export const TasksProvider = (props: any) => {
|
||||
{props.children}
|
||||
</SchemaComponentOptions>
|
||||
</PinnedPluginListProvider>
|
||||
</TasksCountsContext.Provider>
|
||||
);
|
||||
};
|
||||
|
@ -0,0 +1,23 @@
|
||||
/**
|
||||
* 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 React from 'react';
|
||||
import { Tooltip } from 'antd';
|
||||
|
||||
import { lang } from '../locale';
|
||||
|
||||
export function WorkflowTitle(workflow) {
|
||||
return workflow.enabled ? (
|
||||
workflow.title
|
||||
) : (
|
||||
<Tooltip title={lang('New version enabled')}>
|
||||
<span style={{ textDecoration: 'line-through' }}>{workflow.title}</span>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
@ -19,3 +19,4 @@ export * from './renderEngineReference';
|
||||
export * from './Calculation';
|
||||
export * from './Fieldset';
|
||||
export * from './TriggerCollectionRecordSelect';
|
||||
export * from './WorkflowTitle';
|
||||
|
@ -120,12 +120,12 @@ export default class PluginWorkflowClient extends Plugin {
|
||||
});
|
||||
|
||||
this.router.add('admin.workflow.tasks', {
|
||||
path: '/admin/workflow/tasks/:taskType?',
|
||||
path: '/admin/workflow/tasks/:taskType/:status?',
|
||||
Component: WorkflowTasks,
|
||||
});
|
||||
|
||||
this.router.add('admin.workflow.tasks.popup', {
|
||||
path: '/admin/workflow/tasks/:taskType/popups/*',
|
||||
path: '/admin/workflow/tasks/:taskType/:status/popups/*',
|
||||
Component: PagePopups,
|
||||
});
|
||||
|
||||
|
@ -133,6 +133,8 @@
|
||||
"Canceled": "已取消",
|
||||
"Rejected": "已拒绝",
|
||||
"Retry needed": "需重试",
|
||||
"Completed": "已完成",
|
||||
"All": "全部",
|
||||
"View result": "查看结果",
|
||||
|
||||
"Triggered but still waiting in queue to execute.": "已触发但仍在队列中等待执行。",
|
||||
@ -231,5 +233,6 @@
|
||||
"After end of branches": "分支结束后",
|
||||
"Inside of branch": "分支内",
|
||||
|
||||
"Workflow todos": "流程待办"
|
||||
"Workflow todos": "流程待办",
|
||||
"New version enabled": "已启用新版本"
|
||||
}
|
||||
|
@ -16,8 +16,9 @@ import LRUCache from 'lru-cache';
|
||||
import { Op } from '@nocobase/database';
|
||||
import { Plugin } from '@nocobase/server';
|
||||
import { Registry } from '@nocobase/utils';
|
||||
|
||||
import { SequelizeCollectionManager } from '@nocobase/data-source-manager';
|
||||
import { Logger, LoggerOptions } from '@nocobase/logger';
|
||||
|
||||
import Processor from './Processor';
|
||||
import initActions from './actions';
|
||||
import { EXECUTION_STATUS } from './constants';
|
||||
@ -34,10 +35,9 @@ import DestroyInstruction from './instructions/DestroyInstruction';
|
||||
import QueryInstruction from './instructions/QueryInstruction';
|
||||
import UpdateInstruction from './instructions/UpdateInstruction';
|
||||
|
||||
import type { ExecutionModel, JobModel, WorkflowModel } from './types';
|
||||
import type { ExecutionModel, JobModel, WorkflowModel, WorkflowTaskModel } from './types';
|
||||
import WorkflowRepository from './repositories/WorkflowRepository';
|
||||
import { Context } from '@nocobase/actions';
|
||||
import { SequelizeCollectionManager } from '@nocobase/data-source-manager';
|
||||
import WorkflowTasksRepository from './repositories/WorkflowTasksRepository';
|
||||
|
||||
type ID = number | string;
|
||||
|
||||
@ -213,6 +213,7 @@ export default class PluginWorkflowServer extends Plugin {
|
||||
async beforeLoad() {
|
||||
this.db.registerRepositories({
|
||||
WorkflowRepository,
|
||||
WorkflowTasksRepository,
|
||||
});
|
||||
}
|
||||
|
||||
@ -262,6 +263,7 @@ export default class PluginWorkflowServer extends Plugin {
|
||||
actions: ['workflows:list'],
|
||||
});
|
||||
|
||||
this.app.acl.allow('workflowTasks', 'countMine', 'loggedIn');
|
||||
this.app.acl.allow('*', ['trigger'], 'loggedIn');
|
||||
|
||||
this.db.addMigrations({
|
||||
@ -733,4 +735,45 @@ export default class PluginWorkflowServer extends Plugin {
|
||||
return db.sequelize.transaction();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @experimental
|
||||
*/
|
||||
public async toggleTaskStatus(task: WorkflowTaskModel, done: boolean, { transaction }: Transactionable) {
|
||||
const { db } = this.app;
|
||||
const repository = db.getRepository('workflowTasks') as WorkflowTasksRepository;
|
||||
if (done) {
|
||||
await repository.destroy({
|
||||
filter: {
|
||||
type: task.type,
|
||||
key: `${task.key}`,
|
||||
},
|
||||
transaction,
|
||||
});
|
||||
} else {
|
||||
await repository.updateOrCreate({
|
||||
filterKeys: ['key', 'type'],
|
||||
values: task,
|
||||
transaction,
|
||||
});
|
||||
}
|
||||
|
||||
// NOTE:
|
||||
// 1. `ws` not works in backend test cases for now.
|
||||
// 2. `userId` here for compatibility of no user approvals (deprecated).
|
||||
if (task.userId) {
|
||||
const counts =
|
||||
(await repository.countAll({
|
||||
where: {
|
||||
userId: task.userId,
|
||||
},
|
||||
transaction,
|
||||
})) || [];
|
||||
this.app.emit('ws:sendToTag', {
|
||||
tagKey: 'userId',
|
||||
tagValue: `${task.userId}`,
|
||||
message: { type: 'workflow:tasks:updated', payload: counts },
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -10,6 +10,7 @@
|
||||
import * as workflows from './workflows';
|
||||
import * as nodes from './nodes';
|
||||
import * as executions from './executions';
|
||||
import * as workflowTasks from './workflowTasks';
|
||||
|
||||
function make(name, mod) {
|
||||
return Object.keys(mod).reduce(
|
||||
@ -33,5 +34,6 @@ export default function ({ app }) {
|
||||
test: nodes.test,
|
||||
}),
|
||||
...make('executions', executions),
|
||||
...make('workflowTasks', workflowTasks),
|
||||
});
|
||||
}
|
||||
|
@ -0,0 +1,24 @@
|
||||
/**
|
||||
* 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 { Context, utils } from '@nocobase/actions';
|
||||
|
||||
import WorkflowTasksRepository from '../repositories/WorkflowTasksRepository';
|
||||
|
||||
export async function countMine(context: Context, next) {
|
||||
const repository = utils.getRepositoryFromParams(context) as WorkflowTasksRepository;
|
||||
context.body =
|
||||
(await repository.countAll({
|
||||
where: {
|
||||
userId: context.state.currentUser.id,
|
||||
},
|
||||
})) || [];
|
||||
|
||||
next();
|
||||
}
|
@ -8,14 +8,14 @@
|
||||
*/
|
||||
|
||||
import actions, { Context, utils } from '@nocobase/actions';
|
||||
import { Op, Repository } from '@nocobase/database';
|
||||
import { Op } from '@nocobase/database';
|
||||
|
||||
import Plugin from '../Plugin';
|
||||
import Processor from '../Processor';
|
||||
import WorkflowRepository from '../repositories/WorkflowRepository';
|
||||
|
||||
export async function update(context: Context, next) {
|
||||
const repository = utils.getRepositoryFromParams(context) as Repository;
|
||||
const repository = utils.getRepositoryFromParams(context) as WorkflowRepository;
|
||||
const { filterByTk, values } = context.action.params;
|
||||
context.action.mergeParams({
|
||||
whitelist: ['title', 'description', 'enabled', 'triggerTitle', 'config', 'options'],
|
||||
@ -31,7 +31,7 @@ export async function update(context: Context, next) {
|
||||
}
|
||||
|
||||
export async function destroy(context: Context, next) {
|
||||
const repository = utils.getRepositoryFromParams(context) as Repository;
|
||||
const repository = utils.getRepositoryFromParams(context) as WorkflowRepository;
|
||||
const { filterByTk, filter } = context.action.params;
|
||||
|
||||
await context.db.sequelize.transaction(async (transaction) => {
|
||||
|
@ -0,0 +1,44 @@
|
||||
/**
|
||||
* 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 { CollectionOptions } from '@nocobase/database';
|
||||
|
||||
export default function () {
|
||||
return {
|
||||
dumpRules: 'required',
|
||||
migrationRules: ['overwrite', 'schema-only'],
|
||||
name: 'workflowTasks',
|
||||
shared: true,
|
||||
repository: 'WorkflowTasksRepository',
|
||||
fields: [
|
||||
{
|
||||
name: 'user',
|
||||
type: 'belongsTo',
|
||||
},
|
||||
{
|
||||
name: 'workflow',
|
||||
type: 'belongsTo',
|
||||
},
|
||||
{
|
||||
type: 'string',
|
||||
name: 'type',
|
||||
},
|
||||
{
|
||||
type: 'string',
|
||||
name: 'key',
|
||||
},
|
||||
],
|
||||
indexes: [
|
||||
{
|
||||
unique: true,
|
||||
fields: ['type', 'key'],
|
||||
},
|
||||
],
|
||||
} as CollectionOptions;
|
||||
}
|
@ -24,8 +24,14 @@ export default function () {
|
||||
{
|
||||
type: 'string',
|
||||
name: 'title',
|
||||
interface: 'input',
|
||||
uiSchema: {
|
||||
title: '{{t("Name")}}',
|
||||
type: 'string',
|
||||
'x-component': 'Input',
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'boolean',
|
||||
name: 'enabled',
|
||||
|
@ -0,0 +1,21 @@
|
||||
/**
|
||||
* 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 { Repository } from '@nocobase/database';
|
||||
|
||||
export default class WorkflowTasksRepository extends Repository {
|
||||
async countAll(options) {
|
||||
const db = this.database;
|
||||
return this.collection.model.findAll({
|
||||
...options,
|
||||
attributes: ['type', [db.sequelize.fn('COUNT', db.sequelize.col('type')), 'count']],
|
||||
group: ['type'],
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export default interface WorkflowTaskModel {
|
||||
type: string;
|
||||
key: string;
|
||||
|
||||
userId: number;
|
||||
|
||||
workflowId: number;
|
||||
}
|
@ -11,3 +11,4 @@ export type { default as WorkflowModel } from './Workflow';
|
||||
export type { default as FlowNodeModel } from './FlowNode';
|
||||
export type { default as ExecutionModel } from './Execution';
|
||||
export type { default as JobModel } from './Job';
|
||||
export type { default as WorkflowTaskModel } from './WorkflowTask';
|
||||
|
@ -426,9 +426,9 @@ export default {
|
||||
},
|
||||
},
|
||||
},
|
||||
'/users_jobs:list': {
|
||||
'/workflowManualTasks:list': {
|
||||
get: {
|
||||
tags: ['users_jobs'],
|
||||
tags: ['workflowManualTasks'],
|
||||
description: 'List manual jobs',
|
||||
parameters: [],
|
||||
responses: {
|
||||
@ -449,9 +449,9 @@ export default {
|
||||
},
|
||||
},
|
||||
},
|
||||
'/users_jobs:get': {
|
||||
'/workflowManualTasks:get': {
|
||||
get: {
|
||||
tags: ['users_jobs'],
|
||||
tags: ['workflowManualTasks'],
|
||||
description: 'Single user job',
|
||||
parameters: [],
|
||||
responses: {
|
||||
@ -480,9 +480,9 @@ export default {
|
||||
},
|
||||
},
|
||||
},
|
||||
'/users_jobs:submit': {
|
||||
'/workflowManualTasks:submit': {
|
||||
post: {
|
||||
tags: ['users_jobs'],
|
||||
tags: ['workflowManualTasks'],
|
||||
description: '',
|
||||
parameters: [
|
||||
{
|
||||
|
Loading…
x
Reference in New Issue
Block a user