mirror of
https://github.com/nocobase/nocobase.git
synced 2026-08-30 16:56:07 +08:00
feat(plugin-workflow-cc): add new plugin (#7201)
This commit is contained in:
@@ -0,0 +1,45 @@
|
||||
node_modules/
|
||||
lib/
|
||||
esm/
|
||||
es/
|
||||
.env
|
||||
.env.e2e
|
||||
.DS_Store
|
||||
yarn-error.log
|
||||
lerna-debug.log
|
||||
/.vscode
|
||||
/.idea
|
||||
*.sqlite
|
||||
coverage
|
||||
.umi
|
||||
.umi-production
|
||||
/uploads
|
||||
.env.test
|
||||
docs-dist/
|
||||
.npmrc
|
||||
dist/
|
||||
docker/**/storage
|
||||
cache/diskstore-*
|
||||
*.nbdump
|
||||
storage/duplicator/*
|
||||
storage/backups/*
|
||||
**/.dumi/tmp
|
||||
**/.dumi/tmp-test
|
||||
**/.dumi/tmp-production
|
||||
packages/core/client/docs/contributing.md
|
||||
packages/core/app/client/src/.plugins
|
||||
storage/plugins
|
||||
storage/tar
|
||||
storage/tmp
|
||||
storage/app.watch.ts
|
||||
storage/logs-e2e
|
||||
storage/uploads-e2e
|
||||
storage/.pm2-*
|
||||
tsconfig.paths.json
|
||||
/playwright
|
||||
/storage/playwright
|
||||
.swc
|
||||
ncc-cache/
|
||||
yarn--**
|
||||
v8-compile-cache-**
|
||||
vitest.config.mts.timestamp-*
|
||||
@@ -0,0 +1,2 @@
|
||||
/node_modules
|
||||
/src
|
||||
@@ -0,0 +1,16 @@
|
||||
# @nocobase/plugin-workflow-cc
|
||||
|
||||
## 分支
|
||||
|
||||
和 nocobase 仓库一样,也分为 main 和 next 两个分支
|
||||
|
||||
- main:只处理缺陷修复
|
||||
- next:新插件、新特性、不兼容性变更等
|
||||
|
||||
## 提测
|
||||
|
||||
- 提交 PR 之后,会自动生成 PR 测试环境,测试通过后再合并到对应分支
|
||||
|
||||
```bash
|
||||
pr-<number>-<pro-tpl>.test.nocobase.com
|
||||
```
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './dist/client';
|
||||
export { default } from './dist/client';
|
||||
@@ -0,0 +1 @@
|
||||
module.exports = require('./dist/client/index.js');
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "@nocobase/plugin-workflow-cc",
|
||||
"displayName": "Workflow: CC",
|
||||
"displayName.zh-CN": "工作流:抄送",
|
||||
"description": "Information within the workflow can be freely configured into a view interface and made accessible to specific users. A CC task will be generated for them in the tasks center.",
|
||||
"description.zh-CN": "可将流程中的信息自由配置为查看界面,提供给特定的用户查看,将在待办中心为其生成抄送任务。",
|
||||
"homepage": "https://docs.nocobase.com/handbook/workflow-cc",
|
||||
"homepage.zh-CN": "https://docs-cn.nocobase.com/handbook/workflow-cc",
|
||||
"version": "1.8.3",
|
||||
"main": "dist/server/index.js",
|
||||
"peerDependencies": {
|
||||
"@nocobase/actions": "1.x",
|
||||
"@nocobase/client": "1.x",
|
||||
"@nocobase/plugin-ui-schema-storage": "1.x",
|
||||
"@nocobase/plugin-users": "1.x",
|
||||
"@nocobase/plugin-workflow": "1.x",
|
||||
"@nocobase/server": "1.x",
|
||||
"@nocobase/test": "1.x",
|
||||
"@nocobase/utils": "1.x"
|
||||
},
|
||||
"keywords": [
|
||||
"Workflow"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './dist/server';
|
||||
export { default } from './dist/server';
|
||||
@@ -0,0 +1 @@
|
||||
module.exports = require('./dist/server/index.js');
|
||||
@@ -0,0 +1,249 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
// CSS modules
|
||||
type CSSModuleClasses = { readonly [key: string]: string };
|
||||
|
||||
declare module '*.module.css' {
|
||||
const classes: CSSModuleClasses;
|
||||
export default classes;
|
||||
}
|
||||
declare module '*.module.scss' {
|
||||
const classes: CSSModuleClasses;
|
||||
export default classes;
|
||||
}
|
||||
declare module '*.module.sass' {
|
||||
const classes: CSSModuleClasses;
|
||||
export default classes;
|
||||
}
|
||||
declare module '*.module.less' {
|
||||
const classes: CSSModuleClasses;
|
||||
export default classes;
|
||||
}
|
||||
declare module '*.module.styl' {
|
||||
const classes: CSSModuleClasses;
|
||||
export default classes;
|
||||
}
|
||||
declare module '*.module.stylus' {
|
||||
const classes: CSSModuleClasses;
|
||||
export default classes;
|
||||
}
|
||||
declare module '*.module.pcss' {
|
||||
const classes: CSSModuleClasses;
|
||||
export default classes;
|
||||
}
|
||||
declare module '*.module.sss' {
|
||||
const classes: CSSModuleClasses;
|
||||
export default classes;
|
||||
}
|
||||
|
||||
// CSS
|
||||
declare module '*.css' { }
|
||||
declare module '*.scss' { }
|
||||
declare module '*.sass' { }
|
||||
declare module '*.less' { }
|
||||
declare module '*.styl' { }
|
||||
declare module '*.stylus' { }
|
||||
declare module '*.pcss' { }
|
||||
declare module '*.sss' { }
|
||||
|
||||
// Built-in asset types
|
||||
// see `src/node/constants.ts`
|
||||
|
||||
// images
|
||||
declare module '*.apng' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
declare module '*.png' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
declare module '*.jpg' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
declare module '*.jpeg' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
declare module '*.jfif' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
declare module '*.pjpeg' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
declare module '*.pjp' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
declare module '*.gif' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
declare module '*.svg' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
declare module '*.ico' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
declare module '*.webp' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
declare module '*.avif' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
|
||||
// media
|
||||
declare module '*.mp4' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
declare module '*.webm' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
declare module '*.ogg' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
declare module '*.mp3' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
declare module '*.wav' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
declare module '*.flac' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
declare module '*.aac' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
declare module '*.opus' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
declare module '*.mov' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
declare module '*.m4a' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
declare module '*.vtt' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
|
||||
// fonts
|
||||
declare module '*.woff' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
declare module '*.woff2' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
declare module '*.eot' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
declare module '*.ttf' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
declare module '*.otf' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
|
||||
// other
|
||||
declare module '*.webmanifest' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
declare module '*.pdf' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
declare module '*.txt' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
|
||||
// wasm?init
|
||||
declare module '*.wasm?init' {
|
||||
const initWasm: (options?: WebAssembly.Imports) => Promise<WebAssembly.Instance>;
|
||||
export default initWasm;
|
||||
}
|
||||
|
||||
// web worker
|
||||
declare module '*?worker' {
|
||||
const workerConstructor: {
|
||||
new(options?: { name?: string }): Worker;
|
||||
};
|
||||
export default workerConstructor;
|
||||
}
|
||||
|
||||
declare module '*?worker&inline' {
|
||||
const workerConstructor: {
|
||||
new(options?: { name?: string }): Worker;
|
||||
};
|
||||
export default workerConstructor;
|
||||
}
|
||||
|
||||
declare module '*?worker&url' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
|
||||
declare module '*?sharedworker' {
|
||||
const sharedWorkerConstructor: {
|
||||
new(options?: { name?: string }): SharedWorker;
|
||||
};
|
||||
export default sharedWorkerConstructor;
|
||||
}
|
||||
|
||||
declare module '*?sharedworker&inline' {
|
||||
const sharedWorkerConstructor: {
|
||||
new(options?: { name?: string }): SharedWorker;
|
||||
};
|
||||
export default sharedWorkerConstructor;
|
||||
}
|
||||
|
||||
declare module '*?sharedworker&url' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
|
||||
declare module '*?raw' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
|
||||
declare module '*?url' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
|
||||
declare module '*?inline' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
/**
|
||||
* 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 {
|
||||
ButtonEditor,
|
||||
ExtendCollectionsProvider,
|
||||
Plugin,
|
||||
RemoveButton,
|
||||
SchemaSettings,
|
||||
useSchemaToolbar,
|
||||
} from '@nocobase/client';
|
||||
import PluginWorkflowClient from '@nocobase/plugin-workflow/client';
|
||||
import CCInstruction from './instruction';
|
||||
import { addBlockButton } from './instruction/SchemaConfig';
|
||||
import ccCollection from '../common/collections/workflowCcTasks';
|
||||
import { TASK_TYPE_CC } from '../common/constants';
|
||||
import { ccTodo } from './tasks';
|
||||
|
||||
function WorkflowCCProvider(props) {
|
||||
return <ExtendCollectionsProvider collections={[ccCollection]}>{props.children}</ExtendCollectionsProvider>;
|
||||
}
|
||||
|
||||
export class PluginWorkflowCCClient extends Plugin {
|
||||
// You can get and modify the app instance here
|
||||
async load() {
|
||||
this.app.addProvider(WorkflowCCProvider);
|
||||
|
||||
const workflow = this.app.pm.get(PluginWorkflowClient);
|
||||
workflow.registerInstruction('cc', CCInstruction);
|
||||
workflow.registerTaskType(TASK_TYPE_CC, ccTodo);
|
||||
|
||||
this.app.schemaInitializerManager.add(addBlockButton);
|
||||
// this.app.schemaInitializerManager.add(addActionButton);
|
||||
this.app.schemaSettingsManager.add(
|
||||
new SchemaSettings({
|
||||
name: 'actionSettings:workflow:cc:read',
|
||||
items: [
|
||||
{
|
||||
name: 'editButton',
|
||||
Component: ButtonEditor,
|
||||
useComponentProps() {
|
||||
const { buttonEditorProps } = useSchemaToolbar();
|
||||
return buttonEditorProps;
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'delete',
|
||||
sort: 100,
|
||||
Component: RemoveButton as any,
|
||||
useComponentProps() {
|
||||
const { removeButtonProps } = useSchemaToolbar();
|
||||
return removeButtonProps;
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default PluginWorkflowCCClient;
|
||||
+238
@@ -0,0 +1,238 @@
|
||||
/**
|
||||
* 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, { useState } from 'react';
|
||||
import { uid } from '@formily/shared';
|
||||
import { ISchema, observer, useForm } from '@formily/react';
|
||||
import { Button, Spin, Typography } from 'antd';
|
||||
|
||||
import {
|
||||
ActionContextProvider,
|
||||
css,
|
||||
gridRowColWrap,
|
||||
SchemaComponent,
|
||||
SchemaComponentProvider,
|
||||
SchemaInitializer,
|
||||
SchemaInitializerItemType,
|
||||
useActionContext,
|
||||
useAPIClient,
|
||||
usePlugin,
|
||||
useRequest,
|
||||
useSchemaOptionsContext,
|
||||
} from '@nocobase/client';
|
||||
import PluginWorkflowClient, {
|
||||
useAvailableUpstreams,
|
||||
useFlowContext,
|
||||
useNodeContext,
|
||||
useTrigger,
|
||||
useWorkflowExecuted,
|
||||
} from '@nocobase/plugin-workflow/client';
|
||||
import { NAMESPACE } from '../../common/constants';
|
||||
import { lang, usePluginTranslation } from '../locale';
|
||||
|
||||
function useTriggerInitializers(): SchemaInitializerItemType | null {
|
||||
const { workflow } = useFlowContext();
|
||||
const trigger = useTrigger();
|
||||
return trigger.useInitializers ? trigger.useInitializers(workflow.config) : null;
|
||||
}
|
||||
|
||||
export const addBlockButton = new SchemaInitializer({
|
||||
name: 'workflowCc:popup:configureUserInterface:addBlock',
|
||||
wrap: gridRowColWrap,
|
||||
title: '{{t("Add block")}}',
|
||||
items: [
|
||||
{
|
||||
type: 'itemGroup',
|
||||
name: 'dataBlocks',
|
||||
title: '{{t("Data blocks")}}',
|
||||
hideIfNoChildren: true,
|
||||
useChildren() {
|
||||
const workflowPlugin = usePlugin(PluginWorkflowClient);
|
||||
const current = useNodeContext();
|
||||
const nodes = useAvailableUpstreams(current);
|
||||
const triggerInitializers = [useTriggerInitializers()].filter(Boolean);
|
||||
const nodeBlockInitializers = nodes
|
||||
.map((node) => {
|
||||
const instruction = workflowPlugin.instructions.get(node.type);
|
||||
return instruction?.useInitializers?.(node);
|
||||
})
|
||||
.filter(Boolean);
|
||||
const dataBlockInitializers: any = [
|
||||
...triggerInitializers,
|
||||
...(nodeBlockInitializers.length
|
||||
? [
|
||||
{
|
||||
name: 'nodes',
|
||||
type: 'subMenu',
|
||||
title: `{{t("Node result", { ns: "workflow" })}}`,
|
||||
children: nodeBlockInitializers,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
].filter(Boolean);
|
||||
return dataBlockInitializers;
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'itemGroup',
|
||||
name: 'otherBlocks',
|
||||
title: '{{t("Other blocks")}}',
|
||||
children: [
|
||||
{
|
||||
name: 'markdown',
|
||||
title: '{{t("Markdown")}}',
|
||||
Component: 'MarkdownBlockInitializer',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
function SchemaContent({ value, onChange }) {
|
||||
const api = useAPIClient();
|
||||
const workflowPlugin = usePlugin(PluginWorkflowClient);
|
||||
const { components, scope } = useSchemaOptionsContext();
|
||||
const executed = useWorkflowExecuted();
|
||||
const node = useNodeContext();
|
||||
const nodes = useAvailableUpstreams(node);
|
||||
|
||||
const { data, loading } = useRequest(
|
||||
async () => {
|
||||
if (value) {
|
||||
const { data } = await api.request({ url: `uiSchemas:getJsonSchema/${value}` });
|
||||
if (data.data?.['x-uid'] === value) {
|
||||
return data.data;
|
||||
}
|
||||
}
|
||||
|
||||
const id = value ?? uid();
|
||||
// const content = createEditFormBlockUISchema();
|
||||
const newSchema = {
|
||||
type: 'void',
|
||||
name: id,
|
||||
'x-uid': id,
|
||||
'x-component': 'Grid',
|
||||
'x-initializer': 'workflowCc:popup:configureUserInterface:addBlock',
|
||||
properties: {
|
||||
[uid()]: {
|
||||
type: 'void',
|
||||
'x-component': 'Grid.Row',
|
||||
properties: {
|
||||
[uid()]: {
|
||||
type: 'void',
|
||||
'x-component': 'Grid.Col',
|
||||
'x-component-props': {
|
||||
span: 24,
|
||||
},
|
||||
properties: {
|
||||
// [uid()]: content,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
await api.resource('uiSchemas').insert({
|
||||
values: newSchema,
|
||||
});
|
||||
onChange(id);
|
||||
return newSchema;
|
||||
},
|
||||
{
|
||||
refreshDeps: [value],
|
||||
},
|
||||
);
|
||||
|
||||
if (loading) {
|
||||
return <Spin />;
|
||||
}
|
||||
|
||||
const nodeComponents = {};
|
||||
nodes.forEach((item) => {
|
||||
const instruction = workflowPlugin.instructions.get(item.type);
|
||||
Object.assign(nodeComponents, instruction.components);
|
||||
});
|
||||
|
||||
return (
|
||||
<SchemaComponentProvider scope={scope} components={components} designable={!executed}>
|
||||
<SchemaComponent
|
||||
scope={{
|
||||
useReadAction() {
|
||||
return { run() {} };
|
||||
},
|
||||
}}
|
||||
components={{
|
||||
...nodeComponents,
|
||||
}}
|
||||
schema={data as ISchema}
|
||||
/>
|
||||
</SchemaComponentProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export function SchemaConfig({ value, onChange }) {
|
||||
return (
|
||||
<SchemaComponent
|
||||
components={{
|
||||
SchemaContent,
|
||||
}}
|
||||
schema={
|
||||
{
|
||||
name: 'drawer',
|
||||
type: 'void',
|
||||
title: `{{t("User interface", { ns: "${NAMESPACE}" })}}`,
|
||||
'x-component': 'Action.Container',
|
||||
'x-component-props': {
|
||||
className: css`
|
||||
.ant-drawer-body {
|
||||
background: var(--nb-box-bg);
|
||||
}
|
||||
`,
|
||||
},
|
||||
properties: {
|
||||
ccDetail: {
|
||||
type: 'string',
|
||||
'x-component': 'SchemaContent',
|
||||
},
|
||||
},
|
||||
} as ISchema
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const ConfigChangedContext = React.createContext<any>({});
|
||||
|
||||
const ForceReadHint = observer(() => {
|
||||
const { initialValues, values } = useForm();
|
||||
|
||||
return initialValues.forceRead != values.forceRead ? (
|
||||
<Typography.Paragraph type="secondary">
|
||||
<Typography.Text type="warning">{lang('Force read changes unsaved, click "Submit" to save.')}</Typography.Text>
|
||||
</Typography.Paragraph>
|
||||
) : null;
|
||||
});
|
||||
|
||||
export function SchemaConfigButton(props) {
|
||||
const executed = useWorkflowExecuted();
|
||||
const { setFormValueChanged } = useActionContext();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const { t } = usePluginTranslation();
|
||||
return (
|
||||
<ConfigChangedContext.Provider value={{ setFormValueChanged }}>
|
||||
<Button type="primary" onClick={() => setVisible(true)} disabled={false}>
|
||||
{t(executed ? 'View user interface' : 'Configure user interface')}
|
||||
</Button>
|
||||
<ActionContextProvider value={{ visible, setVisible, formValueChanged: false }}>
|
||||
{props.children}
|
||||
</ActionContextProvider>
|
||||
<ForceReadHint />
|
||||
</ConfigChangedContext.Provider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* This file is part of the NocoBase (R) project.
|
||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||
* Authors: NocoBase Team.
|
||||
*
|
||||
* This program is offered under a commercial license.
|
||||
* For more information, see <https://www.nocobase.com/agreement>
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { ArrayItems } from '@formily/antd-v5';
|
||||
import { uid } from '@formily/shared';
|
||||
import _ from 'lodash';
|
||||
import { EyeOutlined } from '@ant-design/icons';
|
||||
|
||||
import { css } from '@nocobase/client';
|
||||
|
||||
import {
|
||||
Instruction,
|
||||
WorkflowVariableTextArea,
|
||||
UsersAddition,
|
||||
UsersSelect,
|
||||
useNodeContext,
|
||||
} from '@nocobase/plugin-workflow/client';
|
||||
|
||||
import { NAMESPACE } from '../../common/constants';
|
||||
// import NotificationFieldset from '../NotificationFieldset';
|
||||
// import { SchemaConfig, SchemaConfigButton } from './SchemaConfig';
|
||||
import { SchemaConfig, SchemaConfigButton } from './SchemaConfig';
|
||||
|
||||
export default class extends Instruction {
|
||||
title = `{{t("CC", { ns: "${NAMESPACE}" })}}`;
|
||||
type = 'cc';
|
||||
group = 'manual';
|
||||
description = `{{t("Information within the workflow can be freely configured into a view interface and made accessible to specific users. A CC task will be generated for them in the tasks center.", { ns: "${NAMESPACE}" })}}`;
|
||||
icon = (<EyeOutlined />);
|
||||
fieldset = {
|
||||
users: {
|
||||
type: 'array',
|
||||
title: `{{t("Recipients", { ns: "${NAMESPACE}" })}}`,
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'ArrayItems',
|
||||
'x-component-props': {
|
||||
className: css`
|
||||
&[disabled] {
|
||||
> .ant-formily-array-base-addition {
|
||||
display: none;
|
||||
}
|
||||
> .ant-formily-array-items-item .ant-space-item:not(:nth-child(2)) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
`,
|
||||
},
|
||||
items: {
|
||||
type: 'void',
|
||||
'x-component': 'Space',
|
||||
'x-component-props': {
|
||||
className: css`
|
||||
width: 100%;
|
||||
&.ant-space.ant-space-horizontal {
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
> .ant-space-item:nth-child(2) {
|
||||
flex-grow: 1;
|
||||
}
|
||||
`,
|
||||
},
|
||||
properties: {
|
||||
sort: {
|
||||
type: 'void',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'ArrayItems.SortHandle',
|
||||
},
|
||||
input: {
|
||||
type: 'string',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'UsersSelect',
|
||||
},
|
||||
remove: {
|
||||
type: 'void',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'ArrayItems.Remove',
|
||||
},
|
||||
},
|
||||
},
|
||||
required: true,
|
||||
properties: {
|
||||
add: {
|
||||
type: 'void',
|
||||
'x-component': 'UsersAddition',
|
||||
},
|
||||
},
|
||||
},
|
||||
ccDetail: {
|
||||
type: 'void',
|
||||
title: `{{t("User interface", { ns: "${NAMESPACE}" })}}`,
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'SchemaConfigButton',
|
||||
properties: {
|
||||
ccDetail: {
|
||||
type: 'void',
|
||||
'x-component': 'SchemaConfig',
|
||||
},
|
||||
},
|
||||
required: true,
|
||||
},
|
||||
title: {
|
||||
type: 'string',
|
||||
title: `{{t("Task title", { ns: "${NAMESPACE}" })}}`,
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'WorkflowVariableTextArea',
|
||||
description: `{{t("Title of each CC item in tasks center. Could use variables in string template. Default to node title.", { ns: "${NAMESPACE}" })}}`,
|
||||
default: '{{useNodeContext().title}}',
|
||||
},
|
||||
notifications: {
|
||||
type: 'void',
|
||||
'x-component': 'NotificationFieldset',
|
||||
},
|
||||
};
|
||||
createDefaultConfig() {
|
||||
return {
|
||||
ccDetail: uid(),
|
||||
};
|
||||
}
|
||||
scope = {
|
||||
useNodeContext,
|
||||
};
|
||||
components = {
|
||||
ArrayItems,
|
||||
UsersSelect,
|
||||
UsersAddition,
|
||||
SchemaConfigButton,
|
||||
SchemaConfig,
|
||||
WorkflowVariableTextArea,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* This file is part of the NocoBase (R) project.
|
||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||
* Authors: NocoBase Team.
|
||||
*
|
||||
* This program is offered under a commercial license.
|
||||
* For more information, see <https://www.nocobase.com/agreement>
|
||||
*/
|
||||
|
||||
import { i18n } from '@nocobase/client';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { NAMESPACE } from '../common/constants';
|
||||
|
||||
export function lang(key: string, options = {}) {
|
||||
return i18n.t(key, { ...options, ns: NAMESPACE });
|
||||
}
|
||||
|
||||
export function usePluginTranslation() {
|
||||
return useTranslation(NAMESPACE);
|
||||
}
|
||||
@@ -0,0 +1,448 @@
|
||||
/**
|
||||
* 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, { useCallback, useContext, useEffect, useState } from 'react';
|
||||
import { useAntdToken } from 'antd-style';
|
||||
import { Card, ConfigProvider, Descriptions, Spin } from 'antd';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { get } from 'lodash';
|
||||
|
||||
import {
|
||||
SchemaComponentContext,
|
||||
useCollectionRecordData,
|
||||
SchemaComponent,
|
||||
css,
|
||||
useCurrentUserContext,
|
||||
useAPIClient,
|
||||
usePlugin,
|
||||
useFormBlockContext,
|
||||
RemoteSchemaComponent,
|
||||
CollectionRecordProvider,
|
||||
useActionContext,
|
||||
useListBlockContext,
|
||||
} from '@nocobase/client';
|
||||
import PluginWorkflowClient, {
|
||||
DetailsBlockProvider,
|
||||
FlowContext,
|
||||
linkNodes,
|
||||
NodeContext,
|
||||
useAvailableUpstreams,
|
||||
usePopupRecordContext,
|
||||
useTasksCountsContext,
|
||||
WorkflowTitle,
|
||||
} from '@nocobase/plugin-workflow/client';
|
||||
|
||||
import { NAMESPACE, TASK_STATUS, TASK_TYPE_CC } from '../common/constants';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { lang } from './locale';
|
||||
|
||||
function useDetailsBlockProps() {
|
||||
const { form } = useFormBlockContext();
|
||||
return { form };
|
||||
}
|
||||
|
||||
function useReadAction() {
|
||||
const record = useCollectionRecordData();
|
||||
const ctx = useActionContext();
|
||||
const api = useAPIClient();
|
||||
const { id } = useCollectionRecordData();
|
||||
const { service } = useListBlockContext() || {};
|
||||
const action = record.status ? 'unread' : 'read';
|
||||
return {
|
||||
async run() {
|
||||
try {
|
||||
await api.resource('workflowCcTasks')[action]({
|
||||
filterByTk: id,
|
||||
});
|
||||
ctx.setVisible(false);
|
||||
ctx.setSubmitted(true);
|
||||
service?.refresh?.();
|
||||
} catch (error) {
|
||||
console.error('Failed to mark task as read:', error);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function useReadActionProps(props) {
|
||||
const record = useCollectionRecordData();
|
||||
return record.status
|
||||
? {
|
||||
title: lang('Mark as unread'),
|
||||
}
|
||||
: {
|
||||
type: 'primary',
|
||||
title: lang('Mark as read'),
|
||||
};
|
||||
}
|
||||
|
||||
function useReadAllAction() {
|
||||
const api = useAPIClient();
|
||||
const ctx = useListBlockContext();
|
||||
|
||||
return {
|
||||
async run() {
|
||||
try {
|
||||
await api.resource('workflowCcTasks').read();
|
||||
ctx?.service?.refresh?.();
|
||||
} catch (error) {
|
||||
console.error('Failed to mark task as read:', error);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function useReadAllActionProps(props) {
|
||||
const { counts } = useTasksCountsContext();
|
||||
return {
|
||||
...props,
|
||||
disabled: !counts[TASK_TYPE_CC].pending,
|
||||
};
|
||||
}
|
||||
|
||||
function FlowContextProvider(props) {
|
||||
const workflowPlugin = usePlugin(PluginWorkflowClient);
|
||||
const api = useAPIClient();
|
||||
const { id } = useCollectionRecordData() || {};
|
||||
const [flowContext, setFlowContext] = useState<any>(null);
|
||||
const [record, setRecord] = useState<any>(useCollectionRecordData());
|
||||
const [node, setNode] = useState<any>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!id) {
|
||||
return;
|
||||
}
|
||||
api
|
||||
.resource('workflowCcTasks')
|
||||
.get?.({
|
||||
filterByTk: id,
|
||||
appends: ['node', 'job', 'workflow', 'workflow.nodes', 'execution', 'execution.jobs'],
|
||||
})
|
||||
.then(({ data }) => {
|
||||
const { node, workflow: { nodes = [], ...workflow } = {}, execution } = data?.data ?? {};
|
||||
linkNodes(nodes);
|
||||
setNode(node);
|
||||
setFlowContext({
|
||||
workflow,
|
||||
nodes,
|
||||
execution,
|
||||
});
|
||||
setRecord(data?.data);
|
||||
return;
|
||||
});
|
||||
}, [api, id]);
|
||||
|
||||
const upstreams = useAvailableUpstreams(flowContext?.nodes.find((item) => item.id === node.id));
|
||||
const nodeComponents = upstreams.reduce(
|
||||
(components, { type }) => Object.assign(components, workflowPlugin.instructions.get(type).components),
|
||||
{},
|
||||
);
|
||||
|
||||
return node && flowContext ? (
|
||||
<CollectionRecordProvider record={record}>
|
||||
<FlowContext.Provider value={flowContext}>
|
||||
<NodeContext.Provider value={node}>
|
||||
<RemoteSchemaComponent
|
||||
components={{
|
||||
DetailsBlockProvider,
|
||||
...nodeComponents,
|
||||
}}
|
||||
scope={{
|
||||
useDetailsBlockProps,
|
||||
}}
|
||||
uid={node.config?.ccDetail}
|
||||
noForm
|
||||
/>
|
||||
</NodeContext.Provider>
|
||||
</FlowContext.Provider>
|
||||
</CollectionRecordProvider>
|
||||
) : (
|
||||
<Spin />
|
||||
);
|
||||
}
|
||||
|
||||
function Drawer() {
|
||||
const ctx = useContext(SchemaComponentContext);
|
||||
const record = useCollectionRecordData();
|
||||
const { id, node, workflow, status } = record || {};
|
||||
|
||||
return record ? (
|
||||
<SchemaComponentContext.Provider value={{ ...ctx, reset() {}, designable: false }}>
|
||||
<SchemaComponent
|
||||
components={{
|
||||
FlowContextProvider,
|
||||
}}
|
||||
scope={{
|
||||
useReadAction,
|
||||
useReadActionProps,
|
||||
}}
|
||||
schema={{
|
||||
type: 'void',
|
||||
name: `manual-detail-drawer-${id}-${status}`,
|
||||
'x-component': 'Action.Container',
|
||||
'x-component-props': {
|
||||
className: css`
|
||||
.ant-modal-content {
|
||||
background: var(--nb-box-bg);
|
||||
}
|
||||
.ant-modal-header {
|
||||
background: none;
|
||||
}
|
||||
`,
|
||||
},
|
||||
title: node?.title ?? `${workflow?.title} - ${`#${node?.id}`}`,
|
||||
properties: {
|
||||
tabs: {
|
||||
type: 'void',
|
||||
'x-component': 'FlowContextProvider',
|
||||
},
|
||||
footer: {
|
||||
type: 'void',
|
||||
'x-component': 'Action.Container.Footer',
|
||||
properties: {
|
||||
content: {
|
||||
type: 'void',
|
||||
'x-component': 'ActionBar',
|
||||
'x-component-props': {
|
||||
style: {
|
||||
marginBottom: 0,
|
||||
},
|
||||
},
|
||||
properties: {
|
||||
read: {
|
||||
type: 'void',
|
||||
'x-component': 'Action',
|
||||
'x-use-component-props': 'useReadActionProps',
|
||||
'x-component-props': {
|
||||
useAction: '{{useReadAction}}',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</SchemaComponentContext.Provider>
|
||||
) : null;
|
||||
}
|
||||
|
||||
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 RecordTitle(props) {
|
||||
const record = useCollectionRecordData();
|
||||
if (Array.isArray(props.dataIndex)) {
|
||||
for (const index of props.dataIndex) {
|
||||
const title = get(record, index);
|
||||
if (title) {
|
||||
return title;
|
||||
}
|
||||
}
|
||||
}
|
||||
return get(record, props.dataIndex);
|
||||
}
|
||||
|
||||
function ContentDetailWithTitle(props) {
|
||||
return (
|
||||
<ContentDetail
|
||||
title={<RecordTitle dataIndex={['title', 'node.title']} />}
|
||||
extra={<RecordTitle dataIndex={'workflow.title'} />}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function TaskItem() {
|
||||
const token = useAntdToken();
|
||||
const record = useCollectionRecordData();
|
||||
const navigate = useNavigate();
|
||||
const { setRecord } = usePopupRecordContext();
|
||||
const onOpen = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
const targetElement = e.target as Element; // 将事件目标转换为Element类型
|
||||
const currentTargetElement = e.currentTarget as Element;
|
||||
if (currentTargetElement.contains(targetElement)) {
|
||||
setRecord(record);
|
||||
navigate(`./${record.id}`);
|
||||
}
|
||||
e.stopPropagation();
|
||||
},
|
||||
[navigate, record, setRecord],
|
||||
);
|
||||
|
||||
return (
|
||||
<Card
|
||||
onClick={onOpen}
|
||||
hoverable
|
||||
size="small"
|
||||
title={record.title}
|
||||
extra={<WorkflowTitle {...record.workflow} />}
|
||||
className={css`
|
||||
.ant-card-extra {
|
||||
color: ${token.colorTextDescription};
|
||||
}
|
||||
`}
|
||||
>
|
||||
<ContentDetail />
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
const StatusFilterMap = {
|
||||
pending: {
|
||||
status: TASK_STATUS.UNREAD,
|
||||
},
|
||||
completed: {
|
||||
status: TASK_STATUS.READ,
|
||||
},
|
||||
};
|
||||
|
||||
function useTodoActionParams(status) {
|
||||
const { data: user } = useCurrentUserContext();
|
||||
const filter = StatusFilterMap[status] ?? {};
|
||||
return {
|
||||
filter,
|
||||
appends: [
|
||||
'node.id',
|
||||
'node.title',
|
||||
'workflow.id',
|
||||
'workflow.title',
|
||||
'workflow.enabled',
|
||||
'execution.id',
|
||||
'execution.status',
|
||||
],
|
||||
except: ['node.config', 'workflow.config', 'workflow.options', 'execution.context', 'execution.output'],
|
||||
};
|
||||
}
|
||||
|
||||
function TodoExtraActions(props) {
|
||||
return (
|
||||
<SchemaComponent
|
||||
scope={{
|
||||
useReadAllAction,
|
||||
useReadAllActionProps,
|
||||
}}
|
||||
schema={{
|
||||
name: 'actions',
|
||||
type: 'void',
|
||||
'x-component': 'ActionBar',
|
||||
properties: {
|
||||
refresh: {
|
||||
type: 'void',
|
||||
title: '{{ t("Refresh") }}',
|
||||
'x-component': 'Action',
|
||||
'x-use-component-props': 'useRefreshActionProps',
|
||||
'x-component-props': {
|
||||
icon: 'ReloadOutlined',
|
||||
...props,
|
||||
},
|
||||
},
|
||||
filter: {
|
||||
type: 'void',
|
||||
title: '{{t("Filter")}}',
|
||||
'x-component': 'Filter.Action',
|
||||
'x-use-component-props': 'useFilterActionProps',
|
||||
'x-component-props': {
|
||||
icon: 'FilterOutlined',
|
||||
...props,
|
||||
},
|
||||
default: {
|
||||
$and: [{ title: { $includes: '' } }, { 'workflow.title': { $includes: '' } }],
|
||||
},
|
||||
},
|
||||
readAll: {
|
||||
type: 'void',
|
||||
title: `{{t("Mark all as read", { ns: "${NAMESPACE}" })}}`,
|
||||
'x-component': 'Action',
|
||||
'x-use-component-props': 'useReadAllActionProps',
|
||||
'x-component-props': {
|
||||
useAction: '{{useReadAllAction}}',
|
||||
icon: 'CheckOutlined',
|
||||
...props,
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export const ccTodo = {
|
||||
title: `{{t("CC to me", { ns: "${NAMESPACE}" })}}`,
|
||||
collection: 'workflowCcTasks',
|
||||
action: 'listMine',
|
||||
useActionParams: useTodoActionParams,
|
||||
Actions: TodoExtraActions,
|
||||
Item: TaskItem,
|
||||
Detail: Drawer,
|
||||
};
|
||||
+144
@@ -0,0 +1,144 @@
|
||||
/**
|
||||
* 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 { NAMESPACE } from '../constants';
|
||||
|
||||
export default {
|
||||
name: 'workflowCcTasks',
|
||||
dumpRules: {
|
||||
group: 'log',
|
||||
},
|
||||
migrationRules: ['schema-only'],
|
||||
shared: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
fields: [
|
||||
{
|
||||
type: 'bigInt',
|
||||
name: 'id',
|
||||
primaryKey: true,
|
||||
autoIncrement: true,
|
||||
},
|
||||
{
|
||||
type: 'belongsTo',
|
||||
name: 'job',
|
||||
target: 'jobs',
|
||||
foreignKey: 'jobId',
|
||||
primaryKey: false,
|
||||
},
|
||||
{
|
||||
type: 'belongsTo',
|
||||
name: 'user',
|
||||
target: 'users',
|
||||
foreignKey: 'userId',
|
||||
primaryKey: false,
|
||||
},
|
||||
{
|
||||
type: 'string',
|
||||
name: 'title',
|
||||
interface: 'input',
|
||||
uiSchema: {
|
||||
type: 'string',
|
||||
title: `{{t("Task title", { ns: "${NAMESPACE}" })}}`,
|
||||
'x-component': 'Input',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'belongsTo',
|
||||
name: 'execution',
|
||||
onDelete: 'CASCADE',
|
||||
},
|
||||
{
|
||||
type: 'belongsTo',
|
||||
name: 'node',
|
||||
target: 'flow_nodes',
|
||||
},
|
||||
{
|
||||
type: 'belongsTo',
|
||||
name: 'workflow',
|
||||
target: 'workflows',
|
||||
foreignKey: 'workflowId',
|
||||
onDelete: 'CASCADE',
|
||||
interface: 'm2o',
|
||||
uiSchema: {
|
||||
type: 'object',
|
||||
title: `{{t("Workflow", { ns: "workflow" })}}`,
|
||||
'x-component': 'AssociationField',
|
||||
'x-component-props': {
|
||||
fieldNames: {
|
||||
label: 'title',
|
||||
value: 'id',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'integer',
|
||||
name: 'status',
|
||||
interface: 'select',
|
||||
uiSchema: {
|
||||
type: 'number',
|
||||
title: `{{t("Status", { ns: "${NAMESPACE}" })}}`,
|
||||
'x-component': 'Select',
|
||||
enum: [
|
||||
{
|
||||
label: `{{t("Unread", { ns: "${NAMESPACE}" })}}`,
|
||||
value: 0,
|
||||
color: 'gold',
|
||||
},
|
||||
{
|
||||
label: `{{t("Read", { ns: "${NAMESPACE}" })}}`,
|
||||
value: 1,
|
||||
color: 'green',
|
||||
},
|
||||
],
|
||||
},
|
||||
defaultValue: 0,
|
||||
},
|
||||
{
|
||||
type: 'date',
|
||||
name: 'readAt',
|
||||
interface: 'datetime',
|
||||
uiSchema: {
|
||||
type: 'string',
|
||||
title: `{{t("Read at", { ns: "${NAMESPACE}" })}}`,
|
||||
'x-component': 'DatePicker',
|
||||
'x-component-props': {
|
||||
showTime: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'date',
|
||||
name: 'createdAt',
|
||||
interface: 'createdAt',
|
||||
uiSchema: {
|
||||
type: 'datetime',
|
||||
title: '{{t("Created at")}}',
|
||||
'x-component': 'DatePicker',
|
||||
'x-component-props': {
|
||||
showTime: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'date',
|
||||
name: 'updatedAt',
|
||||
interface: 'updatedAt',
|
||||
uiSchema: {
|
||||
type: 'datetime',
|
||||
title: '{{t("Updated at")}}',
|
||||
'x-component': 'DatePicker',
|
||||
'x-component-props': {
|
||||
showTime: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -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 const NAMESPACE = '@nocobase/plugin-workflow-cc';
|
||||
|
||||
export const TASK_TYPE_CC = 'cc';
|
||||
|
||||
export const TASK_STATUS = {
|
||||
UNREAD: 0,
|
||||
READ: 1,
|
||||
};
|
||||
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* 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 * from './server';
|
||||
export { default } from './server';
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"CC": "CC",
|
||||
"Information within the workflow can be freely configured into a view interface and made accessible to specific users. A CC task will be generated for them in the tasks center.": "Information within the workflow can be freely configured into a view interface and made accessible to specific users. A CC task will be generated for them in the tasks center.",
|
||||
"Recipients": "Recipients",
|
||||
"User interface": "User interface",
|
||||
"View user interface": "View user interface",
|
||||
"Configure user interface": "Configure user interface",
|
||||
"Title of each CC item in tasks center. Could use variables in string template. Default to node title.": "Title of each CC item in tasks center. Could use variables in string template. Default to node title.",
|
||||
"Read": "Read",
|
||||
"Unread": "Unread",
|
||||
"Mark as read": "Mark as read",
|
||||
"Mark as unread": "Mark as unread",
|
||||
"Mark all as read": "Mark all as read",
|
||||
"CC to me": "CC to me"
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"CC": "抄送",
|
||||
"Information within the workflow can be freely configured into a view interface and made accessible to specific users. A CC task will be generated for them in the tasks center.": "可将流程中的信息自由配置为查看界面,提供给特定的用户查看,将在待办中心为其生成抄送任务。",
|
||||
"Recipients": "接收人",
|
||||
"User interface": "用户界面",
|
||||
"View user interface": "查看用户界面",
|
||||
"Configure user interface": "配置用户界面",
|
||||
"Title of each CC item in tasks center. Could use variables in string template. Default to node title.": "待办中心每个抄送项的标题。可以在字符串模板中使用变量。默认为节点标题。",
|
||||
"Read": "已读",
|
||||
"Unread": "未读",
|
||||
"Mark as read": "标为已读",
|
||||
"Mark as unread": "标为未读",
|
||||
"Mark all as read": "全部标为已读",
|
||||
"CC to me": "抄送我的"
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
/**
|
||||
* 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 { isValidFilter, uid } from '@nocobase/utils';
|
||||
import { UiSchemaRepository } from '@nocobase/plugin-ui-schema-storage';
|
||||
import { Instruction, JOB_STATUS } from '@nocobase/plugin-workflow';
|
||||
import { TASK_STATUS } from '../common/constants';
|
||||
|
||||
async function getUsers(config, { db, transaction }): Promise<number[]> {
|
||||
const users: Set<number> = new Set();
|
||||
const UserRepo = db.getRepository('users');
|
||||
for (const item of config) {
|
||||
if (typeof item === 'object') {
|
||||
if (!isValidFilter(item.filter)) {
|
||||
continue;
|
||||
}
|
||||
const result = await UserRepo.find({
|
||||
...item,
|
||||
fields: ['id'],
|
||||
transaction,
|
||||
});
|
||||
result.forEach((item) => users.add(item.id));
|
||||
} else {
|
||||
users.add(item);
|
||||
}
|
||||
}
|
||||
|
||||
return [...users];
|
||||
}
|
||||
|
||||
export default class CCInstruction extends Instruction {
|
||||
static type = 'cc';
|
||||
|
||||
async run(node, prevJob, processor) {
|
||||
const { db } = processor.options.plugin;
|
||||
const job = processor.saveJob({
|
||||
status: JOB_STATUS.RESOLVED,
|
||||
nodeId: node.id,
|
||||
nodeKey: node.key,
|
||||
upstreamId: prevJob?.id ?? null,
|
||||
});
|
||||
const usersConfig = processor
|
||||
.getParsedValue(node.config.users ?? [], node.id)
|
||||
.flat()
|
||||
.filter(Boolean);
|
||||
const users = await getUsers(usersConfig, { db, transaction: processor.mainTransaction });
|
||||
|
||||
const RecordRepo = db.getRepository('workflowCcTasks');
|
||||
const title = node.config.title ? processor.getParsedValue(node.config.title, node.id) : node.title;
|
||||
const records = await RecordRepo.createMany({
|
||||
records: users.map((userId, index) => ({
|
||||
userId,
|
||||
jobId: job.id,
|
||||
nodeId: node.id,
|
||||
executionId: job.executionId,
|
||||
workflowId: node.workflowId,
|
||||
status: TASK_STATUS.UNREAD,
|
||||
title,
|
||||
})),
|
||||
transaction: processor.mainTransaction,
|
||||
});
|
||||
|
||||
// const { notifications = [] } = node.config;
|
||||
// if (notifications.length) {
|
||||
// records
|
||||
// .filter((record) => record.status === TASK_STATUS.UNREAD)
|
||||
// .forEach((record) => {
|
||||
// this.notify(record, context, node, processor);
|
||||
// });
|
||||
// }
|
||||
|
||||
return job;
|
||||
}
|
||||
|
||||
async duplicateConfig(node, { transaction }) {
|
||||
const uiSchemaRepo = this.workflow.app.db.getRepository('uiSchemas') as UiSchemaRepository;
|
||||
if (!node.config.ccDetail) {
|
||||
return node.config;
|
||||
}
|
||||
const result = await uiSchemaRepo.duplicate(node.config.ccDetail, {
|
||||
transaction,
|
||||
});
|
||||
|
||||
return {
|
||||
...node.config,
|
||||
ccDetail: result?.['x-uid'] ?? uid(),
|
||||
};
|
||||
}
|
||||
}
|
||||
+295
@@ -0,0 +1,295 @@
|
||||
/**
|
||||
* 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 Database from '@nocobase/database';
|
||||
import { MockServer } from '@nocobase/test';
|
||||
|
||||
import PluginWorkflow, { EXECUTION_STATUS, JOB_STATUS } from '@nocobase/plugin-workflow';
|
||||
import { getApp, sleep } from '@nocobase/plugin-workflow-test';
|
||||
import Plugin from '..';
|
||||
import { TASK_STATUS, TASK_TYPE_CC } from '../../common/constants';
|
||||
|
||||
describe('workflow > instructions > cc', () => {
|
||||
let app: MockServer;
|
||||
let db: Database;
|
||||
let WorkflowModel;
|
||||
let workflow;
|
||||
let workflowPlugin: PluginWorkflow;
|
||||
let ccPlugin: Plugin;
|
||||
let root;
|
||||
let rootAgent;
|
||||
let users;
|
||||
let userAgents;
|
||||
let AssignmentModel;
|
||||
|
||||
beforeEach(async () => {
|
||||
app = await getApp({
|
||||
plugins: ['users', 'auth', [Plugin, { packageName: '@nocobase/plugin-workflow-cc' }]],
|
||||
});
|
||||
|
||||
db = app.db;
|
||||
|
||||
workflowPlugin = app.pm.get(PluginWorkflow);
|
||||
ccPlugin = app.pm.get(Plugin);
|
||||
|
||||
WorkflowModel = db.getCollection('workflows').model;
|
||||
AssignmentModel = db.getCollection('workflowCcTasks').model;
|
||||
|
||||
const UserRepo = db.getRepository('users');
|
||||
|
||||
root = await UserRepo.findOne({});
|
||||
|
||||
users = await UserRepo.createMany({
|
||||
records: [
|
||||
{ id: 2, nickname: 'a', email: 'a@test.com', password: 'password' },
|
||||
{ id: 3, nickname: 'b', email: 'b@test.com', password: 'password' },
|
||||
{ id: 4, nickname: 'c', email: 'c@test.com', password: 'password' },
|
||||
],
|
||||
});
|
||||
|
||||
rootAgent = await app.agent().login(root);
|
||||
userAgents = await Promise.all(users.map((user) => app.agent().login(user)));
|
||||
|
||||
workflow = await WorkflowModel.create({
|
||||
enabled: true,
|
||||
type: 'syncTrigger',
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await app.destroy();
|
||||
});
|
||||
|
||||
describe('recipients', () => {
|
||||
it('should do nothing when no recipients configured', async () => {
|
||||
await workflow.createNode({
|
||||
type: 'cc',
|
||||
config: {
|
||||
title: 'test title',
|
||||
},
|
||||
});
|
||||
|
||||
await workflowPlugin.trigger(workflow, {});
|
||||
|
||||
const [execution] = await workflow.getExecutions();
|
||||
expect(execution.status).toBe(EXECUTION_STATUS.RESOLVED);
|
||||
|
||||
const jobs = await execution.getJobs();
|
||||
expect(jobs.length).toBe(1);
|
||||
expect(jobs[0].status).toBe(EXECUTION_STATUS.RESOLVED);
|
||||
|
||||
const assignmentCount = await AssignmentModel.count();
|
||||
expect(assignmentCount).toBe(0);
|
||||
});
|
||||
|
||||
it('task should be created to static user', async () => {
|
||||
await workflow.createNode({
|
||||
type: 'cc',
|
||||
config: {
|
||||
title: 'test title',
|
||||
users: [users[0].id, users[1].id],
|
||||
},
|
||||
});
|
||||
|
||||
await workflowPlugin.trigger(workflow, {});
|
||||
|
||||
const [execution] = await workflow.getExecutions();
|
||||
expect(execution.status).toBe(EXECUTION_STATUS.RESOLVED);
|
||||
|
||||
const jobs = await execution.getJobs();
|
||||
expect(jobs.length).toBe(1);
|
||||
expect(jobs[0].status).toBe(JOB_STATUS.RESOLVED);
|
||||
|
||||
const assignments = await AssignmentModel.findAll();
|
||||
expect(assignments.length).toBe(2);
|
||||
expect(assignments.map((item) => item.userId).sort()).toEqual([users[0].id, users[1].id].sort());
|
||||
assignments.forEach((item) => {
|
||||
expect(item.status).toBe(TASK_STATUS.UNREAD);
|
||||
});
|
||||
});
|
||||
|
||||
it('task should be created to user by variable', async () => {
|
||||
await workflow.createNode({
|
||||
type: 'cc',
|
||||
config: {
|
||||
title: 'test title',
|
||||
users: ['{{$context.data.userId}}'],
|
||||
},
|
||||
});
|
||||
|
||||
await workflowPlugin.trigger(workflow, {
|
||||
data: {
|
||||
userId: users[0].id,
|
||||
},
|
||||
});
|
||||
|
||||
const [execution] = await workflow.getExecutions();
|
||||
expect(execution.status).toBe(EXECUTION_STATUS.RESOLVED);
|
||||
|
||||
const jobs = await execution.getJobs();
|
||||
expect(jobs.length).toBe(1);
|
||||
expect(jobs[0].status).toBe(JOB_STATUS.RESOLVED);
|
||||
|
||||
const assignments = await AssignmentModel.findAll();
|
||||
expect(assignments.length).toBe(1);
|
||||
expect(assignments[0].userId).toBe(users[0].id);
|
||||
expect(assignments[0].status).toBe(TASK_STATUS.UNREAD);
|
||||
});
|
||||
|
||||
it('task should be created to user by query', async () => {
|
||||
await workflow.createNode({
|
||||
type: 'cc',
|
||||
config: {
|
||||
title: 'test title',
|
||||
users: [
|
||||
{
|
||||
filter: {
|
||||
id: ['{{$context.data.userId}}'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
await workflowPlugin.trigger(workflow, {
|
||||
data: {
|
||||
userId: users[0].id,
|
||||
},
|
||||
});
|
||||
|
||||
const [execution] = await workflow.getExecutions();
|
||||
expect(execution.status).toBe(EXECUTION_STATUS.RESOLVED);
|
||||
|
||||
const jobs = await execution.getJobs();
|
||||
expect(jobs.length).toBe(1);
|
||||
expect(jobs[0].status).toBe(JOB_STATUS.RESOLVED);
|
||||
|
||||
const assignments = await AssignmentModel.findAll();
|
||||
expect(assignments.length).toBe(1);
|
||||
expect(assignments[0].userId).toBe(users[0].id);
|
||||
expect(assignments[0].status).toBe(TASK_STATUS.UNREAD);
|
||||
});
|
||||
});
|
||||
|
||||
describe('task title', () => {
|
||||
it('should use node title as task title', async () => {
|
||||
await workflow.createNode({
|
||||
type: 'cc',
|
||||
config: {
|
||||
title: 'test title for {{$context.a}}',
|
||||
users: [users[0].id, users[1].id],
|
||||
},
|
||||
});
|
||||
await workflowPlugin.trigger(workflow, { a: 123 });
|
||||
|
||||
const [execution] = await workflow.getExecutions();
|
||||
expect(execution.status).toBe(EXECUTION_STATUS.RESOLVED);
|
||||
|
||||
const jobs = await execution.getJobs();
|
||||
expect(jobs.length).toBe(1);
|
||||
expect(jobs[0].status).toBe(JOB_STATUS.RESOLVED);
|
||||
|
||||
const assignments = await AssignmentModel.findAll();
|
||||
expect(assignments.length).toBe(2);
|
||||
expect(assignments.map((item) => item.title)).toEqual(['test title for 123', 'test title for 123']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('actions', () => {
|
||||
let UserTasksRepo;
|
||||
beforeEach(async () => {
|
||||
UserTasksRepo = db.getRepository('userWorkflowTasks');
|
||||
await workflow.createNode({
|
||||
type: 'cc',
|
||||
config: {
|
||||
users: [users[0].id, users[1].id],
|
||||
},
|
||||
});
|
||||
|
||||
await workflowPlugin.trigger(workflow, {});
|
||||
});
|
||||
|
||||
it('should read a task', async () => {
|
||||
const uts1 = await UserTasksRepo.find({ sort: 'userId' });
|
||||
expect(uts1.length).toBe(2);
|
||||
expect(uts1[0].type).toBe(TASK_TYPE_CC);
|
||||
expect(uts1[1].type).toBe(TASK_TYPE_CC);
|
||||
expect(uts1[0].stats).toEqual({ pending: 1, all: 1 });
|
||||
expect(uts1[1].stats).toEqual({ pending: 1, all: 1 });
|
||||
|
||||
const [assignment] = await AssignmentModel.findAll({ order: [['userId', 'ASC']] });
|
||||
expect(assignment.status).toBe(TASK_STATUS.UNREAD);
|
||||
|
||||
const res1 = await userAgents[0].resource('workflowCcTasks').read({
|
||||
filterByTk: assignment.id,
|
||||
});
|
||||
expect(res1.status).toBe(200);
|
||||
expect(res1.body.data.length).toBe(1);
|
||||
expect(res1.body.data[0].status).toBe(TASK_STATUS.READ);
|
||||
|
||||
const uts2 = await UserTasksRepo.find({ sort: 'userId' });
|
||||
expect(uts2.length).toBe(2);
|
||||
expect(uts2[0].type).toBe(TASK_TYPE_CC);
|
||||
expect(uts2[1].type).toBe(TASK_TYPE_CC);
|
||||
expect(uts2[0].stats).toEqual({ pending: 0, all: 1 });
|
||||
expect(uts2[1].stats).toEqual({ pending: 1, all: 1 });
|
||||
|
||||
await assignment.reload();
|
||||
expect(assignment.status).toBe(TASK_STATUS.READ);
|
||||
|
||||
const res2 = await userAgents[0].resource('workflowCcTasks').read({
|
||||
filterByTk: assignment.id,
|
||||
});
|
||||
expect(res2.status).toBe(200);
|
||||
expect(res2.body.data.length).toBe(0);
|
||||
});
|
||||
|
||||
it('should unread a task', async () => {
|
||||
const [assignment] = await AssignmentModel.findAll({ order: [['userId', 'ASC']] });
|
||||
expect(assignment.status).toBe(TASK_STATUS.UNREAD);
|
||||
|
||||
const res1 = await userAgents[0].resource('workflowCcTasks').unread({
|
||||
filterByTk: assignment.id,
|
||||
});
|
||||
expect(res1.status).toBe(200);
|
||||
await assignment.reload();
|
||||
expect(assignment.status).toBe(TASK_STATUS.UNREAD);
|
||||
|
||||
await assignment.update({ status: TASK_STATUS.READ });
|
||||
|
||||
const res2 = await userAgents[0].resource('workflowCcTasks').unread({
|
||||
filterByTk: assignment.id,
|
||||
});
|
||||
expect(res2.status).toBe(200);
|
||||
expect(res2.body.data.length).toBe(1);
|
||||
expect(res2.body.data[0].status).toBe(TASK_STATUS.UNREAD);
|
||||
|
||||
await assignment.reload();
|
||||
expect(assignment.status).toBe(TASK_STATUS.UNREAD);
|
||||
});
|
||||
|
||||
it('should read all tasks', async () => {
|
||||
const res = await userAgents[0].resource('workflowCcTasks').read();
|
||||
expect(res.status).toBe(200);
|
||||
|
||||
const [a1, a2] = await AssignmentModel.findAll({ order: [['userId', 'ASC']] });
|
||||
expect(a1.status).toBe(TASK_STATUS.READ);
|
||||
expect(a2.status).toBe(TASK_STATUS.UNREAD);
|
||||
});
|
||||
|
||||
it('should not access others task', async () => {
|
||||
const [assignment] = await AssignmentModel.findAll({ order: [['userId', 'ASC']] });
|
||||
|
||||
const res = await userAgents[2].resource('workflowCcTasks').read({
|
||||
filterByTk: assignment.id,
|
||||
});
|
||||
expect(res.status).toBe(403);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,90 @@
|
||||
/**
|
||||
* 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 actions, { Context } from '@nocobase/actions';
|
||||
|
||||
const workflowCcTasks = {
|
||||
async get(context: Context, next) {
|
||||
context.action.mergeParams({
|
||||
filter: {
|
||||
userId: context.state.currentUser.id,
|
||||
},
|
||||
});
|
||||
return actions.get(context, next);
|
||||
},
|
||||
|
||||
async listMine(context: Context, next) {
|
||||
context.action.mergeParams({
|
||||
filter: {
|
||||
userId: context.state.currentUser.id,
|
||||
},
|
||||
});
|
||||
return actions.list(context, next);
|
||||
},
|
||||
|
||||
async read(context: Context, next) {
|
||||
const { filterByTk } = context.action.params;
|
||||
if (filterByTk) {
|
||||
const repository = context.app.db.getRepository('workflowCcTasks');
|
||||
const item = await repository.findOne({ where: { id: filterByTk } });
|
||||
if (!item) {
|
||||
return context.throw(404, 'Task not found');
|
||||
}
|
||||
if (item.userId !== context.state.currentUser.id) {
|
||||
return context.throw(403, 'You do not have permission to access this task');
|
||||
}
|
||||
}
|
||||
context.action.mergeParams({
|
||||
filterByTk,
|
||||
filter: {
|
||||
userId: context.state.currentUser.id,
|
||||
status: 0,
|
||||
},
|
||||
values: {
|
||||
status: 1,
|
||||
readAt: new Date(),
|
||||
},
|
||||
});
|
||||
return actions.update(context, next);
|
||||
},
|
||||
|
||||
async unread(context: Context, next) {
|
||||
const { filterByTk } = context.action.params;
|
||||
if (!filterByTk) {
|
||||
return context.throw(400, 'filterByTk is required for unread action');
|
||||
}
|
||||
context.action.mergeParams({
|
||||
filterByTk,
|
||||
filter: {
|
||||
userId: context.state.currentUser.id,
|
||||
},
|
||||
values: {
|
||||
status: 0,
|
||||
readAt: null,
|
||||
},
|
||||
});
|
||||
return actions.update(context, next);
|
||||
},
|
||||
};
|
||||
|
||||
function make(name, mod) {
|
||||
return Object.keys(mod).reduce(
|
||||
(result, key) => ({
|
||||
...result,
|
||||
[`${name}:${key}`]: mod[key],
|
||||
}),
|
||||
{},
|
||||
);
|
||||
}
|
||||
|
||||
export function initActions(app) {
|
||||
app.resourceManager.registerActionHandlers({
|
||||
...make('workflowCcTasks', workflowCcTasks),
|
||||
});
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* This file is part of the NocoBase (R) project.
|
||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||
* Authors: NocoBase Team.
|
||||
*
|
||||
* This program is offered under a commercial license.
|
||||
* For more information, see <https://www.nocobase.com/agreement>
|
||||
*/
|
||||
|
||||
import { CollectionOptions } from '@nocobase/database';
|
||||
import collectionOptions from '../../common/collections/workflowCcTasks';
|
||||
|
||||
export default collectionOptions as CollectionOptions;
|
||||
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* 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 } from './plugin';
|
||||
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* 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 { Plugin } from '@nocobase/server';
|
||||
import { Model } from '@nocobase/database';
|
||||
|
||||
import PluginWorkflowServer from '@nocobase/plugin-workflow';
|
||||
import CCInstruction from './CCInstruction';
|
||||
import { TASK_STATUS, TASK_TYPE_CC } from '../common/constants';
|
||||
import { initActions } from './actions';
|
||||
|
||||
export class PluginWorkflowCCServer extends Plugin {
|
||||
onRecordSave = async (record: Model, { transaction }) => {
|
||||
// TODO: no applicant should be deprecated
|
||||
if (!record.userId) {
|
||||
return;
|
||||
}
|
||||
const CCModel = record.constructor as unknown as Model;
|
||||
const pending = await CCModel.count({
|
||||
where: {
|
||||
userId: record.userId,
|
||||
status: TASK_STATUS.UNREAD,
|
||||
},
|
||||
transaction,
|
||||
});
|
||||
const all = await CCModel.count({
|
||||
where: {
|
||||
userId: record.userId,
|
||||
},
|
||||
transaction,
|
||||
});
|
||||
await (this.app.pm.get(PluginWorkflowServer) as PluginWorkflowServer).updateTasksStats(
|
||||
record.userId,
|
||||
TASK_TYPE_CC,
|
||||
{ pending, all },
|
||||
{ transaction },
|
||||
);
|
||||
};
|
||||
|
||||
async load() {
|
||||
initActions(this.app);
|
||||
|
||||
this.app.acl.allow('workflowCcTasks', ['list', 'listMine', 'get', 'read', 'unread', 'readAll'], 'loggedIn');
|
||||
|
||||
const workflowPlugin = this.app.pm.get(PluginWorkflowServer) as PluginWorkflowServer;
|
||||
workflowPlugin.registerInstruction('cc', CCInstruction);
|
||||
|
||||
this.app.db.on('workflowCcTasks.afterSave', this.onRecordSave);
|
||||
}
|
||||
}
|
||||
|
||||
export default PluginWorkflowCCServer;
|
||||
Reference in New Issue
Block a user