mirror of
https://gitee.com/nocobase/nocobase.git
synced 2025-05-05 21:49:25 +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",
|
"key": "ekol7p60nry",
|
||||||
"name": "sortName",
|
"name": "sortName",
|
||||||
|
@ -20,6 +20,8 @@ import { PopupVisibleProvider, PopupVisibleProviderContext } from '../../schema-
|
|||||||
export const PopupContextProvider: React.FC<{
|
export const PopupContextProvider: React.FC<{
|
||||||
visible?: boolean;
|
visible?: boolean;
|
||||||
setVisible?: (visible: boolean) => void;
|
setVisible?: (visible: boolean) => void;
|
||||||
|
openMode?: string;
|
||||||
|
openSize?: string;
|
||||||
}> = (props) => {
|
}> = (props) => {
|
||||||
const { visible: visibleFromProps, setVisible: setVisibleFromProps } = props;
|
const { visible: visibleFromProps, setVisible: setVisibleFromProps } = props;
|
||||||
const [visible, setVisible] = useState(false);
|
const [visible, setVisible] = useState(false);
|
||||||
@ -37,8 +39,8 @@ export const PopupContextProvider: React.FC<{
|
|||||||
},
|
},
|
||||||
[setVisibleFromProps, setVisibleWithURL],
|
[setVisibleFromProps, setVisibleWithURL],
|
||||||
);
|
);
|
||||||
const openMode = fieldSchema['x-component-props']?.['openMode'] || 'drawer';
|
const openMode = props.openMode || fieldSchema['x-component-props']?.['openMode'] || 'drawer';
|
||||||
const openSize = fieldSchema['x-component-props']?.['openSize'];
|
const openSize = props.openSize || fieldSchema['x-component-props']?.['openSize'];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PopupVisibleProvider visible={false}>
|
<PopupVisibleProvider visible={false}>
|
||||||
|
@ -32,12 +32,41 @@ const pinnedPluginListClassName = css`
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
.ant-btn {
|
.ant-btn {
|
||||||
border: 0;
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
height: 46px;
|
height: 46px;
|
||||||
width: 46px;
|
width: 46px;
|
||||||
|
padding: 0;
|
||||||
|
border: 0;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
background: none;
|
background: none;
|
||||||
color: rgba(255, 255, 255, 0.65);
|
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 {
|
&:hover {
|
||||||
background: rgba(255, 255, 255, 0.1) !important;
|
background: rgba(255, 255, 255, 0.1) !important;
|
||||||
}
|
}
|
||||||
|
@ -563,7 +563,7 @@ Grid.Col = observer(
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<GridColContext.Provider value={value}>
|
<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}
|
{props.children}
|
||||||
</div>
|
</div>
|
||||||
</GridColContext.Provider>
|
</GridColContext.Provider>
|
||||||
|
@ -93,7 +93,7 @@ const useFullScreenHeight = (props?) => {
|
|||||||
return pageReservedHeight;
|
return pageReservedHeight;
|
||||||
};
|
};
|
||||||
|
|
||||||
const InternalWorkflowCollection = ['users_jobs', 'approvals', 'approvalRecords'];
|
const InternalWorkflowCollection = ['workflowManualTasks', 'approvals', 'approvalRecords'];
|
||||||
// 表格区块高度计算
|
// 表格区块高度计算
|
||||||
const useTableHeight = () => {
|
const useTableHeight = () => {
|
||||||
const { token } = theme.useToken();
|
const { token } = theme.useToken();
|
||||||
|
@ -76,10 +76,10 @@ export class Gateway extends EventEmitter {
|
|||||||
|
|
||||||
public server: http.Server | null = null;
|
public server: http.Server | null = null;
|
||||||
public ipcSocketServer: IPCSocketServer | null = null;
|
public ipcSocketServer: IPCSocketServer | null = null;
|
||||||
|
public wsServer: WSServer;
|
||||||
loggers = new Registry<SystemLogger>();
|
loggers = new Registry<SystemLogger>();
|
||||||
private port: number = process.env.APP_PORT ? parseInt(process.env.APP_PORT) : null;
|
private port: number = process.env.APP_PORT ? parseInt(process.env.APP_PORT) : null;
|
||||||
private host = '0.0.0.0';
|
private host = '0.0.0.0';
|
||||||
private wsServer: WSServer;
|
|
||||||
private socketPath = resolve(process.cwd(), 'storage', 'gateway.sock');
|
private socketPath = resolve(process.cwd(), 'storage', 'gateway.sock');
|
||||||
|
|
||||||
private constructor() {
|
private constructor() {
|
||||||
|
@ -233,7 +233,7 @@ export class WSServer extends EventEmitter {
|
|||||||
const client = this.webSocketClients.get(clientId);
|
const client = this.webSocketClients.get(clientId);
|
||||||
// remove all tags with the given tagKey
|
// remove all tags with the given tagKey
|
||||||
client.tags.forEach((tag) => {
|
client.tags.forEach((tag) => {
|
||||||
if (tag.startsWith(tagKey)) {
|
if (tag.startsWith(`${tagKey}#`)) {
|
||||||
client.tags.delete(tag);
|
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) {
|
loopThroughConnections(callback: (client: WebSocketClient) => void) {
|
||||||
this.webSocketClients.forEach((client) => {
|
this.webSocketClients.forEach((client) => {
|
||||||
callback(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",
|
"key": "ekol7p60nry",
|
||||||
"name": "sortName",
|
"name": "sortName",
|
||||||
|
@ -136,14 +136,11 @@ const InnerInbox = (props) => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Tooltip title={t('Message')}>
|
<Tooltip title={t('Message')}>
|
||||||
<Badge count={unreadMsgsCountObs.value} size="small" offset={[-12, 14]}>
|
<Button className={styles.button} onClick={onIconClick}>
|
||||||
<Button
|
<Badge count={unreadMsgsCountObs.value} size="small">
|
||||||
className={styles.button}
|
<Icon type={'BellOutlined'} />
|
||||||
title={t('Message')}
|
</Badge>
|
||||||
icon={<Icon type={'BellOutlined'} />}
|
</Button>
|
||||||
onClick={onIconClick}
|
|
||||||
/>
|
|
||||||
</Badge>
|
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Drawer
|
<Drawer
|
||||||
title={DrawerTitle}
|
title={DrawerTitle}
|
||||||
|
@ -7,14 +7,114 @@
|
|||||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { ExtendCollectionsProvider, storePopupContext, useRequest } from '@nocobase/client';
|
import { ExtendCollectionsProvider, storePopupContext } from '@nocobase/client';
|
||||||
import React, { createContext, FC, useContext } from 'react';
|
import React, { FC } from 'react';
|
||||||
import { getWorkflowTodoViewActionSchema, nodeCollection, todoCollection, workflowCollection } from './WorkflowTodo';
|
import { getWorkflowTodoViewActionSchema } from './WorkflowTodo';
|
||||||
import { JOB_STATUS } from '@nocobase/plugin-workflow/client';
|
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 而报错;
|
* 1. 扩展几个工作流相关的 collection,防止在区块中因找不到 collection 而报错;
|
||||||
@ -22,36 +122,19 @@ const ManualTaskCountRequestContext = createContext({});
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const WorkflowManualProvider: FC = (props) => {
|
export const WorkflowManualProvider: FC = (props) => {
|
||||||
const request = useRequest<any>(
|
|
||||||
{
|
|
||||||
resource: 'users_jobs',
|
|
||||||
action: 'countMine',
|
|
||||||
params: {
|
|
||||||
filter: {
|
|
||||||
status: JOB_STATUS.PENDING,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{ manual: true },
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ExtendCollectionsProvider collections={collections}>
|
<ExtendCollectionsProvider collections={[workflowCollection, todoCollection]}>
|
||||||
<ManualTaskCountRequestContext.Provider value={request}>{props.children}</ManualTaskCountRequestContext.Provider>
|
{props.children}
|
||||||
</ExtendCollectionsProvider>
|
</ExtendCollectionsProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export function useCountRequest() {
|
|
||||||
return useContext(ManualTaskCountRequestContext);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 2. 将区块相关的按钮 Schema 缓存起来,这样就可以在弹窗中获取到 Schema,进而实现“弹窗 URL”的功能;
|
* 2. 将区块相关的按钮 Schema 缓存起来,这样就可以在弹窗中获取到 Schema,进而实现“弹窗 URL”的功能;
|
||||||
*/
|
*/
|
||||||
function cacheSchema(collectionNameList: string[]) {
|
function cacheSchema(collectionNameList: string[]) {
|
||||||
collectionNameList.forEach((collectionName) => {
|
collectionNameList.forEach((collectionName) => {
|
||||||
const defaultOpenMode = isMobile() ? 'drawer' : 'page';
|
const defaultOpenMode = isMobile() ? 'page' : 'modal';
|
||||||
const workflowTodoViewActionSchema = getWorkflowTodoViewActionSchema({ defaultOpenMode, collectionName });
|
const workflowTodoViewActionSchema = getWorkflowTodoViewActionSchema({ defaultOpenMode, collectionName });
|
||||||
|
|
||||||
storePopupContext(workflowTodoViewActionSchema['x-uid'], {
|
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() {
|
function isMobile() {
|
||||||
return window.location.pathname.startsWith('/m/');
|
return window.location.pathname.startsWith('/m/');
|
||||||
|
@ -7,13 +7,19 @@
|
|||||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { observer, useField, useFieldSchema, useForm } from '@formily/react';
|
import React, { createContext, useCallback, useContext, useEffect, useState } from 'react';
|
||||||
import { Button, Space, Spin, Tag } from 'antd';
|
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 dayjs from 'dayjs';
|
||||||
import React, { createContext, useContext, useEffect, useState } from 'react';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { get } from 'lodash';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
css,
|
css,
|
||||||
|
PopupContextProvider,
|
||||||
SchemaInitializerItem,
|
SchemaInitializerItem,
|
||||||
useCollectionRecordData,
|
useCollectionRecordData,
|
||||||
useCompile,
|
useCompile,
|
||||||
@ -21,221 +27,33 @@ import {
|
|||||||
usePlugin,
|
usePlugin,
|
||||||
useSchemaInitializer,
|
useSchemaInitializer,
|
||||||
useSchemaInitializerItem,
|
useSchemaInitializerItem,
|
||||||
} from '@nocobase/client';
|
|
||||||
|
|
||||||
import {
|
|
||||||
SchemaComponent,
|
SchemaComponent,
|
||||||
SchemaComponentContext,
|
SchemaComponentContext,
|
||||||
TableBlockProvider,
|
|
||||||
useAPIClient,
|
useAPIClient,
|
||||||
useActionContext,
|
useActionContext,
|
||||||
useCurrentUserContext,
|
useCurrentUserContext,
|
||||||
useFormBlockContext,
|
useFormBlockContext,
|
||||||
useTableBlockContext,
|
useTableBlockContext,
|
||||||
|
List,
|
||||||
|
OpenModeProvider,
|
||||||
} from '@nocobase/client';
|
} from '@nocobase/client';
|
||||||
import WorkflowPlugin, {
|
import WorkflowPlugin, {
|
||||||
DetailsBlockProvider,
|
DetailsBlockProvider,
|
||||||
FlowContext,
|
FlowContext,
|
||||||
JobStatusOptions,
|
|
||||||
JobStatusOptionsMap,
|
|
||||||
linkNodes,
|
linkNodes,
|
||||||
useAvailableUpstreams,
|
useAvailableUpstreams,
|
||||||
useFlowContext,
|
useFlowContext,
|
||||||
|
EXECUTION_STATUS,
|
||||||
|
JOB_STATUS,
|
||||||
|
WorkflowTitle,
|
||||||
} from '@nocobase/plugin-workflow/client';
|
} from '@nocobase/plugin-workflow/client';
|
||||||
|
|
||||||
import { NAMESPACE, useLang } from '../locale';
|
import { NAMESPACE, useLang } from '../locale';
|
||||||
import { FormBlockProvider } from './instruction/FormBlockProvider';
|
import { FormBlockProvider } from './instruction/FormBlockProvider';
|
||||||
import { ManualFormType, manualFormTypes } from './instruction/SchemaConfig';
|
import { ManualFormType, manualFormTypes } from './instruction/SchemaConfig';
|
||||||
import { TableOutlined } from '@ant-design/icons';
|
import { TaskStatusOptionsMap } from '../common/constants';
|
||||||
|
|
||||||
export const nodeCollection = {
|
function TaskStatusColumn(props) {
|
||||||
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) {
|
|
||||||
const recordData = useCollectionRecordData();
|
const recordData = useCollectionRecordData();
|
||||||
const labelUnprocessed = useLang('Unprocessed');
|
const labelUnprocessed = useLang('Unprocessed');
|
||||||
if (recordData?.execution?.status && !recordData?.status) {
|
if (recordData?.execution?.status && !recordData?.status) {
|
||||||
@ -244,172 +62,141 @@ function UserJobStatusColumn(props) {
|
|||||||
return props.children;
|
return props.children;
|
||||||
}
|
}
|
||||||
|
|
||||||
const tableColumns = {
|
function RecordTitle(props) {
|
||||||
title: {
|
const record = useCollectionRecordData();
|
||||||
type: 'void',
|
if (Array.isArray(props.dataIndex)) {
|
||||||
'x-decorator': 'TableV2.Column.Decorator',
|
for (const index of props.dataIndex) {
|
||||||
'x-component': 'TableV2.Column',
|
const title = get(record, index);
|
||||||
'x-component-props': {
|
if (title) {
|
||||||
width: null,
|
return title;
|
||||||
},
|
}
|
||||||
title: `{{t("Task title", { ns: "${NAMESPACE}" })}}`,
|
}
|
||||||
properties: {
|
}
|
||||||
title: {
|
return get(record, props.dataIndex);
|
||||||
'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,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const WorkflowTodo: React.FC<{ columns?: string[] }> & {
|
export const WorkflowTodo: React.FC & {
|
||||||
Initializer: React.FC;
|
Initializer: React.FC;
|
||||||
Drawer: React.FC;
|
Drawer: React.FC;
|
||||||
Decorator: React.FC;
|
Decorator: React.FC;
|
||||||
TaskBlock: React.FC;
|
// TaskBlock: React.FC;
|
||||||
} = (props) => {
|
} = (props) => {
|
||||||
const { columns = Object.keys(tableColumns) } = props;
|
|
||||||
const { defaultOpenMode } = useOpenModeContext();
|
const { defaultOpenMode } = useOpenModeContext();
|
||||||
|
const viewSchema = getWorkflowTodoViewActionSchema({ defaultOpenMode, collectionName: 'workflowManualTasks' });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SchemaComponent
|
<SchemaComponentContext.Provider value={{ designable: false }}>
|
||||||
components={{
|
<SchemaComponent
|
||||||
NodeColumn,
|
scope={{
|
||||||
WorkflowColumn,
|
useCollectionRecordData,
|
||||||
UserColumn,
|
}}
|
||||||
UserJobStatusColumn,
|
components={{
|
||||||
}}
|
FormLayout,
|
||||||
schema={{
|
// WorkflowColumn,
|
||||||
type: 'void',
|
// UserColumn,
|
||||||
properties: {
|
ContentDetailWithTitle,
|
||||||
actions: {
|
}}
|
||||||
type: 'void',
|
schema={{
|
||||||
'x-component': 'ActionBar',
|
type: 'void',
|
||||||
'x-component-props': {
|
properties: {
|
||||||
style: {
|
actions: {
|
||||||
marginBottom: 'var(--nb-spacing)',
|
type: 'void',
|
||||||
|
'x-component': 'ActionBar',
|
||||||
|
'x-component-props': {
|
||||||
|
style: {
|
||||||
|
marginBottom: 'var(--nb-spacing)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
properties: {
|
||||||
|
filter: {
|
||||||
|
type: 'void',
|
||||||
|
title: '{{ t("Filter") }}',
|
||||||
|
'x-action': 'filter',
|
||||||
|
'x-designer': 'Filter.Action.Designer',
|
||||||
|
'x-component': 'Filter.Action',
|
||||||
|
'x-use-component-props': 'useFilterActionProps',
|
||||||
|
'x-component-props': {
|
||||||
|
icon: 'FilterOutlined',
|
||||||
|
},
|
||||||
|
default: {
|
||||||
|
$and: [{ title: { $includes: '' } }, { 'workflow.title': { $includes: '' } }],
|
||||||
|
},
|
||||||
|
'x-align': 'left',
|
||||||
|
},
|
||||||
|
refresher: {
|
||||||
|
type: 'void',
|
||||||
|
title: '{{ t("Refresh") }}',
|
||||||
|
'x-action': 'refresh',
|
||||||
|
'x-component': 'Action',
|
||||||
|
'x-use-component-props': 'useRefreshActionProps',
|
||||||
|
// 'x-designer': 'Action.Designer',
|
||||||
|
'x-toolbar': 'ActionSchemaToolbar',
|
||||||
|
'x-settings': 'actionSettings:refresh',
|
||||||
|
'x-component-props': {
|
||||||
|
icon: 'ReloadOutlined',
|
||||||
|
},
|
||||||
|
'x-align': 'right',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
properties: {
|
list: {
|
||||||
filter: {
|
type: 'array',
|
||||||
type: 'void',
|
'x-component': 'List',
|
||||||
title: '{{ t("Filter") }}',
|
// 'x-use-component-props': 'useListBlockProps',
|
||||||
'x-action': 'filter',
|
properties: {
|
||||||
'x-designer': 'Filter.Action.Designer',
|
item: {
|
||||||
'x-component': 'Filter.Action',
|
type: 'object',
|
||||||
'x-use-component-props': 'useFilterActionProps',
|
'x-component': 'List.Item',
|
||||||
'x-component-props': {
|
properties: {
|
||||||
icon: 'FilterOutlined',
|
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-component': 'ActionBar',
|
||||||
|
'x-use-component-props': 'useListActionBarProps',
|
||||||
|
'x-component-props': {
|
||||||
|
layout: 'one-column',
|
||||||
|
},
|
||||||
|
'x-align': 'left',
|
||||||
|
properties: {
|
||||||
|
view: viewSchema,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
'x-align': 'left',
|
|
||||||
},
|
|
||||||
refresher: {
|
|
||||||
type: 'void',
|
|
||||||
title: '{{ t("Refresh") }}',
|
|
||||||
'x-action': 'refresh',
|
|
||||||
'x-component': 'Action',
|
|
||||||
'x-use-component-props': 'useRefreshActionProps',
|
|
||||||
// 'x-designer': 'Action.Designer',
|
|
||||||
'x-toolbar': 'ActionSchemaToolbar',
|
|
||||||
'x-settings': 'actionSettings:refresh',
|
|
||||||
'x-component-props': {
|
|
||||||
icon: 'ReloadOutlined',
|
|
||||||
},
|
|
||||||
'x-align': 'right',
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
table: {
|
}}
|
||||||
type: 'array',
|
/>
|
||||||
'x-component': 'TableV2',
|
</SchemaComponentContext.Provider>
|
||||||
'x-use-component-props': 'useTableBlockProps',
|
|
||||||
'x-component-props': {
|
|
||||||
rowKey: 'id',
|
|
||||||
},
|
|
||||||
properties: {
|
|
||||||
actions: {
|
|
||||||
type: 'void',
|
|
||||||
'x-decorator': 'TableV2.Column.Decorator',
|
|
||||||
'x-component': 'TableV2.Column',
|
|
||||||
'x-component-props': {
|
|
||||||
width: 60,
|
|
||||||
},
|
|
||||||
title: '{{t("Actions")}}',
|
|
||||||
properties: {
|
|
||||||
view: getWorkflowTodoViewActionSchema({ defaultOpenMode, collectionName: 'users_jobs' }),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
...columns.reduce((schema, key) => {
|
|
||||||
schema[key] = tableColumns[key];
|
|
||||||
return schema;
|
|
||||||
}, {}),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -518,7 +305,7 @@ function useSubmit() {
|
|||||||
field.data = field.data || {};
|
field.data = field.data || {};
|
||||||
field.data.loading = true;
|
field.data.loading = true;
|
||||||
|
|
||||||
await api.resource('users_jobs').submit({
|
await api.resource('workflowManualTasks').submit({
|
||||||
filterByTk: userJob.id,
|
filterByTk: userJob.id,
|
||||||
values: {
|
values: {
|
||||||
result: { [formKey]: { ...values, ...assignedValues.values }, _: actionKey },
|
result: { [formKey]: { ...values, ...assignedValues.values }, _: actionKey },
|
||||||
@ -545,7 +332,7 @@ function FlowContextProvider(props) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
api
|
api
|
||||||
.resource('users_jobs')
|
.resource('workflowManualTasks')
|
||||||
.get?.({
|
.get?.({
|
||||||
filterByTk: id,
|
filterByTk: id,
|
||||||
appends: ['node', 'job', 'workflow', 'workflow.nodes', 'execution', 'execution.jobs'],
|
appends: ['node', 'job', 'workflow', 'workflow.nodes', 'execution', 'execution.jobs'],
|
||||||
@ -638,19 +425,18 @@ function useDetailsBlockProps() {
|
|||||||
function FooterStatus() {
|
function FooterStatus() {
|
||||||
const compile = useCompile();
|
const compile = useCompile();
|
||||||
const { status, updatedAt } = useCollectionRecordData() || {};
|
const { status, updatedAt } = useCollectionRecordData() || {};
|
||||||
const statusOption = JobStatusOptionsMap[status];
|
const statusOption = TaskStatusOptionsMap[status];
|
||||||
return status ? (
|
return status ? (
|
||||||
<Space>
|
<Space
|
||||||
<time
|
className={css`
|
||||||
className={css`
|
margin-bottom: 1em;
|
||||||
|
time {
|
||||||
margin-right: 0.5em;
|
margin-right: 0.5em;
|
||||||
`}
|
}
|
||||||
>
|
`}
|
||||||
{dayjs(updatedAt).format('YYYY-MM-DD HH:mm:ss')}
|
>
|
||||||
</time>
|
<time>{dayjs(updatedAt).format('YYYY-MM-DD HH:mm:ss')}</time>
|
||||||
<Tag icon={statusOption.icon} color={statusOption.color}>
|
<Tag color={statusOption.color}>{compile(statusOption.label)}</Tag>
|
||||||
{compile(statusOption.label)}
|
|
||||||
</Tag>
|
|
||||||
</Space>
|
</Space>
|
||||||
) : null;
|
) : null;
|
||||||
}
|
}
|
||||||
@ -699,8 +485,8 @@ function Drawer() {
|
|||||||
function Decorator(props) {
|
function Decorator(props) {
|
||||||
const { params = {}, children } = props;
|
const { params = {}, children } = props;
|
||||||
const blockProps = {
|
const blockProps = {
|
||||||
collection: 'users_jobs',
|
collection: 'workflowManualTasks',
|
||||||
resource: 'users_jobs',
|
resource: 'workflowManualTasks',
|
||||||
action: 'list',
|
action: 'list',
|
||||||
params: {
|
params: {
|
||||||
pageSize: 20,
|
pageSize: 20,
|
||||||
@ -712,15 +498,12 @@ function Decorator(props) {
|
|||||||
appends: ['user', 'node', 'workflow', 'execution.status'],
|
appends: ['user', 'node', 'workflow', 'execution.status'],
|
||||||
except: ['node.config', 'workflow.config', 'workflow.options'],
|
except: ['node.config', 'workflow.config', 'workflow.options'],
|
||||||
},
|
},
|
||||||
rowKey: 'id',
|
|
||||||
showIndex: true,
|
|
||||||
dragSort: false,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableBlockProvider name="workflow-todo" {...blockProps}>
|
<OpenModeProvider defaultOpenMode="modal">
|
||||||
{children}
|
<List.Decorator {...blockProps}>{children}</List.Decorator>
|
||||||
</TableBlockProvider>
|
</OpenModeProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -754,43 +537,192 @@ function Initializer() {
|
|||||||
WorkflowTodo.Initializer = Initializer;
|
WorkflowTodo.Initializer = Initializer;
|
||||||
WorkflowTodo.Drawer = Drawer;
|
WorkflowTodo.Drawer = Drawer;
|
||||||
WorkflowTodo.Decorator = Decorator;
|
WorkflowTodo.Decorator = Decorator;
|
||||||
WorkflowTodo.TaskBlock = TaskBlock;
|
|
||||||
|
|
||||||
function TaskBlock() {
|
function ContentDetail(props) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const token = useAntdToken();
|
||||||
|
return (
|
||||||
|
<ConfigProvider
|
||||||
|
theme={{
|
||||||
|
token: {
|
||||||
|
fontSizeLG: 14,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Descriptions
|
||||||
|
{...props}
|
||||||
|
column={1}
|
||||||
|
items={[
|
||||||
|
{
|
||||||
|
key: 'createdAt',
|
||||||
|
label: t('Created at'),
|
||||||
|
children: (
|
||||||
|
<SchemaComponent
|
||||||
|
schema={{
|
||||||
|
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 { data: user } = useCurrentUserContext();
|
||||||
|
const filter = StatusFilterMap[status] ?? {};
|
||||||
|
return {
|
||||||
|
filter: {
|
||||||
|
...filter,
|
||||||
|
userId: user?.data?.id,
|
||||||
|
},
|
||||||
|
appends: [
|
||||||
|
'job.id',
|
||||||
|
'job.status',
|
||||||
|
'job.result',
|
||||||
|
'workflow.id',
|
||||||
|
'workflow.title',
|
||||||
|
'workflow.enabled',
|
||||||
|
'execution.id',
|
||||||
|
'execution.status',
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function TodoExtraActions() {
|
||||||
return (
|
return (
|
||||||
<SchemaComponent
|
<SchemaComponent
|
||||||
components={{
|
|
||||||
WorkflowTodo,
|
|
||||||
}}
|
|
||||||
schema={{
|
schema={{
|
||||||
name: 'todos',
|
name: 'actions',
|
||||||
type: 'void',
|
type: 'void',
|
||||||
'x-decorator': 'WorkflowTodo.Decorator',
|
'x-component': 'ActionBar',
|
||||||
'x-decorator-props': {
|
|
||||||
params: {
|
|
||||||
filter: {
|
|
||||||
userId: user?.data?.id,
|
|
||||||
},
|
|
||||||
appends: [
|
|
||||||
'job.id',
|
|
||||||
'job.status',
|
|
||||||
'job.result',
|
|
||||||
'workflow.id',
|
|
||||||
'workflow.title',
|
|
||||||
'workflow.enabled',
|
|
||||||
'execution.id',
|
|
||||||
'execution.status',
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
'x-component': 'CardItem',
|
|
||||||
properties: {
|
properties: {
|
||||||
todos: {
|
refresh: {
|
||||||
type: 'void',
|
type: 'void',
|
||||||
'x-component': 'WorkflowTodo',
|
title: '{{ t("Refresh") }}',
|
||||||
|
'x-component': 'Action',
|
||||||
|
'x-use-component-props': 'useRefreshActionProps',
|
||||||
'x-component-props': {
|
'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.
|
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
* 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.getByRole('menuitem', { name: 'check-square Workflow todos' }).click();
|
||||||
await page.mouse.move(300, 0, { steps: 100 });
|
await page.mouse.move(300, 0, { steps: 100 });
|
||||||
await page.waitForTimeout(300);
|
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位小数
|
// 定义获取2位小数
|
||||||
const manualNodeRecord = faker.number.float({ min: 0, max: 100, precision: 2 });
|
const manualNodeRecord = faker.number.float({ min: 0, max: 100, precision: 2 });
|
||||||
await page.getByRole('spinbutton').fill(manualNodeRecord.toString());
|
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.getByRole('menuitem', { name: 'check-square Workflow todos' }).click();
|
||||||
await page.mouse.move(300, 0, { steps: 100 });
|
await page.mouse.move(300, 0, { steps: 100 });
|
||||||
await page.waitForTimeout(300);
|
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();
|
// const manualNodeRecord = faker.number.float();
|
||||||
await page.getByRole('checkbox').check();
|
await page.getByRole('checkbox').check();
|
||||||
await page.getByRole('button', { name: 'Continue the process' }).click();
|
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.getByRole('menuitem', { name: 'check-square Workflow todos' }).click();
|
||||||
await page.mouse.move(300, 0, { steps: 100 });
|
await page.mouse.move(300, 0, { steps: 100 });
|
||||||
await page.waitForTimeout(300);
|
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.getByTestId('select-single').click();
|
||||||
await page.getByRole('option', { name: '存续' }).click();
|
await page.getByRole('option', { name: '存续' }).click();
|
||||||
await page.getByRole('button', { name: 'Continue the process' }).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.getByRole('menuitem', { name: 'check-square Workflow todos' }).click();
|
||||||
await page.mouse.move(300, 0, { steps: 100 });
|
await page.mouse.move(300, 0, { steps: 100 });
|
||||||
await page.waitForTimeout(300);
|
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();
|
const manualNodeRecord = manualNodeFieldDisplayName + dayjs().format('YYYYMMDDHHmmss.SSS').toString();
|
||||||
await page.getByRole('textbox').fill(manualNodeRecord);
|
await page.getByRole('textbox').fill(manualNodeRecord);
|
||||||
await page.getByRole('button', { name: 'Continue the process' }).click();
|
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.getByRole('menuitem', { name: 'check-square Workflow todos' }).click();
|
||||||
await page.mouse.move(300, 0, { steps: 100 });
|
await page.mouse.move(300, 0, { steps: 100 });
|
||||||
await page.waitForTimeout(300);
|
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();
|
const manualNodeRecord = manualNodeFieldDisplayName + dayjs().format('YYYYMMDDHHmmss.SSS').toString();
|
||||||
await page.getByRole('textbox').fill(manualNodeRecord);
|
await page.getByRole('textbox').fill(manualNodeRecord);
|
||||||
await page.getByRole('button', { name: 'Terminate the process' }).click();
|
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.getByRole('menuitem', { name: 'check-square Workflow todos' }).click();
|
||||||
await page.mouse.move(300, 0, { steps: 100 });
|
await page.mouse.move(300, 0, { steps: 100 });
|
||||||
await page.waitForTimeout(300);
|
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();
|
const manualNodeRecord = manualNodeFieldDisplayName + dayjs().format('YYYYMMDDHHmmss.SSS').toString();
|
||||||
await page.getByRole('textbox').fill(manualNodeRecord);
|
await page.getByRole('textbox').fill(manualNodeRecord);
|
||||||
await page.getByRole('button', { name: 'Save temporarily' }).click();
|
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.getByRole('menuitem', { name: 'check-square Workflow todos' }).click();
|
||||||
await page.mouse.move(300, 0, { steps: 100 });
|
await page.mouse.move(300, 0, { steps: 100 });
|
||||||
await page.waitForTimeout(300);
|
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.getByTestId('select-multiple').click();
|
||||||
await page.getByRole('option', { name: '软件销售', exact: true }).click();
|
await page.getByRole('option', { name: '软件销售', exact: true }).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.getByRole('menuitem', { name: 'check-square Workflow todos' }).click();
|
||||||
await page.mouse.move(300, 0, { steps: 100 });
|
await page.mouse.move(300, 0, { steps: 100 });
|
||||||
await page.waitForTimeout(300);
|
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.getByLabel('存续').check();
|
||||||
await page.getByRole('button', { name: 'Continue the process' }).click();
|
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.getByRole('menuitem', { name: 'check-square Workflow todos' }).click();
|
||||||
await page.mouse.move(300, 0, { steps: 100 });
|
await page.mouse.move(300, 0, { steps: 100 });
|
||||||
await page.waitForTimeout(300);
|
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.getByLabel('软件开发', { exact: true }).check();
|
await page.getByLabel('软件开发', { exact: true }).check();
|
||||||
await page.getByRole('button', { name: 'Continue the process' }).click();
|
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.getByRole('menuitem', { name: 'check-square Workflow todos' }).click();
|
||||||
await page.mouse.move(300, 0, { steps: 100 });
|
await page.mouse.move(300, 0, { steps: 100 });
|
||||||
await page.waitForTimeout(300);
|
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');
|
const manualNodeRecord = dayjs().format('YYYY-MM-DD');
|
||||||
await page.getByPlaceholder('Select date').click();
|
await page.getByPlaceholder('Select date').click();
|
||||||
await page.getByTitle(manualNodeRecord.toString()).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.getByRole('menuitem', { name: 'check-square Workflow todos' }).click();
|
||||||
await page.mouse.move(300, 0, { steps: 100 });
|
await page.mouse.move(300, 0, { steps: 100 });
|
||||||
await page.waitForTimeout(300);
|
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();
|
const manualNodeRecord = manualNodeFieldDisplayName + dayjs().format('YYYYMMDDHHmmss.SSS').toString();
|
||||||
await page.getByRole('textbox').fill(manualNodeRecord);
|
await page.getByRole('textbox').fill(manualNodeRecord);
|
||||||
await page.getByRole('button', { name: 'Continue the process' }).click();
|
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.getByRole('menuitem', { name: 'check-square Workflow todos' }).click();
|
||||||
await page.mouse.move(300, 0, { steps: 100 });
|
await page.mouse.move(300, 0, { steps: 100 });
|
||||||
await page.waitForTimeout(300);
|
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();
|
const manualNodeRecord = manualNodeFieldDisplayName + dayjs().format('YYYYMMDDHHmmss.SSS').toString();
|
||||||
await page.getByRole('textbox').fill(manualNodeRecord);
|
await page.getByRole('textbox').fill(manualNodeRecord);
|
||||||
await page.getByRole('button', { name: 'Continue the process' }).click();
|
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.getByRole('menuitem', { name: 'check-square Workflow todos' }).click();
|
||||||
await page.mouse.move(300, 0, { steps: 100 });
|
await page.mouse.move(300, 0, { steps: 100 });
|
||||||
await page.waitForTimeout(300);
|
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();
|
const manualNodeRecord = manualNodeFieldDisplayName + dayjs().format('YYYYMMDDHHmmss.SSS').toString();
|
||||||
await page.getByRole('textbox').fill(manualNodeRecord);
|
await page.getByRole('textbox').fill(manualNodeRecord);
|
||||||
await page.getByRole('button', { name: 'Continue the process' }).click();
|
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.getByRole('menuitem', { name: 'check-square Workflow todos' }).click();
|
||||||
await page.mouse.move(300, 0, { steps: 100 });
|
await page.mouse.move(300, 0, { steps: 100 });
|
||||||
await page.waitForTimeout(300);
|
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();
|
const manualNodeRecord = faker.internet.email();
|
||||||
await page.getByRole('textbox').fill(manualNodeRecord);
|
await page.getByRole('textbox').fill(manualNodeRecord);
|
||||||
await page.getByRole('button', { name: 'Continue the process' }).click();
|
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.getByRole('menuitem', { name: 'check-square Workflow todos' }).click();
|
||||||
await page.mouse.move(300, 0, { steps: 100 });
|
await page.mouse.move(300, 0, { steps: 100 });
|
||||||
await page.waitForTimeout(300);
|
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();
|
const manualNodeRecord = faker.number.int();
|
||||||
await page.getByRole('spinbutton').fill(manualNodeRecord.toString());
|
await page.getByRole('spinbutton').fill(manualNodeRecord.toString());
|
||||||
await page.getByRole('button', { name: 'Continue the process' }).click();
|
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.getByRole('menuitem', { name: 'check-square Workflow todos' }).click();
|
||||||
await page.mouse.move(300, 0, { steps: 100 });
|
await page.mouse.move(300, 0, { steps: 100 });
|
||||||
await page.waitForTimeout(300);
|
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 });
|
const manualNodeRecord = faker.number.float({ min: 0, max: 999999999, precision: 2 });
|
||||||
await page.getByRole('spinbutton').fill(manualNodeRecord.toString());
|
await page.getByRole('spinbutton').fill(manualNodeRecord.toString());
|
||||||
await page.getByRole('button', { name: 'Continue the process' }).click();
|
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.getByRole('menuitem', { name: 'check-square Workflow todos' }).click();
|
||||||
await page.mouse.move(300, 0, { steps: 100 });
|
await page.mouse.move(300, 0, { steps: 100 });
|
||||||
await page.waitForTimeout(300);
|
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 });
|
const manualNodeRecord = faker.number.float({ min: 0, max: 100, precision: 2 });
|
||||||
await page.getByRole('spinbutton').fill(manualNodeRecord.toString());
|
await page.getByRole('spinbutton').fill(manualNodeRecord.toString());
|
||||||
await page.getByRole('button', { name: 'Continue the process' }).click();
|
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.getByRole('menuitem', { name: 'check-square Workflow todos' }).click();
|
||||||
await page.mouse.move(300, 0, { steps: 100 });
|
await page.mouse.move(300, 0, { steps: 100 });
|
||||||
await page.waitForTimeout(300);
|
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 });
|
// const manualNodeRecord = faker.number.float({ min: 0, max: 100, precision: 2 });
|
||||||
await page.getByRole('checkbox').check();
|
await page.getByRole('checkbox').check();
|
||||||
await page.getByRole('button', { name: 'Continue the process' }).click();
|
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.getByRole('menuitem', { name: 'check-square Workflow todos' }).click();
|
||||||
await page.mouse.move(300, 0, { steps: 100 });
|
await page.mouse.move(300, 0, { steps: 100 });
|
||||||
await page.waitForTimeout(300);
|
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 });
|
// const manualNodeRecord = faker.number.float({ min: 0, max: 100, precision: 2 });
|
||||||
await page.getByTestId('select-single').click();
|
await page.getByTestId('select-single').click();
|
||||||
await page.getByRole('option', { name: '存续' }).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.getByRole('menuitem', { name: 'check-square Workflow todos' }).click();
|
||||||
await page.mouse.move(300, 0, { steps: 100 });
|
await page.mouse.move(300, 0, { steps: 100 });
|
||||||
await page.waitForTimeout(300);
|
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.getByTestId('select-multiple').click();
|
||||||
await page.getByRole('option', { name: '软件销售', exact: true }).click();
|
await page.getByRole('option', { name: '软件销售', exact: true }).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.getByRole('menuitem', { name: 'check-square Workflow todos' }).click();
|
||||||
await page.mouse.move(300, 0, { steps: 100 });
|
await page.mouse.move(300, 0, { steps: 100 });
|
||||||
await page.waitForTimeout(300);
|
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 });
|
// const manualNodeRecord = faker.number.float({ min: 0, max: 100, precision: 2 });
|
||||||
await page.getByLabel('存续').check();
|
await page.getByLabel('存续').check();
|
||||||
await page.getByRole('button', { name: 'Continue the process' }).click();
|
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.getByRole('menuitem', { name: 'check-square Workflow todos' }).click();
|
||||||
await page.mouse.move(300, 0, { steps: 100 });
|
await page.mouse.move(300, 0, { steps: 100 });
|
||||||
await page.waitForTimeout(300);
|
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.getByLabel('软件开发', { exact: true }).check();
|
await page.getByLabel('软件开发', { exact: true }).check();
|
||||||
await page.getByRole('button', { name: 'Continue the process' }).click();
|
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.getByRole('menuitem', { name: 'check-square Workflow todos' }).click();
|
||||||
await page.mouse.move(300, 0, { steps: 100 });
|
await page.mouse.move(300, 0, { steps: 100 });
|
||||||
await page.waitForTimeout(300);
|
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');
|
const manualNodeRecord = dayjs().format('YYYY-MM-DD');
|
||||||
await page.getByPlaceholder('Select date').click();
|
await page.getByPlaceholder('Select date').click();
|
||||||
await page.getByTitle(manualNodeRecord.toString()).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.getByRole('menuitem', { name: 'check-square Workflow todos' }).click();
|
||||||
await page.mouse.move(300, 0, { steps: 100 });
|
await page.mouse.move(300, 0, { steps: 100 });
|
||||||
await page.waitForTimeout(300);
|
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();
|
const manualNodeRecord = manualNodeFieldDisplayName + dayjs().format('YYYYMMDDHHmmss.SSS').toString();
|
||||||
await page.getByRole('textbox').fill(manualNodeRecord);
|
await page.getByRole('textbox').fill(manualNodeRecord);
|
||||||
await page.getByRole('button', { name: 'Continue the process' }).click();
|
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.getByRole('menuitem', { name: 'check-square Workflow todos' }).click();
|
||||||
await page.mouse.move(300, 0, { steps: 100 });
|
await page.mouse.move(300, 0, { steps: 100 });
|
||||||
await page.waitForTimeout(300);
|
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();
|
const manualNodeRecord = manualNodeFieldDisplayName + dayjs().format('YYYYMMDDHHmmss.SSS').toString();
|
||||||
await page.getByRole('textbox').fill(manualNodeRecord);
|
await page.getByRole('textbox').fill(manualNodeRecord);
|
||||||
await page.getByRole('button', { name: 'Continue the process' }).click();
|
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.getByRole('menuitem', { name: 'check-square Workflow todos' }).click();
|
||||||
await page.mouse.move(300, 0, { steps: 100 });
|
await page.mouse.move(300, 0, { steps: 100 });
|
||||||
await page.waitForTimeout(300);
|
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();
|
const manualNodeRecord = manualNodeFieldDisplayName + dayjs().format('YYYYMMDDHHmmss.SSS').toString();
|
||||||
await page.getByRole('textbox').fill(manualNodeRecord);
|
await page.getByRole('textbox').fill(manualNodeRecord);
|
||||||
await page.getByRole('button', { name: 'Continue the process' }).click();
|
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.getByRole('menuitem', { name: 'check-square Workflow todos' }).click();
|
||||||
await page.mouse.move(300, 0, { steps: 100 });
|
await page.mouse.move(300, 0, { steps: 100 });
|
||||||
await page.waitForTimeout(300);
|
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();
|
const manualNodeRecord = faker.internet.email();
|
||||||
await page.getByRole('textbox').fill(manualNodeRecord);
|
await page.getByRole('textbox').fill(manualNodeRecord);
|
||||||
await page.getByRole('button', { name: 'Continue the process' }).click();
|
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.getByRole('menuitem', { name: 'check-square Workflow todos' }).click();
|
||||||
await page.mouse.move(300, 0, { steps: 100 });
|
await page.mouse.move(300, 0, { steps: 100 });
|
||||||
await page.waitForTimeout(300);
|
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();
|
const manualNodeRecord = faker.number.int();
|
||||||
await page.getByRole('spinbutton').fill(manualNodeRecord.toString());
|
await page.getByRole('spinbutton').fill(manualNodeRecord.toString());
|
||||||
await page.getByRole('button', { name: 'Continue the process' }).click();
|
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.getByRole('menuitem', { name: 'check-square Workflow todos' }).click();
|
||||||
await page.mouse.move(300, 0, { steps: 100 });
|
await page.mouse.move(300, 0, { steps: 100 });
|
||||||
await page.waitForTimeout(300);
|
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();
|
const manualNodeRecord = faker.number.float();
|
||||||
await page.getByRole('spinbutton').fill(manualNodeRecord.toString());
|
await page.getByRole('spinbutton').fill(manualNodeRecord.toString());
|
||||||
await page.getByRole('button', { name: 'Continue the process' }).click();
|
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.getByRole('menuitem', { name: 'check-square Workflow todos' }).click();
|
||||||
await page.mouse.move(300, 0, { steps: 100 });
|
await page.mouse.move(300, 0, { steps: 100 });
|
||||||
await page.waitForTimeout(300);
|
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();
|
await expect(page.getByText(triggerNodeCollectionRecordOne)).toBeAttached();
|
||||||
// 4、后置处理:删除工作流
|
// 4、后置处理:删除工作流
|
||||||
await apiDeleteWorkflow(workflowId);
|
await apiDeleteWorkflow(workflowId);
|
||||||
@ -301,11 +301,11 @@ test.describe('field data', () => {
|
|||||||
await page.getByRole('menuitem', { name: 'check-square Workflow todos' }).click();
|
await page.getByRole('menuitem', { name: 'check-square Workflow todos' }).click();
|
||||||
await page.mouse.move(300, 0, { steps: 100 });
|
await page.mouse.move(300, 0, { steps: 100 });
|
||||||
await page.waitForTimeout(300);
|
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.getByText('8')).toBeAttached();
|
||||||
await expect(
|
await expect(
|
||||||
page
|
page
|
||||||
.getByLabel(`block-item-CardItem-users_jobs-${preAggregateNodeTitle}`)
|
.getByLabel(`block-item-CardItem-workflowManualTasks-${preAggregateNodeTitle}`)
|
||||||
.locator('.ant-card-body')
|
.locator('.ant-card-body')
|
||||||
.getByText('8'),
|
.getByText('8'),
|
||||||
).toBeAttached();
|
).toBeAttached();
|
||||||
|
@ -174,18 +174,22 @@ test.describe('field data', () => {
|
|||||||
await page.getByRole('menuitem', { name: 'check-square Workflow todos' }).click();
|
await page.getByRole('menuitem', { name: 'check-square Workflow todos' }).click();
|
||||||
await page.mouse.move(300, 0, { steps: 100 });
|
await page.mouse.move(300, 0, { steps: 100 });
|
||||||
await page.waitForTimeout(300);
|
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();
|
const preManualNodeRecord = triggerNodeFieldDisplayName + dayjs().format('YYYYMMDDHHmmss.SSS').toString();
|
||||||
await page.getByRole('textbox').fill(preManualNodeRecord);
|
await page.getByRole('textbox').fill(preManualNodeRecord);
|
||||||
await page.getByRole('button', { name: 'Continue the process' }).click();
|
await page.getByRole('button', { name: 'Continue the process' }).click();
|
||||||
await page.getByLabel('action-Filter.Action-Filter-filter-users_jobs-workflow-todo').click();
|
await page.waitForTimeout(300);
|
||||||
await page.getByText('Add condition', { exact: true }).click();
|
await page.getByLabel('action-Filter.Action-Filter-').click();
|
||||||
await page.getByTestId('select-filter-field').click();
|
// await page.getByText('Add condition', { exact: true }).click();
|
||||||
await page.getByRole('menuitemcheckbox', { name: 'Task right' }).click();
|
// await page.getByRole('button', { name: 'Task title' }).click();
|
||||||
await page.getByRole('menuitemcheckbox', { name: 'Title' }).click();
|
// await page.getByRole('menuitemcheckbox', { name: 'Task title' }).click();
|
||||||
await page.getByRole('textbox').fill(manualNodeName);
|
await page.getByRole('textbox').first().fill(manualNodeName);
|
||||||
await page.getByRole('button', { name: 'Submit' }).click();
|
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();
|
await expect(page.getByText(preManualNodeRecord)).toBeAttached();
|
||||||
// 4、后置处理:删除工作流
|
// 4、后置处理:删除工作流
|
||||||
await apiDeleteWorkflow(workflowId);
|
await apiDeleteWorkflow(workflowId);
|
||||||
@ -330,18 +334,21 @@ test.describe('field data', () => {
|
|||||||
await page.getByRole('menuitem', { name: 'check-square Workflow todos' }).click();
|
await page.getByRole('menuitem', { name: 'check-square Workflow todos' }).click();
|
||||||
await page.mouse.move(300, 0, { steps: 100 });
|
await page.mouse.move(300, 0, { steps: 100 });
|
||||||
await page.waitForTimeout(300);
|
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();
|
const preManualNodeRecord = triggerNodeFieldDisplayName + dayjs().format('YYYYMMDDHHmmss.SSS').toString();
|
||||||
await page.getByRole('textbox').fill(preManualNodeRecord);
|
await page.getByRole('textbox').fill(preManualNodeRecord);
|
||||||
await page.getByRole('button', { name: 'Continue the process' }).click();
|
await page.getByRole('button', { name: 'Continue the process' }).click();
|
||||||
await page.getByLabel('action-Filter.Action-Filter-filter-users_jobs-workflow-todo').click();
|
await page.getByLabel('action-Filter.Action-Filter-').click();
|
||||||
await page.getByText('Add condition', { exact: true }).click();
|
// await page.getByText('Add condition', { exact: true }).click();
|
||||||
await page.getByTestId('select-filter-field').click();
|
// await page.getByTestId('select-filter-field').click();
|
||||||
await page.getByRole('menuitemcheckbox', { name: 'Task right' }).click();
|
// await page.getByRole('menuitemcheckbox', { name: 'Task title' }).click();
|
||||||
await page.getByRole('menuitemcheckbox', { name: 'Title' }).click();
|
await page.getByRole('textbox').first().fill(manualNodeName);
|
||||||
await page.getByRole('textbox').fill(manualNodeName);
|
|
||||||
await page.getByRole('button', { name: 'Submit' }).click();
|
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();
|
await expect(page.getByText(preManualNodeRecord)).toBeAttached();
|
||||||
|
|
||||||
const createNodeCollectionData = await apiGetList(preManualNodeCollectionName);
|
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.getByRole('menuitem', { name: 'check-square Workflow todos' }).click();
|
||||||
await page.mouse.move(300, 0, { steps: 100 });
|
await page.mouse.move(300, 0, { steps: 100 });
|
||||||
await page.waitForTimeout(300);
|
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();
|
const preManualNodeRecord = triggerNodeFieldDisplayName + dayjs().format('YYYYMMDDHHmmss.SSS').toString();
|
||||||
await page.getByRole('textbox').fill(preManualNodeRecord);
|
await page.getByRole('textbox').fill(preManualNodeRecord);
|
||||||
await page.getByRole('button', { name: 'Continue the process' }).click();
|
await page.getByRole('button', { name: 'Continue the process' }).click();
|
||||||
await page.getByLabel('action-Filter.Action-Filter-filter-users_jobs-workflow-todo').click();
|
await page.getByLabel('action-Filter.Action-Filter-').click();
|
||||||
await page.getByText('Add condition', { exact: true }).click();
|
// await page.getByText('Add condition', { exact: true }).click();
|
||||||
await page.getByTestId('select-filter-field').click();
|
// await page.getByTestId('select-filter-field').click();
|
||||||
await page.getByRole('menuitemcheckbox', { name: 'Task right' }).click();
|
// await page.getByRole('menuitemcheckbox', { name: 'Task title' }).click();
|
||||||
await page.getByRole('menuitemcheckbox', { name: 'Title' }).click();
|
await page.getByRole('textbox').first().fill(manualNodeName);
|
||||||
await page.getByRole('textbox').fill(manualNodeName);
|
|
||||||
await page.getByRole('button', { name: 'Submit' }).click();
|
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();
|
await expect(page.getByText(preManualNodeRecord)).toBeAttached();
|
||||||
const filter = `pageSize=20&page=1&filter={"$and":[{"orgname":{"$eq":"${preManualNodeRecord}"}}]}`;
|
const filter = `pageSize=20&page=1&filter={"$and":[{"orgname":{"$eq":"${preManualNodeRecord}"}}]}`;
|
||||||
const createNodeCollectionData = await apiFilterList(preManualNodeCollectionName, filter);
|
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.getByRole('menuitem', { name: 'check-square Workflow todos' }).click();
|
||||||
await page.mouse.move(300, 0, { steps: 100 });
|
await page.mouse.move(300, 0, { steps: 100 });
|
||||||
await page.waitForTimeout(300);
|
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();
|
await expect(page.getByText(triggerNodeCollectionRecordOne)).toBeAttached();
|
||||||
// 4、后置处理:删除工作流
|
// 4、后置处理:删除工作流
|
||||||
await apiDeleteWorkflow(workflowId);
|
await apiDeleteWorkflow(workflowId);
|
||||||
@ -222,7 +222,7 @@ test.describe('field data', () => {
|
|||||||
await page.getByRole('menuitem', { name: 'check-square Workflow todos' }).click();
|
await page.getByRole('menuitem', { name: 'check-square Workflow todos' }).click();
|
||||||
await page.mouse.move(300, 0, { steps: 100 });
|
await page.mouse.move(300, 0, { steps: 100 });
|
||||||
await page.waitForTimeout(300);
|
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();
|
await expect(page.getByText(triggerNodeCollectionRecordOne)).toBeAttached();
|
||||||
// 4、后置处理:删除工作流
|
// 4、后置处理:删除工作流
|
||||||
await apiDeleteWorkflow(workflowId);
|
await apiDeleteWorkflow(workflowId);
|
||||||
@ -341,7 +341,7 @@ test.describe('field data', () => {
|
|||||||
await page.getByRole('menuitem', { name: 'check-square Workflow todos' }).click();
|
await page.getByRole('menuitem', { name: 'check-square Workflow todos' }).click();
|
||||||
await page.mouse.move(300, 0, { steps: 100 });
|
await page.mouse.move(300, 0, { steps: 100 });
|
||||||
await page.waitForTimeout(300);
|
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();
|
await expect(page.getByText(triggerNodeCollectionRecordOne)).toBeAttached();
|
||||||
// 4、后置处理:删除工作流
|
// 4、后置处理:删除工作流
|
||||||
await apiDeleteWorkflow(workflowId);
|
await apiDeleteWorkflow(workflowId);
|
||||||
|
@ -181,7 +181,7 @@ test.describe('field data update', () => {
|
|||||||
await page.getByRole('menuitem', { name: 'check-square Workflow todos' }).click();
|
await page.getByRole('menuitem', { name: 'check-square Workflow todos' }).click();
|
||||||
await page.mouse.move(300, 0, { steps: 100 });
|
await page.mouse.move(300, 0, { steps: 100 });
|
||||||
await page.waitForTimeout(300);
|
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 });
|
const manualNodeRecord = faker.number.float({ min: 0, max: 100, precision: 2 });
|
||||||
await page.getByRole('spinbutton').fill(manualNodeRecord.toString());
|
await page.getByRole('spinbutton').fill(manualNodeRecord.toString());
|
||||||
await page.getByRole('button', { name: 'Continue the process' }).click();
|
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.getByRole('menuitem', { name: 'check-square Workflow todos' }).click();
|
||||||
await page.mouse.move(300, 0, { steps: 100 });
|
await page.mouse.move(300, 0, { steps: 100 });
|
||||||
await page.waitForTimeout(300);
|
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 });
|
// const manualNodeRecord = faker.number.float({ min: 0, max: 100, precision: 2 });
|
||||||
await page.getByRole('checkbox').check();
|
await page.getByRole('checkbox').check();
|
||||||
await page.getByRole('button', { name: 'Continue the process' }).click();
|
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.getByRole('menuitem', { name: 'check-square Workflow todos' }).click();
|
||||||
await page.mouse.move(300, 0, { steps: 100 });
|
await page.mouse.move(300, 0, { steps: 100 });
|
||||||
await page.waitForTimeout(300);
|
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 });
|
// const manualNodeRecord = faker.number.float({ min: 0, max: 100, precision: 2 });
|
||||||
await page.getByTestId('select-single').click();
|
await page.getByTestId('select-single').click();
|
||||||
await page.getByRole('option', { name: '存续' }).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.getByRole('menuitem', { name: 'check-square Workflow todos' }).click();
|
||||||
await page.mouse.move(300, 0, { steps: 100 });
|
await page.mouse.move(300, 0, { steps: 100 });
|
||||||
await page.waitForTimeout(300);
|
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 });
|
// const manualNodeRecord = faker.number.float({ min: 0, max: 100, precision: 2 });
|
||||||
await page.getByTestId('select-multiple').click();
|
await page.getByTestId('select-multiple').click();
|
||||||
await page.getByRole('option', { name: '软件销售', exact: true }).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.getByRole('menuitem', { name: 'check-square Workflow todos' }).click();
|
||||||
await page.mouse.move(300, 0, { steps: 100 });
|
await page.mouse.move(300, 0, { steps: 100 });
|
||||||
await page.waitForTimeout(300);
|
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 });
|
// const manualNodeRecord = faker.number.float({ min: 0, max: 100, precision: 2 });
|
||||||
await page.getByLabel('存续').check();
|
await page.getByLabel('存续').check();
|
||||||
await page.getByRole('button', { name: 'Continue the process' }).click();
|
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.getByRole('menuitem', { name: 'check-square Workflow todos' }).click();
|
||||||
await page.mouse.move(300, 0, { steps: 100 });
|
await page.mouse.move(300, 0, { steps: 100 });
|
||||||
await page.waitForTimeout(300);
|
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.getByLabel('软件开发', { exact: true }).check();
|
await page.getByLabel('软件开发', { exact: true }).check();
|
||||||
await page.getByRole('button', { name: 'Continue the process' }).click();
|
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.getByRole('menuitem', { name: 'check-square Workflow todos' }).click();
|
||||||
await page.mouse.move(300, 0, { steps: 100 });
|
await page.mouse.move(300, 0, { steps: 100 });
|
||||||
await page.waitForTimeout(300);
|
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');
|
const manualNodeRecord = dayjs().format('YYYY-MM-DD');
|
||||||
await page.getByPlaceholder('Select date').click();
|
await page.getByPlaceholder('Select date').click();
|
||||||
await page.getByTitle(manualNodeRecord.toString()).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.getByRole('menuitem', { name: 'check-square Workflow todos' }).click();
|
||||||
await page.mouse.move(300, 0, { steps: 100 });
|
await page.mouse.move(300, 0, { steps: 100 });
|
||||||
await page.waitForTimeout(300);
|
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();
|
const manualNodeRecord = manualNodeFieldDisplayName + dayjs().format('YYYYMMDDHHmmss.SSS').toString();
|
||||||
await page.getByRole('textbox').fill(manualNodeRecord);
|
await page.getByRole('textbox').fill(manualNodeRecord);
|
||||||
await page.getByRole('button', { name: 'Continue the process' }).click();
|
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.getByRole('menuitem', { name: 'check-square Workflow todos' }).click();
|
||||||
await page.mouse.move(300, 0, { steps: 100 });
|
await page.mouse.move(300, 0, { steps: 100 });
|
||||||
await page.waitForTimeout(300);
|
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();
|
const manualNodeRecord = manualNodeFieldDisplayName + dayjs().format('YYYYMMDDHHmmss.SSS').toString();
|
||||||
await page.getByRole('textbox').fill(manualNodeRecord);
|
await page.getByRole('textbox').fill(manualNodeRecord);
|
||||||
await page.getByRole('button', { name: 'Continue the process' }).click();
|
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.getByRole('menuitem', { name: 'check-square Workflow todos' }).click();
|
||||||
await page.mouse.move(300, 0, { steps: 100 });
|
await page.mouse.move(300, 0, { steps: 100 });
|
||||||
await page.waitForTimeout(300);
|
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();
|
const manualNodeRecord = manualNodeFieldDisplayName + dayjs().format('YYYYMMDDHHmmss.SSS').toString();
|
||||||
await page.getByRole('textbox').fill(manualNodeRecord);
|
await page.getByRole('textbox').fill(manualNodeRecord);
|
||||||
await page.getByRole('button', { name: 'Continue the process' }).click();
|
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.getByRole('menuitem', { name: 'check-square Workflow todos' }).click();
|
||||||
await page.mouse.move(300, 0, { steps: 100 });
|
await page.mouse.move(300, 0, { steps: 100 });
|
||||||
await page.waitForTimeout(300);
|
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();
|
const manualNodeRecord = faker.internet.email();
|
||||||
await page.getByRole('textbox').fill(manualNodeRecord);
|
await page.getByRole('textbox').fill(manualNodeRecord);
|
||||||
await page.getByRole('button', { name: 'Continue the process' }).click();
|
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.getByRole('menuitem', { name: 'check-square Workflow todos' }).click();
|
||||||
await page.mouse.move(300, 0, { steps: 100 });
|
await page.mouse.move(300, 0, { steps: 100 });
|
||||||
await page.waitForTimeout(300);
|
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();
|
const manualNodeRecord = faker.number.int();
|
||||||
await page.getByRole('spinbutton').fill(manualNodeRecord.toString());
|
await page.getByRole('spinbutton').fill(manualNodeRecord.toString());
|
||||||
await page.getByRole('button', { name: 'Continue the process' }).click();
|
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.getByRole('menuitem', { name: 'check-square Workflow todos' }).click();
|
||||||
await page.mouse.move(300, 0, { steps: 100 });
|
await page.mouse.move(300, 0, { steps: 100 });
|
||||||
await page.waitForTimeout(300);
|
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();
|
const manualNodeRecord = faker.number.float();
|
||||||
await page.getByRole('spinbutton').fill(manualNodeRecord.toString());
|
await page.getByRole('spinbutton').fill(manualNodeRecord.toString());
|
||||||
await page.getByRole('button', { name: 'Continue the process' }).click();
|
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.getByRole('menuitem', { name: 'check-square Workflow todos' }).click();
|
||||||
await page.mouse.move(300, 0, { steps: 100 });
|
await page.mouse.move(300, 0, { steps: 100 });
|
||||||
await page.waitForTimeout(300);
|
await page.waitForTimeout(300);
|
||||||
await page.getByLabel('action-Filter.Action-Filter-filter-users_jobs-workflow-todo').click();
|
await page.getByLabel('action-Filter.Action-Filter-filter-').click();
|
||||||
await page.getByText('Add condition', { exact: true }).click();
|
// await page.getByText('Add condition', { exact: true }).click();
|
||||||
await page.getByTestId('select-filter-field').click();
|
// await page.getByTestId('select-filter-field').click();
|
||||||
await page.getByRole('menuitemcheckbox', { name: 'Task right' }).click();
|
// await page.getByRole('menuitemcheckbox', { name: 'Task right' }).click();
|
||||||
await page.getByRole('menuitemcheckbox', { name: 'Title' }).click();
|
// await page.getByRole('menuitemcheckbox', { name: 'Title' }).click();
|
||||||
await page.getByRole('textbox').fill(manualNodeName);
|
await page.getByRole('textbox').first().fill(manualNodeName);
|
||||||
await page.getByRole('button', { name: 'Submit' }).click();
|
await page.getByRole('button', { name: 'Submit' }).click();
|
||||||
|
|
||||||
// 3、预期结果:列表中出现筛选的工作流
|
// 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.getByRole('menuitem', { name: 'check-square Workflow todos' }).click();
|
||||||
await page.mouse.move(300, 0, { steps: 100 });
|
await page.mouse.move(300, 0, { steps: 100 });
|
||||||
await page.waitForTimeout(300);
|
await page.waitForTimeout(300);
|
||||||
await page.getByLabel('action-Filter.Action-Filter-filter-users_jobs-workflow-todo').click();
|
await page.getByLabel('action-Filter.Action-Filter-filter-').click();
|
||||||
await page.getByText('Add condition', { exact: true }).click();
|
// await page.getByText('Add condition', { exact: true }).click();
|
||||||
await page.getByTestId('select-filter-field').click();
|
// await page.getByTestId('select-filter-field').click();
|
||||||
await page.getByRole('menuitemcheckbox', { name: 'Workflow right' }).click();
|
// await page.getByRole('menuitemcheckbox', { name: 'Workflow right' }).click();
|
||||||
await page.getByRole('menuitemcheckbox', { name: 'Name' }).click();
|
// await page.getByRole('menuitemcheckbox', { name: 'Name' }).click();
|
||||||
await page.getByRole('textbox').fill(workFlowName);
|
await page.getByRole('textbox').last().fill(workFlowName);
|
||||||
await page.getByRole('button', { name: 'Submit' }).click();
|
await page.getByRole('button', { name: 'Submit' }).click();
|
||||||
|
|
||||||
// 3、预期结果:列表中出现筛选的工作流
|
// 3、预期结果:列表中出现筛选的工作流
|
||||||
|
@ -13,8 +13,8 @@ import WorkflowPlugin from '@nocobase/plugin-workflow/client';
|
|||||||
import Manual from './instruction';
|
import Manual from './instruction';
|
||||||
|
|
||||||
import { NAMESPACE } from '../locale';
|
import { NAMESPACE } from '../locale';
|
||||||
import { useCountRequest, WorkflowManualProvider } from './WorkflowManualProvider';
|
import { WorkflowManualProvider } from './WorkflowManualProvider';
|
||||||
import { WorkflowTodo } from './WorkflowTodo';
|
import { manualTodo, WorkflowTodo } from './WorkflowTodo';
|
||||||
import {
|
import {
|
||||||
addActionButton,
|
addActionButton,
|
||||||
addActionButton_deprecated,
|
addActionButton_deprecated,
|
||||||
@ -22,6 +22,7 @@ import {
|
|||||||
addBlockButton_deprecated,
|
addBlockButton_deprecated,
|
||||||
} from './instruction/SchemaConfig';
|
} from './instruction/SchemaConfig';
|
||||||
import { addCustomFormField, addCustomFormField_deprecated } from './instruction/forms/custom';
|
import { addCustomFormField, addCustomFormField_deprecated } from './instruction/forms/custom';
|
||||||
|
import { MANUAL_TASK_TYPE } from '../common/constants';
|
||||||
|
|
||||||
export default class extends Plugin {
|
export default class extends Plugin {
|
||||||
async afterAdd() {
|
async afterAdd() {
|
||||||
@ -37,11 +38,7 @@ export default class extends Plugin {
|
|||||||
const workflow = this.app.pm.get('workflow') as WorkflowPlugin;
|
const workflow = this.app.pm.get('workflow') as WorkflowPlugin;
|
||||||
workflow.registerInstruction('manual', Manual);
|
workflow.registerInstruction('manual', Manual);
|
||||||
|
|
||||||
workflow.registerTaskType('manual', {
|
workflow.registerTaskType(MANUAL_TASK_TYPE, manualTodo);
|
||||||
title: `{{t("My manual tasks", { ns: "${NAMESPACE}" })}}`,
|
|
||||||
useCountRequest,
|
|
||||||
component: WorkflowTodo.TaskBlock,
|
|
||||||
});
|
|
||||||
|
|
||||||
this.app.schemaInitializerManager.add(addBlockButton_deprecated);
|
this.app.schemaInitializerManager.add(addBlockButton_deprecated);
|
||||||
this.app.schemaInitializerManager.add(addBlockButton);
|
this.app.schemaInitializerManager.add(addBlockButton);
|
||||||
|
@ -445,13 +445,13 @@ export function SchemaConfig({ value, onChange }) {
|
|||||||
type: 'void',
|
type: 'void',
|
||||||
title: `{{t("User interface", { ns: "${NAMESPACE}" })}}`,
|
title: `{{t("User interface", { ns: "${NAMESPACE}" })}}`,
|
||||||
'x-decorator': 'Form',
|
'x-decorator': 'Form',
|
||||||
'x-component': 'Action.Drawer',
|
'x-component': 'Action.Container',
|
||||||
'x-component-props': {
|
'x-component-props': {
|
||||||
className: css`
|
// className: css`
|
||||||
.ant-drawer-body {
|
// .ant-drawer-body {
|
||||||
background: var(--nb-box-bg);
|
// background: var(--nb-box-bg);
|
||||||
}
|
// }
|
||||||
`,
|
// `,
|
||||||
// Using ref to call refresh ensures accessing the latest refresh function
|
// Using ref to call refresh ensures accessing the latest refresh function
|
||||||
onClose: () => refreshRef.current(),
|
onClose: () => refreshRef.current(),
|
||||||
},
|
},
|
||||||
|
@ -18,6 +18,7 @@ import {
|
|||||||
CollectionBlockInitializer,
|
CollectionBlockInitializer,
|
||||||
Instruction,
|
Instruction,
|
||||||
WorkflowVariableTextArea,
|
WorkflowVariableTextArea,
|
||||||
|
useNodeContext,
|
||||||
} from '@nocobase/plugin-workflow/client';
|
} from '@nocobase/plugin-workflow/client';
|
||||||
|
|
||||||
import { SchemaConfig, SchemaConfigButton } from './SchemaConfig';
|
import { SchemaConfig, SchemaConfigButton } from './SchemaConfig';
|
||||||
@ -149,6 +150,7 @@ export default class extends Instruction {
|
|||||||
'x-decorator': 'FormItem',
|
'x-decorator': 'FormItem',
|
||||||
'x-component': 'WorkflowVariableTextArea',
|
'x-component': 'WorkflowVariableTextArea',
|
||||||
description: `{{t("Title of each task item. Default to node title.", { ns: "${NAMESPACE}" })}}`,
|
description: `{{t("Title of each task item. Default to node title.", { ns: "${NAMESPACE}" })}}`,
|
||||||
|
default: '{{useNodeContext().title}}',
|
||||||
},
|
},
|
||||||
schema: {
|
schema: {
|
||||||
type: 'void',
|
type: 'void',
|
||||||
@ -168,6 +170,9 @@ export default class extends Instruction {
|
|||||||
default: {},
|
default: {},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
scope = {
|
||||||
|
useNodeContext,
|
||||||
|
};
|
||||||
components = {
|
components = {
|
||||||
SchemaConfigButton,
|
SchemaConfigButton,
|
||||||
SchemaConfig,
|
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 { useTranslation } from 'react-i18next';
|
||||||
|
import { NAMESPACE } from '../common/constants';
|
||||||
|
|
||||||
export const NAMESPACE = 'workflow-manual';
|
export { NAMESPACE };
|
||||||
|
|
||||||
export function useLang(key: string, options = {}) {
|
export function useLang(key: string, options = {}) {
|
||||||
const { t } = usePluginTranslation(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;
|
const title = config.title ? processor.getParsedValue(config.title, node.id) : node.title;
|
||||||
// NOTE: batch create users jobs
|
// NOTE: batch create users jobs
|
||||||
const UserJobModel = this.workflow.app.db.getModel('users_jobs');
|
const TaskRepo = this.workflow.app.db.getRepository('workflowManualTasks');
|
||||||
await UserJobModel.bulkCreate(
|
await TaskRepo.createMany({
|
||||||
assignees.map((userId) => ({
|
records: assignees.map((userId) => ({
|
||||||
userId,
|
userId,
|
||||||
jobId: job.id,
|
jobId: job.id,
|
||||||
nodeId: node.id,
|
nodeId: node.id,
|
||||||
@ -129,10 +129,8 @@ export default class extends Instruction {
|
|||||||
status: JOB_STATUS.PENDING,
|
status: JOB_STATUS.PENDING,
|
||||||
title,
|
title,
|
||||||
})),
|
})),
|
||||||
{
|
transaction: processor.mainTransaction,
|
||||||
transaction: processor.mainTransaction,
|
});
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
return job;
|
return job;
|
||||||
}
|
}
|
||||||
@ -141,15 +139,15 @@ export default class extends Instruction {
|
|||||||
// NOTE: check all users jobs related if all done then continue as parallel
|
// NOTE: check all users jobs related if all done then continue as parallel
|
||||||
const { mode } = node.config as ManualConfig;
|
const { mode } = node.config as ManualConfig;
|
||||||
|
|
||||||
const UserJobRepo = this.workflow.app.db.getRepository('users_jobs');
|
const TaskRepo = this.workflow.app.db.getRepository('workflowManualTasks');
|
||||||
const jobs = await UserJobRepo.find({
|
const tasks = await TaskRepo.find({
|
||||||
where: {
|
where: {
|
||||||
jobId: job.id,
|
jobId: job.id,
|
||||||
},
|
},
|
||||||
transaction: processor.mainTransaction,
|
transaction: processor.mainTransaction,
|
||||||
});
|
});
|
||||||
const assignees = [];
|
const assignees = [];
|
||||||
const distributionMap = jobs.reduce((result, item) => {
|
const distributionMap = tasks.reduce((result, item) => {
|
||||||
if (result[item.status] == null) {
|
if (result[item.status] == null) {
|
||||||
result[item.status] = 0;
|
result[item.status] = 0;
|
||||||
}
|
}
|
||||||
@ -162,9 +160,9 @@ export default class extends Instruction {
|
|||||||
count: distributionMap[status],
|
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 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}`);
|
processor.logger.debug(`manual resume job and next status: ${status}`);
|
||||||
job.set({
|
job.set({
|
||||||
status,
|
status,
|
||||||
|
@ -15,11 +15,20 @@ import WorkflowPlugin, { JOB_STATUS } from '@nocobase/plugin-workflow';
|
|||||||
import * as jobActions from './actions';
|
import * as jobActions from './actions';
|
||||||
|
|
||||||
import ManualInstruction from './ManualInstruction';
|
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 {
|
export default class extends Plugin {
|
||||||
async load() {
|
async load() {
|
||||||
this.app.resourceManager.define({
|
this.app.resourceManager.define({
|
||||||
name: 'users_jobs',
|
name: 'workflowManualTasks',
|
||||||
actions: {
|
actions: {
|
||||||
list: {
|
list: {
|
||||||
filter: {
|
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;
|
const workflowPlugin = this.app.pm.get(WorkflowPlugin) as WorkflowPlugin;
|
||||||
workflowPlugin.registerInstruction('manual', ManualInstruction);
|
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;
|
PostRepo = db.getCollection('posts').repository;
|
||||||
CommentRepo = db.getCollection('comments').repository;
|
CommentRepo = db.getCollection('comments').repository;
|
||||||
UserModel = db.getCollection('users').model;
|
UserModel = db.getCollection('users').model;
|
||||||
UserJobModel = db.getModel('users_jobs');
|
UserJobModel = db.getModel('workflowManualTasks');
|
||||||
|
|
||||||
users = await UserModel.bulkCreate([
|
users = await UserModel.bulkCreate([
|
||||||
{ id: 2, nickname: 'a' },
|
{ id: 2, nickname: 'a' },
|
||||||
@ -117,13 +117,13 @@ describe('workflow > instructions > manual > assignees', () => {
|
|||||||
expect(usersJobs[0].userId).toBe(users[0].id);
|
expect(usersJobs[0].userId).toBe(users[0].id);
|
||||||
expect(usersJobs[0].jobId).toBe(j1.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,
|
filterByTk: usersJobs[0].id,
|
||||||
values: { result: { f1: {}, _: 'resolve' } },
|
values: { result: { f1: {}, _: 'resolve' } },
|
||||||
});
|
});
|
||||||
expect(res1.status).toBe(401);
|
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,
|
filterByTk: usersJobs[0].id,
|
||||||
values: {
|
values: {
|
||||||
result: { f1: {}, _: 'resolve' },
|
result: { f1: {}, _: 'resolve' },
|
||||||
@ -131,7 +131,7 @@ describe('workflow > instructions > manual > assignees', () => {
|
|||||||
});
|
});
|
||||||
expect(res2.status).toBe(403);
|
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,
|
filterByTk: usersJobs[0].id,
|
||||||
values: {
|
values: {
|
||||||
result: { f1: { a: 1 }, _: 'resolve' },
|
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].status).toBe(JOB_STATUS.RESOLVED);
|
||||||
expect(usersJobsAfter[0].result).toEqual({ f1: { a: 1 }, _: 'resolve' });
|
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,
|
filterByTk: usersJobs[0].id,
|
||||||
values: {
|
values: {
|
||||||
result: { f1: { a: 2 }, _: 'resolve' },
|
result: { f1: { a: 2 }, _: 'resolve' },
|
||||||
|
@ -37,7 +37,7 @@ describe('workflow > instructions > manual', () => {
|
|||||||
PostRepo = db.getCollection('posts').repository;
|
PostRepo = db.getCollection('posts').repository;
|
||||||
AnotherPostRepo = app.dataSourceManager.dataSources.get('another').collectionManager.getRepository('posts');
|
AnotherPostRepo = app.dataSourceManager.dataSources.get('another').collectionManager.getRepository('posts');
|
||||||
UserModel = db.getCollection('users').model;
|
UserModel = db.getCollection('users').model;
|
||||||
UserJobModel = db.getModel('users_jobs');
|
UserJobModel = db.getModel('workflowManualTasks');
|
||||||
|
|
||||||
users = await UserModel.bulkCreate([
|
users = await UserModel.bulkCreate([
|
||||||
{ id: 2, nickname: 'a' },
|
{ id: 2, nickname: 'a' },
|
||||||
@ -80,13 +80,13 @@ describe('workflow > instructions > manual', () => {
|
|||||||
|
|
||||||
await sleep(500);
|
await sleep(500);
|
||||||
|
|
||||||
const UserJobModel = db.getModel('users_jobs');
|
const UserJobModel = db.getModel('workflowManualTasks');
|
||||||
const pendingJobs = await UserJobModel.findAll({
|
const pendingJobs = await UserJobModel.findAll({
|
||||||
order: [['userId', 'ASC']],
|
order: [['userId', 'ASC']],
|
||||||
});
|
});
|
||||||
expect(pendingJobs.length).toBe(1);
|
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'),
|
filterByTk: pendingJobs[0].get('id'),
|
||||||
values: {
|
values: {
|
||||||
result: { f1: { title: 't1' }, _: 'resolve' },
|
result: { f1: { title: 't1' }, _: 'resolve' },
|
||||||
@ -130,13 +130,13 @@ describe('workflow > instructions > manual', () => {
|
|||||||
|
|
||||||
await sleep(500);
|
await sleep(500);
|
||||||
|
|
||||||
const UserJobModel = db.getModel('users_jobs');
|
const UserJobModel = db.getModel('workflowManualTasks');
|
||||||
const pendingJobs = await UserJobModel.findAll({
|
const pendingJobs = await UserJobModel.findAll({
|
||||||
order: [['userId', 'ASC']],
|
order: [['userId', 'ASC']],
|
||||||
});
|
});
|
||||||
expect(pendingJobs.length).toBe(1);
|
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'),
|
filterByTk: pendingJobs[0].get('id'),
|
||||||
values: {
|
values: {
|
||||||
result: { f1: { title: 't1' }, _: 'pending' },
|
result: { f1: { title: 't1' }, _: 'pending' },
|
||||||
@ -155,7 +155,7 @@ describe('workflow > instructions > manual', () => {
|
|||||||
const c1 = await AnotherPostRepo.find();
|
const c1 = await AnotherPostRepo.find();
|
||||||
expect(c1.length).toBe(0);
|
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'),
|
filterByTk: pendingJobs[0].get('id'),
|
||||||
values: {
|
values: {
|
||||||
result: { f1: { title: 't2' }, _: 'resolve' },
|
result: { f1: { title: 't2' }, _: 'resolve' },
|
||||||
@ -201,13 +201,13 @@ describe('workflow > instructions > manual', () => {
|
|||||||
|
|
||||||
await sleep(500);
|
await sleep(500);
|
||||||
|
|
||||||
const UserJobModel = db.getModel('users_jobs');
|
const UserJobModel = db.getModel('workflowManualTasks');
|
||||||
const pendingJobs = await UserJobModel.findAll({
|
const pendingJobs = await UserJobModel.findAll({
|
||||||
order: [['userId', 'ASC']],
|
order: [['userId', 'ASC']],
|
||||||
});
|
});
|
||||||
expect(pendingJobs.length).toBe(1);
|
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'),
|
filterByTk: pendingJobs[0].get('id'),
|
||||||
values: {
|
values: {
|
||||||
result: { f1: { title: 't2' }, _: 'resolve' },
|
result: { f1: { title: 't2' }, _: 'resolve' },
|
||||||
|
@ -37,7 +37,7 @@ describe('workflow > instructions > manual', () => {
|
|||||||
PostRepo = db.getCollection('posts').repository;
|
PostRepo = db.getCollection('posts').repository;
|
||||||
CommentRepo = db.getCollection('comments').repository;
|
CommentRepo = db.getCollection('comments').repository;
|
||||||
UserModel = db.getCollection('users').model;
|
UserModel = db.getCollection('users').model;
|
||||||
UserJobModel = db.getModel('users_jobs');
|
UserJobModel = db.getModel('workflowManualTasks');
|
||||||
|
|
||||||
users = await UserModel.bulkCreate([
|
users = await UserModel.bulkCreate([
|
||||||
{ id: 2, nickname: 'a' },
|
{ id: 2, nickname: 'a' },
|
||||||
@ -85,7 +85,7 @@ describe('workflow > instructions > manual', () => {
|
|||||||
expect(usersJobs[0].userId).toBe(users[0].id);
|
expect(usersJobs[0].userId).toBe(users[0].id);
|
||||||
expect(usersJobs[0].jobId).toBe(j1.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,
|
filterByTk: usersJobs[0].id,
|
||||||
values: {
|
values: {
|
||||||
result: { f1: { a: 1 }, _: 'resolve' },
|
result: { f1: { a: 1 }, _: 'resolve' },
|
||||||
@ -123,7 +123,7 @@ describe('workflow > instructions > manual', () => {
|
|||||||
expect(usersJobs[0].userId).toBe(users[0].id);
|
expect(usersJobs[0].userId).toBe(users[0].id);
|
||||||
expect(usersJobs[0].jobId).toBe(j1.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,
|
filterByTk: usersJobs[0].id,
|
||||||
values: {
|
values: {
|
||||||
result: { f1: { a: 1 } },
|
result: { f1: { a: 1 } },
|
||||||
@ -167,7 +167,7 @@ describe('workflow > instructions > manual', () => {
|
|||||||
expect(usersJobs[0].userId).toBe(users[0].id);
|
expect(usersJobs[0].userId).toBe(users[0].id);
|
||||||
expect(usersJobs[0].jobId).toBe(j1.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,
|
filterByTk: usersJobs[0].id,
|
||||||
values: {
|
values: {
|
||||||
result: { f1: { a: 1 }, _: 'resolve' },
|
result: { f1: { a: 1 }, _: 'resolve' },
|
||||||
@ -217,7 +217,7 @@ describe('workflow > instructions > manual', () => {
|
|||||||
expect(usersJobs[0].userId).toBe(users[0].id);
|
expect(usersJobs[0].userId).toBe(users[0].id);
|
||||||
expect(usersJobs[0].jobId).toBe(j1.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,
|
filterByTk: usersJobs[0].id,
|
||||||
values: {
|
values: {
|
||||||
result: { f1: { a: 1 }, _: 'reject' },
|
result: { f1: { a: 1 }, _: 'reject' },
|
||||||
@ -267,7 +267,7 @@ describe('workflow > instructions > manual', () => {
|
|||||||
expect(usersJobs[0].userId).toBe(users[0].id);
|
expect(usersJobs[0].userId).toBe(users[0].id);
|
||||||
expect(usersJobs[0].jobId).toBe(j1.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,
|
filterByTk: usersJobs[0].id,
|
||||||
values: {
|
values: {
|
||||||
result: { f1: { a: 1 }, _: 'save' },
|
result: { f1: { a: 1 }, _: 'save' },
|
||||||
@ -323,7 +323,7 @@ describe('workflow > instructions > manual', () => {
|
|||||||
expect(usersJobs[0].jobId).toBe(j1.id);
|
expect(usersJobs[0].jobId).toBe(j1.id);
|
||||||
|
|
||||||
const now = new Date();
|
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,
|
filterByTk: usersJobs[0].id,
|
||||||
values: {
|
values: {
|
||||||
result: { f1: { a: 2, id: 3 }, _: 'resolve' },
|
result: { f1: { a: 2, id: 3 }, _: 'resolve' },
|
||||||
@ -371,13 +371,13 @@ describe('workflow > instructions > manual', () => {
|
|||||||
|
|
||||||
await sleep(500);
|
await sleep(500);
|
||||||
|
|
||||||
const UserJobModel = db.getModel('users_jobs');
|
const UserJobModel = db.getModel('workflowManualTasks');
|
||||||
const pendingJobs = await UserJobModel.findAll({
|
const pendingJobs = await UserJobModel.findAll({
|
||||||
order: [['userId', 'ASC']],
|
order: [['userId', 'ASC']],
|
||||||
});
|
});
|
||||||
expect(pendingJobs.length).toBe(2);
|
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'),
|
filterByTk: pendingJobs[0].get('id'),
|
||||||
values: {
|
values: {
|
||||||
result: { f1: { number: 1 }, _: 'resolve' },
|
result: { f1: { number: 1 }, _: 'resolve' },
|
||||||
@ -420,13 +420,13 @@ describe('workflow > instructions > manual', () => {
|
|||||||
|
|
||||||
await sleep(500);
|
await sleep(500);
|
||||||
|
|
||||||
const UserJobModel = db.getModel('users_jobs');
|
const UserJobModel = db.getModel('workflowManualTasks');
|
||||||
const pendingJobs = await UserJobModel.findAll({
|
const pendingJobs = await UserJobModel.findAll({
|
||||||
order: [['userId', 'ASC']],
|
order: [['userId', 'ASC']],
|
||||||
});
|
});
|
||||||
expect(pendingJobs.length).toBe(2);
|
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'),
|
filterByTk: pendingJobs[0].get('id'),
|
||||||
values: {
|
values: {
|
||||||
result: { f1: { number: 1 }, _: 'pending' },
|
result: { f1: { number: 1 }, _: 'pending' },
|
||||||
@ -442,7 +442,7 @@ describe('workflow > instructions > manual', () => {
|
|||||||
expect(j1.status).toBe(JOB_STATUS.PENDING);
|
expect(j1.status).toBe(JOB_STATUS.PENDING);
|
||||||
expect(j1.result).toMatchObject({ f1: { number: 1 } });
|
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'),
|
filterByTk: pendingJobs[0].get('id'),
|
||||||
values: {
|
values: {
|
||||||
result: { f2: { number: 2 }, _: 'pending' },
|
result: { f2: { number: 2 }, _: 'pending' },
|
||||||
@ -461,7 +461,7 @@ describe('workflow > instructions > manual', () => {
|
|||||||
f2: { number: 2 },
|
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'),
|
filterByTk: pendingJobs[0].get('id'),
|
||||||
values: {
|
values: {
|
||||||
result: { f2: { number: 3 }, _: 'resolve' },
|
result: { f2: { number: 3 }, _: 'resolve' },
|
||||||
@ -500,13 +500,13 @@ describe('workflow > instructions > manual', () => {
|
|||||||
|
|
||||||
await sleep(500);
|
await sleep(500);
|
||||||
|
|
||||||
const UserJobModel = db.getModel('users_jobs');
|
const UserJobModel = db.getModel('workflowManualTasks');
|
||||||
const pendingJobs = await UserJobModel.findAll({
|
const pendingJobs = await UserJobModel.findAll({
|
||||||
order: [['userId', 'ASC']],
|
order: [['userId', 'ASC']],
|
||||||
});
|
});
|
||||||
expect(pendingJobs.length).toBe(1);
|
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'),
|
filterByTk: pendingJobs[0].get('id'),
|
||||||
values: {
|
values: {
|
||||||
result: { f1: { status: 1 }, _: 'resolve' },
|
result: { f1: { status: 1 }, _: 'resolve' },
|
||||||
@ -549,13 +549,13 @@ describe('workflow > instructions > manual', () => {
|
|||||||
|
|
||||||
await sleep(500);
|
await sleep(500);
|
||||||
|
|
||||||
const UserJobModel = db.getModel('users_jobs');
|
const UserJobModel = db.getModel('workflowManualTasks');
|
||||||
const pendingJobs = await UserJobModel.findAll({
|
const pendingJobs = await UserJobModel.findAll({
|
||||||
order: [['userId', 'ASC']],
|
order: [['userId', 'ASC']],
|
||||||
});
|
});
|
||||||
expect(pendingJobs.length).toBe(1);
|
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'),
|
filterByTk: pendingJobs[0].get('id'),
|
||||||
values: {
|
values: {
|
||||||
result: { f1: { status: 1 }, _: 'pending' },
|
result: { f1: { status: 1 }, _: 'pending' },
|
||||||
@ -574,7 +574,7 @@ describe('workflow > instructions > manual', () => {
|
|||||||
const c1 = await CommentRepo.find();
|
const c1 = await CommentRepo.find();
|
||||||
expect(c1.length).toBe(0);
|
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'),
|
filterByTk: pendingJobs[0].get('id'),
|
||||||
values: {
|
values: {
|
||||||
result: { f1: { status: 1 }, _: 'resolve' },
|
result: { f1: { status: 1 }, _: 'resolve' },
|
||||||
@ -615,13 +615,13 @@ describe('workflow > instructions > manual', () => {
|
|||||||
|
|
||||||
await sleep(500);
|
await sleep(500);
|
||||||
|
|
||||||
const UserJobModel = db.getModel('users_jobs');
|
const UserJobModel = db.getModel('workflowManualTasks');
|
||||||
const pendingJobs = await UserJobModel.findAll({
|
const pendingJobs = await UserJobModel.findAll({
|
||||||
order: [['userId', 'ASC']],
|
order: [['userId', 'ASC']],
|
||||||
});
|
});
|
||||||
expect(pendingJobs.length).toBe(1);
|
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'),
|
filterByTk: pendingJobs[0].get('id'),
|
||||||
values: {
|
values: {
|
||||||
result: { f1: { title: 't2' }, _: 'resolve' },
|
result: { f1: { title: 't2' }, _: 'resolve' },
|
||||||
|
@ -37,7 +37,7 @@ describe('workflow > instructions > manual', () => {
|
|||||||
PostRepo = db.getCollection('posts').repository;
|
PostRepo = db.getCollection('posts').repository;
|
||||||
CommentRepo = db.getCollection('comments').repository;
|
CommentRepo = db.getCollection('comments').repository;
|
||||||
UserModel = db.getCollection('users').model;
|
UserModel = db.getCollection('users').model;
|
||||||
UserJobModel = db.getModel('users_jobs');
|
UserJobModel = db.getModel('workflowManualTasks');
|
||||||
|
|
||||||
users = await UserModel.bulkCreate([
|
users = await UserModel.bulkCreate([
|
||||||
{ id: 2, nickname: 'a' },
|
{ id: 2, nickname: 'a' },
|
||||||
@ -87,13 +87,13 @@ describe('workflow > instructions > manual', () => {
|
|||||||
expect(usersJobs[0].userId).toBe(users[0].id);
|
expect(usersJobs[0].userId).toBe(users[0].id);
|
||||||
expect(usersJobs[0].jobId).toBe(j1.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,
|
filterByTk: usersJobs[0].id,
|
||||||
values: { result: { f1: {}, _: 'resolve' } },
|
values: { result: { f1: {}, _: 'resolve' } },
|
||||||
});
|
});
|
||||||
expect(res1.status).toBe(401);
|
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,
|
filterByTk: usersJobs[0].id,
|
||||||
values: {
|
values: {
|
||||||
result: { f1: {}, _: 'resolve' },
|
result: { f1: {}, _: 'resolve' },
|
||||||
@ -101,7 +101,7 @@ describe('workflow > instructions > manual', () => {
|
|||||||
});
|
});
|
||||||
expect(res2.status).toBe(403);
|
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,
|
filterByTk: usersJobs[0].id,
|
||||||
values: {
|
values: {
|
||||||
result: { f1: { a: 1 }, _: 'resolve' },
|
result: { f1: { a: 1 }, _: 'resolve' },
|
||||||
@ -120,7 +120,7 @@ describe('workflow > instructions > manual', () => {
|
|||||||
expect(usersJobsAfter[0].status).toBe(JOB_STATUS.RESOLVED);
|
expect(usersJobsAfter[0].status).toBe(JOB_STATUS.RESOLVED);
|
||||||
expect(usersJobsAfter[0].result).toEqual({ f1: { a: 1 }, _: 'resolve' });
|
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,
|
filterByTk: usersJobs[0].id,
|
||||||
values: {
|
values: {
|
||||||
result: { f1: { a: 2 }, _: 'resolve' },
|
result: { f1: { a: 2 }, _: 'resolve' },
|
||||||
@ -151,9 +151,13 @@ describe('workflow > instructions > manual', () => {
|
|||||||
const [j1] = await pending.getJobs();
|
const [j1] = await pending.getJobs();
|
||||||
expect(j1.status).toBe(JOB_STATUS.PENDING);
|
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,
|
filterByTk: usersJobs.find((item) => item.userId === users[1].id).id,
|
||||||
values: {
|
values: {
|
||||||
result: { f1: { a: 1 }, _: 'resolve' },
|
result: { f1: { a: 1 }, _: 'resolve' },
|
||||||
@ -167,7 +171,7 @@ describe('workflow > instructions > manual', () => {
|
|||||||
expect(j2.status).toBe(JOB_STATUS.RESOLVED);
|
expect(j2.status).toBe(JOB_STATUS.RESOLVED);
|
||||||
expect(j2.result).toEqual({ f1: { a: 1 }, _: 'resolve' });
|
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,
|
filterByTk: usersJobs.find((item) => item.userId === users[0].id).id,
|
||||||
values: {
|
values: {
|
||||||
result: { f1: { a: 1 }, _: 'resolve' },
|
result: { f1: { a: 1 }, _: 'resolve' },
|
||||||
@ -176,7 +180,7 @@ describe('workflow > instructions > manual', () => {
|
|||||||
expect(res2.status).toBe(400);
|
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({
|
const n1 = await workflow.createNode({
|
||||||
type: 'manual',
|
type: 'manual',
|
||||||
config: {
|
config: {
|
||||||
@ -193,13 +197,13 @@ describe('workflow > instructions > manual', () => {
|
|||||||
|
|
||||||
await sleep(500);
|
await sleep(500);
|
||||||
|
|
||||||
const UserJobModel = db.getModel('users_jobs');
|
const UserJobModel = db.getModel('workflowManualTasks');
|
||||||
const usersJobs = await UserJobModel.findAll();
|
const usersJobs = await UserJobModel.findAll();
|
||||||
expect(usersJobs.length).toBe(1);
|
expect(usersJobs.length).toBe(1);
|
||||||
expect(usersJobs[0].get('status')).toBe(JOB_STATUS.PENDING);
|
expect(usersJobs[0].get('status')).toBe(JOB_STATUS.PENDING);
|
||||||
expect(usersJobs[0].get('userId')).toBe(users[0].id);
|
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'),
|
filterByTk: usersJobs[0].get('id'),
|
||||||
values: {
|
values: {
|
||||||
result: { f1: { a: 1 }, _: 'resolve' },
|
result: { f1: { a: 1 }, _: 'resolve' },
|
||||||
@ -236,13 +240,13 @@ describe('workflow > instructions > manual', () => {
|
|||||||
|
|
||||||
await sleep(500);
|
await sleep(500);
|
||||||
|
|
||||||
const UserJobModel = db.getModel('users_jobs');
|
const UserJobModel = db.getModel('workflowManualTasks');
|
||||||
const pendingJobs = await UserJobModel.findAll({
|
const pendingJobs = await UserJobModel.findAll({
|
||||||
order: [['userId', 'ASC']],
|
order: [['userId', 'ASC']],
|
||||||
});
|
});
|
||||||
expect(pendingJobs.length).toBe(2);
|
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'),
|
filterByTk: pendingJobs[0].get('id'),
|
||||||
values: {
|
values: {
|
||||||
result: { f1: { a: 1 }, _: 'resolve' },
|
result: { f1: { a: 1 }, _: 'resolve' },
|
||||||
@ -262,7 +266,7 @@ describe('workflow > instructions > manual', () => {
|
|||||||
});
|
});
|
||||||
expect(usersJobs1.length).toBe(2);
|
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'),
|
filterByTk: pendingJobs[1].get('id'),
|
||||||
values: {
|
values: {
|
||||||
result: { f1: { a: 2 }, _: 'resolve' },
|
result: { f1: { a: 2 }, _: 'resolve' },
|
||||||
@ -297,13 +301,13 @@ describe('workflow > instructions > manual', () => {
|
|||||||
|
|
||||||
await sleep(500);
|
await sleep(500);
|
||||||
|
|
||||||
const UserJobModel = db.getModel('users_jobs');
|
const UserJobModel = db.getModel('workflowManualTasks');
|
||||||
const pendingJobs = await UserJobModel.findAll({
|
const pendingJobs = await UserJobModel.findAll({
|
||||||
order: [['userId', 'ASC']],
|
order: [['userId', 'ASC']],
|
||||||
});
|
});
|
||||||
expect(pendingJobs.length).toBe(2);
|
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'),
|
filterByTk: pendingJobs[0].get('id'),
|
||||||
values: {
|
values: {
|
||||||
result: { f1: { a: 0 }, _: 'reject' },
|
result: { f1: { a: 0 }, _: 'reject' },
|
||||||
@ -323,7 +327,7 @@ describe('workflow > instructions > manual', () => {
|
|||||||
});
|
});
|
||||||
expect(usersJobs1.length).toBe(2);
|
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'),
|
filterByTk: pendingJobs[1].get('id'),
|
||||||
values: {
|
values: {
|
||||||
result: { f1: { a: 0 }, _: 'reject' },
|
result: { f1: { a: 0 }, _: 'reject' },
|
||||||
@ -353,13 +357,13 @@ describe('workflow > instructions > manual', () => {
|
|||||||
|
|
||||||
await sleep(500);
|
await sleep(500);
|
||||||
|
|
||||||
const UserJobModel = db.getModel('users_jobs');
|
const UserJobModel = db.getModel('workflowManualTasks');
|
||||||
const pendingJobs = await UserJobModel.findAll({
|
const pendingJobs = await UserJobModel.findAll({
|
||||||
order: [['userId', 'ASC']],
|
order: [['userId', 'ASC']],
|
||||||
});
|
});
|
||||||
expect(pendingJobs.length).toBe(2);
|
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'),
|
filterByTk: pendingJobs[0].get('id'),
|
||||||
values: {
|
values: {
|
||||||
result: { f1: { a: 1 }, _: 'resolve' },
|
result: { f1: { a: 1 }, _: 'resolve' },
|
||||||
@ -379,7 +383,7 @@ describe('workflow > instructions > manual', () => {
|
|||||||
});
|
});
|
||||||
expect(usersJobs1.length).toBe(2);
|
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'),
|
filterByTk: pendingJobs[1].get('id'),
|
||||||
values: {
|
values: {
|
||||||
result: { f1: { a: 0 }, _: 'reject' },
|
result: { f1: { a: 0 }, _: 'reject' },
|
||||||
@ -419,13 +423,13 @@ describe('workflow > instructions > manual', () => {
|
|||||||
|
|
||||||
await sleep(500);
|
await sleep(500);
|
||||||
|
|
||||||
const UserJobModel = db.getModel('users_jobs');
|
const UserJobModel = db.getModel('workflowManualTasks');
|
||||||
const pendingJobs = await UserJobModel.findAll({
|
const pendingJobs = await UserJobModel.findAll({
|
||||||
order: [['userId', 'ASC']],
|
order: [['userId', 'ASC']],
|
||||||
});
|
});
|
||||||
expect(pendingJobs.length).toBe(2);
|
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'),
|
filterByTk: pendingJobs[0].get('id'),
|
||||||
values: {
|
values: {
|
||||||
result: { f1: { a: 1 }, _: 'resolve' },
|
result: { f1: { a: 1 }, _: 'resolve' },
|
||||||
@ -441,7 +445,7 @@ describe('workflow > instructions > manual', () => {
|
|||||||
expect(j1.status).toBe(JOB_STATUS.RESOLVED);
|
expect(j1.status).toBe(JOB_STATUS.RESOLVED);
|
||||||
expect(j1.result).toBe(0.5);
|
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'),
|
filterByTk: pendingJobs[1].get('id'),
|
||||||
values: {
|
values: {
|
||||||
result: { f1: { a: 0 }, _: 'reject' },
|
result: { f1: { a: 0 }, _: 'reject' },
|
||||||
@ -471,13 +475,13 @@ describe('workflow > instructions > manual', () => {
|
|||||||
|
|
||||||
await sleep(500);
|
await sleep(500);
|
||||||
|
|
||||||
const UserJobModel = db.getModel('users_jobs');
|
const UserJobModel = db.getModel('workflowManualTasks');
|
||||||
const pendingJobs = await UserJobModel.findAll({
|
const pendingJobs = await UserJobModel.findAll({
|
||||||
order: [['userId', 'ASC']],
|
order: [['userId', 'ASC']],
|
||||||
});
|
});
|
||||||
expect(pendingJobs.length).toBe(2);
|
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'),
|
filterByTk: pendingJobs[0].get('id'),
|
||||||
values: {
|
values: {
|
||||||
result: { f1: { a: 0 }, _: 'reject' },
|
result: { f1: { a: 0 }, _: 'reject' },
|
||||||
@ -493,7 +497,7 @@ describe('workflow > instructions > manual', () => {
|
|||||||
expect(j1.status).toBe(JOB_STATUS.PENDING);
|
expect(j1.status).toBe(JOB_STATUS.PENDING);
|
||||||
expect(j1.result).toBe(0.5);
|
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'),
|
filterByTk: pendingJobs[1].get('id'),
|
||||||
values: {
|
values: {
|
||||||
result: { f1: { a: 1 }, _: 'resolve' },
|
result: { f1: { a: 1 }, _: 'resolve' },
|
||||||
@ -528,13 +532,13 @@ describe('workflow > instructions > manual', () => {
|
|||||||
|
|
||||||
await sleep(500);
|
await sleep(500);
|
||||||
|
|
||||||
const UserJobModel = db.getModel('users_jobs');
|
const UserJobModel = db.getModel('workflowManualTasks');
|
||||||
const pendingJobs = await UserJobModel.findAll({
|
const pendingJobs = await UserJobModel.findAll({
|
||||||
order: [['userId', 'ASC']],
|
order: [['userId', 'ASC']],
|
||||||
});
|
});
|
||||||
expect(pendingJobs.length).toBe(2);
|
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'),
|
filterByTk: pendingJobs[0].get('id'),
|
||||||
values: {
|
values: {
|
||||||
result: { f1: { a: 0 }, _: 'reject' },
|
result: { f1: { a: 0 }, _: 'reject' },
|
||||||
@ -550,7 +554,7 @@ describe('workflow > instructions > manual', () => {
|
|||||||
expect(j1.status).toBe(JOB_STATUS.PENDING);
|
expect(j1.status).toBe(JOB_STATUS.PENDING);
|
||||||
expect(j1.result).toBe(0.5);
|
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'),
|
filterByTk: pendingJobs[1].get('id'),
|
||||||
values: {
|
values: {
|
||||||
result: { f1: { a: 0 }, _: 'reject' },
|
result: { f1: { a: 0 }, _: 'reject' },
|
||||||
|
@ -24,7 +24,7 @@ export async function submit(context: Context, next) {
|
|||||||
const plugin: WorkflowPlugin = context.app.getPlugin(WorkflowPlugin);
|
const plugin: WorkflowPlugin = context.app.getPlugin(WorkflowPlugin);
|
||||||
const instruction = plugin.instructions.get('manual') as ManualInstruction;
|
const instruction = plugin.instructions.get('manual') as ManualInstruction;
|
||||||
|
|
||||||
const userJob = await repository.findOne({
|
const task = await repository.findOne({
|
||||||
filterByTk,
|
filterByTk,
|
||||||
// filter: {
|
// filter: {
|
||||||
// userId: currentUser?.id
|
// userId: currentUser?.id
|
||||||
@ -33,40 +33,40 @@ export async function submit(context: Context, next) {
|
|||||||
context,
|
context,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!userJob) {
|
if (!task) {
|
||||||
return context.throw(404);
|
return context.throw(404);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { forms = {} } = userJob.node.config;
|
const { forms = {} } = task.node.config;
|
||||||
const [formKey] = Object.keys(values.result ?? {}).filter((key) => key !== '_');
|
const [formKey] = Object.keys(values.result ?? {}).filter((key) => key !== '_');
|
||||||
const actionKey = values.result?._;
|
const actionKey = values.result?._;
|
||||||
|
|
||||||
const actionItem = forms[formKey]?.actions?.find((item) => item.key === actionKey);
|
const actionItem = forms[formKey]?.actions?.find((item) => item.key === actionKey);
|
||||||
// NOTE: validate status
|
// NOTE: validate status
|
||||||
if (
|
if (
|
||||||
userJob.status !== JOB_STATUS.PENDING ||
|
task.status !== JOB_STATUS.PENDING ||
|
||||||
userJob.job.status !== JOB_STATUS.PENDING ||
|
task.job.status !== JOB_STATUS.PENDING ||
|
||||||
userJob.execution.status !== EXECUTION_STATUS.STARTED ||
|
task.execution.status !== EXECUTION_STATUS.STARTED ||
|
||||||
!userJob.workflow.enabled ||
|
!task.workflow.enabled ||
|
||||||
!actionKey ||
|
!actionKey ||
|
||||||
actionItem?.status == null
|
actionItem?.status == null
|
||||||
) {
|
) {
|
||||||
return context.throw(400);
|
return context.throw(400);
|
||||||
}
|
}
|
||||||
|
|
||||||
userJob.execution.workflow = userJob.workflow;
|
task.execution.workflow = task.workflow;
|
||||||
const processor = plugin.createProcessor(userJob.execution);
|
const processor = plugin.createProcessor(task.execution);
|
||||||
await processor.prepare();
|
await processor.prepare();
|
||||||
|
|
||||||
// NOTE: validate assignee
|
// NOTE: validate assignee
|
||||||
const assignees = processor
|
const assignees = processor
|
||||||
.getParsedValue(userJob.node.config.assignees ?? [], userJob.nodeId)
|
.getParsedValue(task.node.config.assignees ?? [], task.nodeId)
|
||||||
.flat()
|
.flat()
|
||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
if (!assignees.includes(currentUser.id) || userJob.userId !== currentUser.id) {
|
if (!assignees.includes(currentUser.id) || task.userId !== currentUser.id) {
|
||||||
return context.throw(403);
|
return context.throw(403);
|
||||||
}
|
}
|
||||||
const presetValues = processor.getParsedValue(actionItem.values ?? {}, userJob.nodeId, {
|
const presetValues = processor.getParsedValue(actionItem.values ?? {}, task.nodeId, {
|
||||||
additionalScope: {
|
additionalScope: {
|
||||||
// @deprecated
|
// @deprecated
|
||||||
currentUser: currentUser,
|
currentUser: currentUser,
|
||||||
@ -82,56 +82,32 @@ export async function submit(context: Context, next) {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
userJob.set({
|
task.set({
|
||||||
status: actionItem.status,
|
status: actionItem.status,
|
||||||
result: actionItem.status
|
result: actionItem.status
|
||||||
? { [formKey]: Object.assign(values.result[formKey], presetValues), _: actionKey }
|
? { [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);
|
const handler = instruction.formTypes.get(forms[formKey].type);
|
||||||
if (handler && userJob.status) {
|
if (handler && task.status) {
|
||||||
await handler.call(instruction, userJob, forms[formKey], processor);
|
await handler.call(instruction, task, forms[formKey], processor);
|
||||||
}
|
}
|
||||||
|
|
||||||
await userJob.save();
|
await task.save();
|
||||||
|
|
||||||
await processor.exit();
|
await processor.exit();
|
||||||
|
|
||||||
context.body = userJob;
|
context.body = task;
|
||||||
context.status = 202;
|
context.status = 202;
|
||||||
|
|
||||||
await next();
|
await next();
|
||||||
|
|
||||||
userJob.job.execution = userJob.execution;
|
task.job.execution = task.execution;
|
||||||
userJob.job.latestUserJob = userJob;
|
task.job.latestTask = task;
|
||||||
|
|
||||||
// NOTE: resume the process and no `await` for quick returning
|
// 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);
|
plugin.resume(task.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();
|
|
||||||
}
|
}
|
||||||
|
@ -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 { defineCollection } from '@nocobase/database';
|
||||||
|
import { NAMESPACE } from '../../common/constants';
|
||||||
|
|
||||||
export default defineCollection({
|
export default defineCollection({
|
||||||
name: 'users_jobs',
|
name: 'workflowManualTasks',
|
||||||
dumpRules: {
|
dumpRules: {
|
||||||
group: 'log',
|
group: 'log',
|
||||||
},
|
},
|
||||||
@ -40,6 +41,12 @@ export default defineCollection({
|
|||||||
{
|
{
|
||||||
type: 'string',
|
type: 'string',
|
||||||
name: 'title',
|
name: 'title',
|
||||||
|
interface: 'input',
|
||||||
|
uiSchema: {
|
||||||
|
type: 'string',
|
||||||
|
title: `{{t("Task title", { ns: "${NAMESPACE}" })}}`,
|
||||||
|
'x-component': 'Input',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'belongsTo',
|
type: 'belongsTo',
|
||||||
@ -53,6 +60,20 @@ export default defineCollection({
|
|||||||
{
|
{
|
||||||
type: 'belongsTo',
|
type: 'belongsTo',
|
||||||
name: 'workflow',
|
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',
|
type: 'integer',
|
@ -14,7 +14,7 @@ export default class extends Migration {
|
|||||||
async up() {
|
async up() {
|
||||||
const { db } = this.context;
|
const { db } = this.context;
|
||||||
const NodeRepo = db.getRepository('flow_nodes');
|
const NodeRepo = db.getRepository('flow_nodes');
|
||||||
const TaskRepo = db.getRepository('users_jobs');
|
const TaskRepo = db.getRepository('workflowManualTasks');
|
||||||
await db.sequelize.transaction(async (transaction) => {
|
await db.sequelize.transaction(async (transaction) => {
|
||||||
const nodes = await NodeRepo.find({
|
const nodes = await NodeRepo.find({
|
||||||
filter: {
|
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.node = page.getByLabel(`Approval-${nodeName}`, { exact: true });
|
||||||
this.nodeTitle = page.getByLabel(`Approval-${nodeName}`, { exact: true }).getByRole('textbox');
|
this.nodeTitle = page.getByLabel(`Approval-${nodeName}`, { exact: true }).getByRole('textbox');
|
||||||
this.nodeConfigure = this.node.locator('>div').first();
|
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.addSelectAssigneesMenu = page.getByRole('button', { name: 'Select assignees' });
|
||||||
this.addQueryAssigneesMenu = page.getByRole('button', { name: 'Query assignees' });
|
this.addQueryAssigneesMenu = page.getByRole('button', { name: 'Query assignees' });
|
||||||
this.assigneesDropDown = page.getByTestId('select-single');
|
this.assigneesDropDown = page.getByTestId('select-single');
|
||||||
@ -183,14 +185,14 @@ export class ApprovalPassthroughModeNode {
|
|||||||
this.sequentiallyRadio = page.getByLabel('Sequentially', { exact: true });
|
this.sequentiallyRadio = page.getByLabel('Sequentially', { exact: true });
|
||||||
this.goToconfigureButton = page.getByRole('button', { name: 'Go to configure' });
|
this.goToconfigureButton = page.getByRole('button', { name: 'Go to configure' });
|
||||||
this.addBlockButton = page.getByLabel('schema-initializer-Grid-ApprovalProcessAddBlockButton-workflows');
|
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(
|
this.detailsConfigureFieldsButton = page.getByLabel(
|
||||||
`schema-initializer-Grid-details:configureFields-${collectionName}`,
|
`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.actionsConfigureFieldsButton = page.getByLabel('schema-initializer-Grid-FormItemInitializers-approvalRecords');
|
||||||
this.actionsConfigureActionsButton = page.getByLabel(
|
this.actionsConfigureActionsButton = page.getByLabel(
|
||||||
'schema-initializer-ActionBar-ApprovalProcessAddActionButton-approvalRecords',
|
'schema-initializer-ActionBar-ApprovalProcessAddActionButton-',
|
||||||
);
|
);
|
||||||
this.addApproveButton = page.getByRole('menuitem', { name: 'Approve' }).getByRole('switch');
|
this.addApproveButton = page.getByRole('menuitem', { name: 'Approve' }).getByRole('switch');
|
||||||
this.addRejectButton = page.getByRole('menuitem', { name: 'Reject' }).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.node = page.getByLabel(`Approval-${nodeName}`, { exact: true });
|
||||||
this.nodeTitle = page.getByLabel(`Approval-${nodeName}`, { exact: true }).getByRole('textbox');
|
this.nodeTitle = page.getByLabel(`Approval-${nodeName}`, { exact: true }).getByRole('textbox');
|
||||||
this.nodeConfigure = this.node.locator('>div').first();
|
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.addSelectAssigneesMenu = page.getByRole('button', { name: 'Select assignees' });
|
||||||
this.addQueryAssigneesMenu = page.getByRole('button', { name: 'Query assignees' });
|
this.addQueryAssigneesMenu = page.getByRole('button', { name: 'Query assignees' });
|
||||||
this.assigneesDropDown = page.getByTestId('select-single');
|
this.assigneesDropDown = page.getByTestId('select-single');
|
||||||
@ -260,9 +264,7 @@ export class ApprovalBranchModeNode {
|
|||||||
);
|
);
|
||||||
this.addActionsMenu = page.getByRole('menuitem', { name: 'Process form' }).getByRole('switch');
|
this.addActionsMenu = page.getByRole('menuitem', { name: 'Process form' }).getByRole('switch');
|
||||||
this.actionsConfigureFieldsButton = page.getByLabel('schema-initializer-Grid-FormItemInitializers-approvalRecords');
|
this.actionsConfigureFieldsButton = page.getByLabel('schema-initializer-Grid-FormItemInitializers-approvalRecords');
|
||||||
this.actionsConfigureActionsButton = page.getByLabel(
|
this.actionsConfigureActionsButton = page.getByLabel('schema-initializer-ActionBar-');
|
||||||
'schema-initializer-ActionBar-ApprovalProcessAddActionButton-approvalRecords',
|
|
||||||
);
|
|
||||||
this.addApproveButton = page.getByRole('menuitem', { name: 'Approve' }).getByRole('switch');
|
this.addApproveButton = page.getByRole('menuitem', { name: 'Approve' }).getByRole('switch');
|
||||||
this.addRejectButton = page.getByRole('menuitem', { name: 'Reject' }).getByRole('switch');
|
this.addRejectButton = page.getByRole('menuitem', { name: 'Reject' }).getByRole('switch');
|
||||||
this.addReturnButton = page.getByRole('menuitem', { name: 'Return' }).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 state = await api.storageState();
|
||||||
const headers = getHeaders(state);
|
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, {
|
const result = await api.get(url, {
|
||||||
headers,
|
headers,
|
||||||
});
|
});
|
||||||
|
@ -6,9 +6,9 @@
|
|||||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
* 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 { 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 { PageHeader } from '@ant-design/pro-layout';
|
||||||
import { CheckCircleOutlined } from '@ant-design/icons';
|
import { CheckCircleOutlined } from '@ant-design/icons';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
@ -16,17 +16,31 @@ import classnames from 'classnames';
|
|||||||
import {
|
import {
|
||||||
css,
|
css,
|
||||||
PinnedPluginListProvider,
|
PinnedPluginListProvider,
|
||||||
|
SchemaComponent,
|
||||||
SchemaComponentContext,
|
SchemaComponentContext,
|
||||||
SchemaComponentOptions,
|
SchemaComponentOptions,
|
||||||
|
useApp,
|
||||||
useCompile,
|
useCompile,
|
||||||
|
useDocumentTitle,
|
||||||
usePlugin,
|
usePlugin,
|
||||||
|
useRequest,
|
||||||
|
useToken,
|
||||||
} from '@nocobase/client';
|
} from '@nocobase/client';
|
||||||
|
|
||||||
import PluginWorkflowClient from '.';
|
import PluginWorkflowClient from '.';
|
||||||
import { lang } from './locale';
|
import { lang, NAMESPACE } from './locale';
|
||||||
|
|
||||||
|
const layoutClass = css`
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
`;
|
||||||
|
|
||||||
const sideClass = css`
|
const sideClass = css`
|
||||||
height: calc(100vh - 46px);
|
overflow: auto;
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
.ant-layout-sider-children {
|
.ant-layout-sider-children {
|
||||||
width: 200px;
|
width: 200px;
|
||||||
@ -34,21 +48,29 @@ const sideClass = css`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const contentClass = css`
|
||||||
|
padding: 24px;
|
||||||
|
min-height: 280px;
|
||||||
|
overflow: auto;
|
||||||
|
`;
|
||||||
|
|
||||||
export interface TaskTypeOptions {
|
export interface TaskTypeOptions {
|
||||||
title: string;
|
title: string;
|
||||||
useCountRequest?: Function;
|
collection: string;
|
||||||
component?: React.ComponentType;
|
useActionParams: Function;
|
||||||
children?: TaskTypeOptions[];
|
component: React.ComponentType;
|
||||||
|
extraActions?: React.ComponentType;
|
||||||
|
// children?: TaskTypeOptions[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const TasksCountsContext = createContext<{ counts: Record<string, number>; total: number }>({ counts: {}, total: 0 });
|
||||||
|
|
||||||
function MenuLink({ type }: any) {
|
function MenuLink({ type }: any) {
|
||||||
const workflowPlugin = usePlugin(PluginWorkflowClient);
|
const workflowPlugin = usePlugin(PluginWorkflowClient);
|
||||||
const compile = useCompile();
|
const compile = useCompile();
|
||||||
const { title, useCountRequest } = workflowPlugin.taskTypes.get(type);
|
const { title } = workflowPlugin.taskTypes.get(type);
|
||||||
const { data, loading, run } = useCountRequest?.() || { loading: false };
|
const { counts } = useContext(TasksCountsContext);
|
||||||
useEffect(() => {
|
const typeTitle = compile(title);
|
||||||
run?.();
|
|
||||||
}, [run]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
@ -57,24 +79,72 @@ function MenuLink({ type }: any) {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
> span:first-child {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
<span>{compile(title)}</span>
|
<span>{typeTitle}</span>
|
||||||
{loading ? <Spin /> : <Badge count={data?.data || 0} />}
|
<Badge count={counts[type] || 0} size="small" />
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function WorkflowTasks() {
|
const TASK_STATUS = {
|
||||||
const workflowPlugin = usePlugin(PluginWorkflowClient);
|
ALL: 'all',
|
||||||
const navigate = useNavigate();
|
PENDING: 'pending',
|
||||||
const { taskType } = useParams();
|
COMPLETED: 'completed',
|
||||||
const compile = useCompile();
|
};
|
||||||
const {
|
|
||||||
token: { colorBgContainer },
|
|
||||||
} = theme.useToken();
|
|
||||||
|
|
||||||
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) => {
|
Array.from(workflowPlugin.taskTypes.getKeys()).map((key: string) => {
|
||||||
return {
|
return {
|
||||||
@ -84,37 +154,143 @@ export function WorkflowTasks() {
|
|||||||
}),
|
}),
|
||||||
[workflowPlugin.taskTypes],
|
[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) ?? {},
|
() => workflowPlugin.taskTypes.get(taskType ?? items[0]?.key) ?? {},
|
||||||
[items, taskType, workflowPlugin.taskTypes],
|
[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(() => {
|
useEffect(() => {
|
||||||
if (!taskType && items[0].key) {
|
setTitle?.(`${lang('Workflow todo')}${title ? `: ${compile(title)}` : ''}`);
|
||||||
navigate(`/admin/workflow/tasks/${items[0].key}`, { replace: true });
|
}, [taskType, status, setTitle, title, compile]);
|
||||||
}
|
|
||||||
}, [items, navigate, taskType]);
|
|
||||||
|
|
||||||
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 (
|
return (
|
||||||
<Layout>
|
<Layout className={layoutClass}>
|
||||||
<Layout.Sider className={sideClass} theme="light">
|
<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.Sider>
|
||||||
<Layout>
|
<Layout
|
||||||
<PageHeader
|
className={css`
|
||||||
className={classnames('pageHeaderCss', 'height0')}
|
> div {
|
||||||
style={{ background: colorBgContainer, padding: '12px 24px 0 24px' }}
|
height: 100%;
|
||||||
title={compile(title)}
|
overflow: hidden;
|
||||||
/>
|
|
||||||
<Layout.Content style={{ padding: '24px', minHeight: 280 }}>
|
> .ant-formily-layout {
|
||||||
<SchemaComponentContext.Provider value={{ designable: false }}>
|
height: 100%;
|
||||||
{Component ? <Component /> : null}
|
|
||||||
<Outlet />
|
> div {
|
||||||
</SchemaComponentContext.Provider>
|
display: flex;
|
||||||
</Layout.Content>
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
<SchemaComponentContext.Provider value={{ designable: false }}>
|
||||||
|
<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>
|
</Layout>
|
||||||
</Layout>
|
</Layout>
|
||||||
);
|
);
|
||||||
@ -122,52 +298,91 @@ export function WorkflowTasks() {
|
|||||||
|
|
||||||
function WorkflowTasksLink() {
|
function WorkflowTasksLink() {
|
||||||
const workflowPlugin = usePlugin(PluginWorkflowClient);
|
const workflowPlugin = usePlugin(PluginWorkflowClient);
|
||||||
|
const { total } = useContext(TasksCountsContext);
|
||||||
|
|
||||||
const types = Array.from(workflowPlugin.taskTypes.getKeys());
|
const types = Array.from(workflowPlugin.taskTypes.getKeys());
|
||||||
return types.length ? (
|
return types.length ? (
|
||||||
<Tooltip title={lang('Workflow todos')}>
|
<Tooltip title={lang('Workflow todos')}>
|
||||||
<Button
|
<Button>
|
||||||
className={css`
|
<Link to={`/admin/workflow/tasks/${types[0]}`}>
|
||||||
padding: 0;
|
<Badge count={total} size="small">
|
||||||
display: inline-flex;
|
<CheckCircleOutlined />
|
||||||
vertical-align: middle;
|
</Badge>
|
||||||
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">
|
|
||||||
<CheckCircleOutlined />
|
|
||||||
</Link>
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
) : null;
|
) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function transform(detail) {
|
||||||
|
return detail.reduce((result, stats) => {
|
||||||
|
result[stats.type] = stats.count;
|
||||||
|
return result;
|
||||||
|
}, {});
|
||||||
|
}
|
||||||
|
|
||||||
export const TasksProvider = (props: any) => {
|
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 (
|
return (
|
||||||
<PinnedPluginListProvider
|
<TasksCountsContext.Provider value={{ total, counts }}>
|
||||||
items={{
|
<PinnedPluginListProvider
|
||||||
todo: { component: 'WorkflowTasksLink', pin: true, snippet: '*' },
|
items={{
|
||||||
}}
|
todo: { component: 'WorkflowTasksLink', pin: true, snippet: '*' },
|
||||||
>
|
|
||||||
<SchemaComponentOptions
|
|
||||||
components={{
|
|
||||||
WorkflowTasksLink,
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{props.children}
|
<SchemaComponentOptions
|
||||||
</SchemaComponentOptions>
|
components={{
|
||||||
</PinnedPluginListProvider>
|
WorkflowTasksLink,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{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 './Calculation';
|
||||||
export * from './Fieldset';
|
export * from './Fieldset';
|
||||||
export * from './TriggerCollectionRecordSelect';
|
export * from './TriggerCollectionRecordSelect';
|
||||||
|
export * from './WorkflowTitle';
|
||||||
|
@ -120,12 +120,12 @@ export default class PluginWorkflowClient extends Plugin {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.router.add('admin.workflow.tasks', {
|
this.router.add('admin.workflow.tasks', {
|
||||||
path: '/admin/workflow/tasks/:taskType?',
|
path: '/admin/workflow/tasks/:taskType/:status?',
|
||||||
Component: WorkflowTasks,
|
Component: WorkflowTasks,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.router.add('admin.workflow.tasks.popup', {
|
this.router.add('admin.workflow.tasks.popup', {
|
||||||
path: '/admin/workflow/tasks/:taskType/popups/*',
|
path: '/admin/workflow/tasks/:taskType/:status/popups/*',
|
||||||
Component: PagePopups,
|
Component: PagePopups,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -133,6 +133,8 @@
|
|||||||
"Canceled": "已取消",
|
"Canceled": "已取消",
|
||||||
"Rejected": "已拒绝",
|
"Rejected": "已拒绝",
|
||||||
"Retry needed": "需重试",
|
"Retry needed": "需重试",
|
||||||
|
"Completed": "已完成",
|
||||||
|
"All": "全部",
|
||||||
"View result": "查看结果",
|
"View result": "查看结果",
|
||||||
|
|
||||||
"Triggered but still waiting in queue to execute.": "已触发但仍在队列中等待执行。",
|
"Triggered but still waiting in queue to execute.": "已触发但仍在队列中等待执行。",
|
||||||
@ -231,5 +233,6 @@
|
|||||||
"After end of branches": "分支结束后",
|
"After end of branches": "分支结束后",
|
||||||
"Inside of branch": "分支内",
|
"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 { Op } from '@nocobase/database';
|
||||||
import { Plugin } from '@nocobase/server';
|
import { Plugin } from '@nocobase/server';
|
||||||
import { Registry } from '@nocobase/utils';
|
import { Registry } from '@nocobase/utils';
|
||||||
|
import { SequelizeCollectionManager } from '@nocobase/data-source-manager';
|
||||||
import { Logger, LoggerOptions } from '@nocobase/logger';
|
import { Logger, LoggerOptions } from '@nocobase/logger';
|
||||||
|
|
||||||
import Processor from './Processor';
|
import Processor from './Processor';
|
||||||
import initActions from './actions';
|
import initActions from './actions';
|
||||||
import { EXECUTION_STATUS } from './constants';
|
import { EXECUTION_STATUS } from './constants';
|
||||||
@ -34,10 +35,9 @@ import DestroyInstruction from './instructions/DestroyInstruction';
|
|||||||
import QueryInstruction from './instructions/QueryInstruction';
|
import QueryInstruction from './instructions/QueryInstruction';
|
||||||
import UpdateInstruction from './instructions/UpdateInstruction';
|
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 WorkflowRepository from './repositories/WorkflowRepository';
|
||||||
import { Context } from '@nocobase/actions';
|
import WorkflowTasksRepository from './repositories/WorkflowTasksRepository';
|
||||||
import { SequelizeCollectionManager } from '@nocobase/data-source-manager';
|
|
||||||
|
|
||||||
type ID = number | string;
|
type ID = number | string;
|
||||||
|
|
||||||
@ -213,6 +213,7 @@ export default class PluginWorkflowServer extends Plugin {
|
|||||||
async beforeLoad() {
|
async beforeLoad() {
|
||||||
this.db.registerRepositories({
|
this.db.registerRepositories({
|
||||||
WorkflowRepository,
|
WorkflowRepository,
|
||||||
|
WorkflowTasksRepository,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -262,6 +263,7 @@ export default class PluginWorkflowServer extends Plugin {
|
|||||||
actions: ['workflows:list'],
|
actions: ['workflows:list'],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.app.acl.allow('workflowTasks', 'countMine', 'loggedIn');
|
||||||
this.app.acl.allow('*', ['trigger'], 'loggedIn');
|
this.app.acl.allow('*', ['trigger'], 'loggedIn');
|
||||||
|
|
||||||
this.db.addMigrations({
|
this.db.addMigrations({
|
||||||
@ -733,4 +735,45 @@ export default class PluginWorkflowServer extends Plugin {
|
|||||||
return db.sequelize.transaction();
|
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 workflows from './workflows';
|
||||||
import * as nodes from './nodes';
|
import * as nodes from './nodes';
|
||||||
import * as executions from './executions';
|
import * as executions from './executions';
|
||||||
|
import * as workflowTasks from './workflowTasks';
|
||||||
|
|
||||||
function make(name, mod) {
|
function make(name, mod) {
|
||||||
return Object.keys(mod).reduce(
|
return Object.keys(mod).reduce(
|
||||||
@ -33,5 +34,6 @@ export default function ({ app }) {
|
|||||||
test: nodes.test,
|
test: nodes.test,
|
||||||
}),
|
}),
|
||||||
...make('executions', executions),
|
...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 actions, { Context, utils } from '@nocobase/actions';
|
||||||
import { Op, Repository } from '@nocobase/database';
|
import { Op } from '@nocobase/database';
|
||||||
|
|
||||||
import Plugin from '../Plugin';
|
import Plugin from '../Plugin';
|
||||||
import Processor from '../Processor';
|
import Processor from '../Processor';
|
||||||
import WorkflowRepository from '../repositories/WorkflowRepository';
|
import WorkflowRepository from '../repositories/WorkflowRepository';
|
||||||
|
|
||||||
export async function update(context: Context, next) {
|
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;
|
const { filterByTk, values } = context.action.params;
|
||||||
context.action.mergeParams({
|
context.action.mergeParams({
|
||||||
whitelist: ['title', 'description', 'enabled', 'triggerTitle', 'config', 'options'],
|
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) {
|
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;
|
const { filterByTk, filter } = context.action.params;
|
||||||
|
|
||||||
await context.db.sequelize.transaction(async (transaction) => {
|
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,7 +24,13 @@ export default function () {
|
|||||||
{
|
{
|
||||||
type: 'string',
|
type: 'string',
|
||||||
name: 'title',
|
name: 'title',
|
||||||
required: true,
|
interface: 'input',
|
||||||
|
uiSchema: {
|
||||||
|
title: '{{t("Name")}}',
|
||||||
|
type: 'string',
|
||||||
|
'x-component': 'Input',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
|
@ -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 FlowNodeModel } from './FlowNode';
|
||||||
export type { default as ExecutionModel } from './Execution';
|
export type { default as ExecutionModel } from './Execution';
|
||||||
export type { default as JobModel } from './Job';
|
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: {
|
get: {
|
||||||
tags: ['users_jobs'],
|
tags: ['workflowManualTasks'],
|
||||||
description: 'List manual jobs',
|
description: 'List manual jobs',
|
||||||
parameters: [],
|
parameters: [],
|
||||||
responses: {
|
responses: {
|
||||||
@ -449,9 +449,9 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
'/users_jobs:get': {
|
'/workflowManualTasks:get': {
|
||||||
get: {
|
get: {
|
||||||
tags: ['users_jobs'],
|
tags: ['workflowManualTasks'],
|
||||||
description: 'Single user job',
|
description: 'Single user job',
|
||||||
parameters: [],
|
parameters: [],
|
||||||
responses: {
|
responses: {
|
||||||
@ -480,9 +480,9 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
'/users_jobs:submit': {
|
'/workflowManualTasks:submit': {
|
||||||
post: {
|
post: {
|
||||||
tags: ['users_jobs'],
|
tags: ['workflowManualTasks'],
|
||||||
description: '',
|
description: '',
|
||||||
parameters: [
|
parameters: [
|
||||||
{
|
{
|
||||||
|
Loading…
x
Reference in New Issue
Block a user