feat: 工作流审批组件完善 (#673)
Co-authored-by: sealday <zhanglin@daoyoucloud.com> Co-authored-by: sealday <sealday@gmail.com> Reviewed-on: daoyoucloud/tachybase#673 Co-authored-by: bai.zixv <bai.zixv@foxmail.com> Co-committed-by: bai.zixv <bai.zixv@foxmail.com>
This commit is contained in:
parent
090db400a4
commit
1b59cb2a6b
@ -6,7 +6,9 @@ import { useActionContext } from './hooks';
|
|||||||
export const ActionContext = createContext<ActionContextProps>({});
|
export const ActionContext = createContext<ActionContextProps>({});
|
||||||
ActionContext.displayName = 'ActionContext';
|
ActionContext.displayName = 'ActionContext';
|
||||||
|
|
||||||
export const ActionContextProvider: React.FC<ActionContextProps & { value?: ActionContextProps }> = (props) => {
|
export const ActionContextProvider: React.FC<ActionContextProps & { value?: ActionContextProps; children: any }> = (
|
||||||
|
props,
|
||||||
|
) => {
|
||||||
const contextProps = useActionContext();
|
const contextProps = useActionContext();
|
||||||
return (
|
return (
|
||||||
<ActionContext.Provider value={{ ...contextProps, ...props, ...props?.value }}>
|
<ActionContext.Provider value={{ ...contextProps, ...props, ...props?.value }}>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { ExpressionScope, SchemaComponentsContext, SchemaOptionsContext } from '@nocobase/schema';
|
import { ExpressionScope, SchemaComponentsContext, SchemaOptionsContext } from '@nocobase/schema';
|
||||||
import React, { memo, useContext, useMemo } from 'react';
|
import React, { PropsWithChildren, memo, useContext, useMemo } from 'react';
|
||||||
import { ISchemaComponentOptionsProps } from '../types';
|
import { ISchemaComponentOptionsProps } from '../types';
|
||||||
|
|
||||||
export const useSchemaOptionsContext = () => {
|
export const useSchemaOptionsContext = () => {
|
||||||
@ -7,7 +7,7 @@ export const useSchemaOptionsContext = () => {
|
|||||||
return options || {};
|
return options || {};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SchemaComponentOptions: React.FC<ISchemaComponentOptionsProps> = memo((props) => {
|
export const SchemaComponentOptions: React.FC<PropsWithChildren<ISchemaComponentOptionsProps>> = memo((props) => {
|
||||||
const { children } = props;
|
const { children } = props;
|
||||||
const options = useSchemaOptionsContext();
|
const options = useSchemaOptionsContext();
|
||||||
const components = useMemo(() => {
|
const components = useMemo(() => {
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { createForm } from '@nocobase/schema';
|
import { createForm } from '@nocobase/schema';
|
||||||
import { FormProvider, Schema } from '@nocobase/schema';
|
import { FormProvider, Schema } from '@nocobase/schema';
|
||||||
import { uid } from '@nocobase/schema';
|
import { uid } from '@nocobase/schema';
|
||||||
import React, { useMemo, useState } from 'react';
|
import React, { useMemo, useState, PropsWithChildren } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { SchemaComponentContext } from '../context';
|
import { SchemaComponentContext } from '../context';
|
||||||
import { ISchemaComponentProvider } from '../types';
|
import { ISchemaComponentProvider } from '../types';
|
||||||
@ -42,7 +42,7 @@ const Registry = {
|
|||||||
|
|
||||||
Schema.registerCompiler(Registry.compile);
|
Schema.registerCompiler(Registry.compile);
|
||||||
|
|
||||||
export const SchemaComponentProvider: React.FC<ISchemaComponentProvider> = (props) => {
|
export const SchemaComponentProvider: React.FC<PropsWithChildren<ISchemaComponentProvider>> = (props) => {
|
||||||
const { designable, onDesignableChange, components, children } = props;
|
const { designable, onDesignableChange, components, children } = props;
|
||||||
const [uidValue, setUid] = useState(uid());
|
const [uidValue, setUid] = useState(uid());
|
||||||
const [formId, setFormId] = useState(uid());
|
const [formId, setFormId] = useState(uid());
|
||||||
|
@ -3,6 +3,9 @@
|
|||||||
"displayName": "Workflow: Approval Center",
|
"displayName": "Workflow: Approval Center",
|
||||||
"version": "0.1.13",
|
"version": "0.1.13",
|
||||||
"description": "Can configure a universal approval process to complete the complete approval process in the approval center.",
|
"description": "Can configure a universal approval process to complete the complete approval process in the approval center.",
|
||||||
|
"keywords": [
|
||||||
|
"Workflow"
|
||||||
|
],
|
||||||
"main": "dist/server/index.js",
|
"main": "dist/server/index.js",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@ant-design/icons": "5.x",
|
"@ant-design/icons": "5.x",
|
||||||
|
@ -1,26 +0,0 @@
|
|||||||
import { SchemaInitializer } from '@nocobase/client';
|
|
||||||
import { NAMESPACE } from '../locale';
|
|
||||||
import { ke, X } from './refined';
|
|
||||||
|
|
||||||
export const ApprovalApplyAddActionButton = new SchemaInitializer({
|
|
||||||
name: 'ApprovalApplyAddActionButton',
|
|
||||||
title: '{{t("Configure actions")}}',
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
name: 'submit',
|
|
||||||
type: 'item',
|
|
||||||
title: '{{t("Submit")}}',
|
|
||||||
Component: ke,
|
|
||||||
action: X.SUBMITTED,
|
|
||||||
actionProps: { type: 'primary' },
|
|
||||||
disabled: !0,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'save',
|
|
||||||
type: 'item',
|
|
||||||
title: `{{t("Save draft", { ns: "${NAMESPACE}" })}}`,
|
|
||||||
Component: ke,
|
|
||||||
action: X.DRAFT,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
@ -1,23 +0,0 @@
|
|||||||
import { SchemaInitializer, gridRowColWrap } from '@nocobase/client';
|
|
||||||
import { NAMESPACE } from '../locale';
|
|
||||||
import { Po } from './refined';
|
|
||||||
|
|
||||||
export const ApprovalApplyAddBlockButton = new SchemaInitializer({
|
|
||||||
name: 'ApprovalApplyAddBlockButton',
|
|
||||||
wrap: gridRowColWrap,
|
|
||||||
title: '{{t("Add block")}}',
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
name: 'forms',
|
|
||||||
type: 'itemGroup',
|
|
||||||
title: '{{t("Form")}}',
|
|
||||||
children: [{ name: 'form', type: 'item', title: `{{t("Apply form", { ns: "${NAMESPACE}" })}}`, Component: Po }],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'others',
|
|
||||||
type: 'itemGroup',
|
|
||||||
title: '{{t("Other blocks")}}',
|
|
||||||
children: [{ name: 'markdown', type: 'item', title: '{{t("Markdown")}}', Component: 'MarkdownBlockInitializer' }],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
@ -1,163 +0,0 @@
|
|||||||
import { ArrayItems } from '@formily/antd-v5';
|
|
||||||
import { css } from '@nocobase/client';
|
|
||||||
import { Instruction, RadioWithTooltip } from '@nocobase/plugin-workflow/client';
|
|
||||||
import { uid } from '@nocobase/utils/client';
|
|
||||||
import { NAMESPACE } from '../locale';
|
|
||||||
import { T, rt, ot, et, Ro, jo, it, st, at, b } from './refined';
|
|
||||||
|
|
||||||
export class ApprovalInstruction extends Instruction {
|
|
||||||
constructor() {
|
|
||||||
super(...arguments);
|
|
||||||
T(this, 'title', `{{t("Approval", { ns: "${NAMESPACE}" })}}`);
|
|
||||||
T(this, 'type', 'approval');
|
|
||||||
T(
|
|
||||||
this,
|
|
||||||
'description',
|
|
||||||
`{{t("Manual approval operations within the approval process, the approver can approve in the global approval block or in the approval block of a single record.", { ns: "${NAMESPACE}" })}}`
|
|
||||||
);
|
|
||||||
T(this, 'group', 'manual');
|
|
||||||
T(this, 'fieldset', {
|
|
||||||
branchMode: {
|
|
||||||
type: 'boolean',
|
|
||||||
title: `{{t("Pass mode", { ns: "${NAMESPACE}" })}}`,
|
|
||||||
'x-decorator': 'FormItem',
|
|
||||||
'x-component': 'Radio.Group',
|
|
||||||
'x-component-props': { disabled: !0 },
|
|
||||||
enum: rt([
|
|
||||||
{
|
|
||||||
value: !1,
|
|
||||||
label: `{{t('Passthrough mode', { ns: "${NAMESPACE}" })}}`,
|
|
||||||
tooltip: `{{t('When rejected or returned, the workflow will be terminated immediately.', { ns: "${NAMESPACE}" })}}`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: !0,
|
|
||||||
label: `{{t('Branch mode', { ns: "${NAMESPACE}" })}}`,
|
|
||||||
tooltip: `{{t('Could run different branch based on result.', { ns: "${NAMESPACE}" })}}`,
|
|
||||||
},
|
|
||||||
]),
|
|
||||||
default: !1,
|
|
||||||
},
|
|
||||||
assignees: {
|
|
||||||
type: 'array',
|
|
||||||
title: `{{t("Assignees", { 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': 'AssigneesSelect' },
|
|
||||||
remove: { type: 'void', 'x-decorator': 'FormItem', 'x-component': 'ArrayItems.Remove' },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
required: !0,
|
|
||||||
properties: {
|
|
||||||
add: {
|
|
||||||
type: 'void',
|
|
||||||
title: `{{t("Add assignee", { ns: "${NAMESPACE}" })}}`,
|
|
||||||
'x-component': 'AssigneesAddition',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
negotiation: {
|
|
||||||
type: 'number',
|
|
||||||
title: `{{t("Negotiation mode", { ns: "${NAMESPACE}" })}}`,
|
|
||||||
'x-decorator': 'FormItem',
|
|
||||||
'x-component': 'NegotiationConfig',
|
|
||||||
default: 0,
|
|
||||||
},
|
|
||||||
order: {
|
|
||||||
type: 'boolean',
|
|
||||||
title: `{{t("Order", { ns: "${NAMESPACE}" })}}`,
|
|
||||||
'x-decorator': 'FormItem',
|
|
||||||
'x-component': 'RadioWithTooltip',
|
|
||||||
'x-component-props': {
|
|
||||||
options: [
|
|
||||||
{
|
|
||||||
label: `{{t("Parallelly", { ns: "${NAMESPACE}" })}}`,
|
|
||||||
value: !1,
|
|
||||||
tooltip: `{{t("Multiple approvers can approve in any order.", { ns: "${NAMESPACE}" })}}`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: `{{t("Sequentially", { ns: "${NAMESPACE}" })}}`,
|
|
||||||
value: !0,
|
|
||||||
tooltip: `{{t("Multiple approvers in sequential order.", { ns: "${NAMESPACE}" })}}`,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
default: !1,
|
|
||||||
},
|
|
||||||
endOnReject: {
|
|
||||||
type: 'boolean',
|
|
||||||
'x-decorator': 'FormItem',
|
|
||||||
'x-component': 'Checkbox',
|
|
||||||
'x-content': `{{t("End the workflow after rejection branch", { ns: "${NAMESPACE}" })}}`,
|
|
||||||
description: `{{t("When checked, the workflow will terminate when the rejection branch ends.", { ns: "${NAMESPACE}" })}}`,
|
|
||||||
'x-reactions': [{ dependencies: ['.branchMode'], fulfill: { state: { visible: '{{$deps[0]}}' } } }],
|
|
||||||
},
|
|
||||||
applyDetail: {
|
|
||||||
type: 'void',
|
|
||||||
title: `{{t("Approver's interface", { ns: "${NAMESPACE}" })}}`,
|
|
||||||
'x-decorator': 'FormItem',
|
|
||||||
'x-component': 'SchemaConfigButton',
|
|
||||||
properties: { applyDetail: { type: 'void', 'x-component': 'SchemaConfig' } },
|
|
||||||
required: !0,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
T(this, 'options', [
|
|
||||||
{
|
|
||||||
label: `{{t('Passthrough mode', { ns: "${NAMESPACE}" })}}`,
|
|
||||||
key: 'false',
|
|
||||||
value() {
|
|
||||||
return { branchMode: !1, applyDetail: uid() };
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: `{{t('Branch mode', { ns: "${NAMESPACE}" })}}`,
|
|
||||||
key: 'true',
|
|
||||||
value() {
|
|
||||||
return { branchMode: !0, applyDetail: uid() };
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
T(this, 'Component', ot);
|
|
||||||
T(this, 'components', {
|
|
||||||
ArrayItems: ArrayItems,
|
|
||||||
SchemaConfigButton: et,
|
|
||||||
SchemaConfig: Ro,
|
|
||||||
AssigneesSelect: jo,
|
|
||||||
NegotiationConfig: it,
|
|
||||||
RadioWithTooltip: RadioWithTooltip,
|
|
||||||
AssigneesAddition: st,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
isAvailable({ workflow: n, upstream: a, branchIndex: s }) {
|
|
||||||
return !(
|
|
||||||
n.type !== 'approval' || at(a, s, (i, v) => (i == null ? void 0 : i.type) === 'approval' && v === b.RETURNED)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,33 +0,0 @@
|
|||||||
import { SchemaInitializer } from '@nocobase/client';
|
|
||||||
import { NAMESPACE } from '../locale';
|
|
||||||
import { b, ie } from './refined';
|
|
||||||
|
|
||||||
export const ApprovalProcessAddActionButton = new SchemaInitializer({
|
|
||||||
name: 'ApprovalProcessAddActionButton',
|
|
||||||
title: '{{t("Configure actions")}}',
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
name: `action-${b.APPROVED}`,
|
|
||||||
type: 'item',
|
|
||||||
title: `{{t("Approve", { ns: "${NAMESPACE}" })}}`,
|
|
||||||
Component: ie,
|
|
||||||
action: b.APPROVED,
|
|
||||||
actionProps: { type: 'primary' },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: `action-${b.REJECTED}`,
|
|
||||||
type: 'item',
|
|
||||||
title: `{{t("Reject", { ns: "${NAMESPACE}" })}}`,
|
|
||||||
Component: ie,
|
|
||||||
action: b.REJECTED,
|
|
||||||
actionProps: { danger: !0 },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: `action-${b.RETURNED}`,
|
|
||||||
type: 'item',
|
|
||||||
title: `{{t("Return", { ns: "${NAMESPACE}" })}}`,
|
|
||||||
Component: ie,
|
|
||||||
action: b.RETURNED,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
@ -1,53 +0,0 @@
|
|||||||
import { SchemaInitializer, gridRowColWrap, usePlugin } from '@nocobase/client';
|
|
||||||
import PluginWorkflow, {
|
|
||||||
useAvailableUpstreams,
|
|
||||||
useFlowContext,
|
|
||||||
useNodeContext, useTrigger
|
|
||||||
} from '@nocobase/plugin-workflow/client';
|
|
||||||
import { NAMESPACE } from '../locale';
|
|
||||||
import { Lo, Jo } from './refined';
|
|
||||||
|
|
||||||
export const ApprovalProcessAddBlockButton = new SchemaInitializer({
|
|
||||||
name: 'ApprovalProcessAddBlockButton',
|
|
||||||
wrap: gridRowColWrap,
|
|
||||||
title: "{{t('Add block')}}",
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
name: 'approval',
|
|
||||||
type: 'itemGroup',
|
|
||||||
title: `{{t("Approval blocks", { ns: "${NAMESPACE}" })}}`,
|
|
||||||
children: [
|
|
||||||
{ name: 'detail', type: 'item', title: '{{t("Details")}}', Component: Lo },
|
|
||||||
{ name: 'actions', type: 'item', title: '{{t("Actions")}}', Component: Jo },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'itemGroup',
|
|
||||||
name: 'dataBlocks',
|
|
||||||
title: '{{t("Data blocks")}}',
|
|
||||||
checkChildrenLength: !0,
|
|
||||||
useChildren() {
|
|
||||||
var l;
|
|
||||||
const e = usePlugin(PluginWorkflow), { workflow: t } = useFlowContext(), n = useTrigger(), a = useNodeContext(), s = useAvailableUpstreams(a), i = [(l = n.useInitializers) == null ? void 0 : l.call(n, t.config)].filter(Boolean), v = s
|
|
||||||
.map((d) => {
|
|
||||||
var h;
|
|
||||||
const m = e.instructions.get(d.type);
|
|
||||||
return (h = m == null ? void 0 : m.useInitializers) == null ? void 0 : h.call(m, d);
|
|
||||||
})
|
|
||||||
.filter(Boolean);
|
|
||||||
return [
|
|
||||||
...i,
|
|
||||||
...(v.length
|
|
||||||
? [{ name: 'nodes', type: 'subMenu', title: '{{t("Node result", { ns: "workflow" })}}', children: v }]
|
|
||||||
: []),
|
|
||||||
].filter(Boolean);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'others',
|
|
||||||
type: 'itemGroup',
|
|
||||||
title: '{{t("Other blocks")}}',
|
|
||||||
children: [{ name: 'markdown', type: 'item', title: '{{t("Markdown")}}', component: 'MarkdownBlockInitializer' }],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
@ -0,0 +1,94 @@
|
|||||||
|
import { approvalStatusOptions } from '../constants';
|
||||||
|
import { NAMESPACE } from '../locale';
|
||||||
|
|
||||||
|
export const CollectionApprovalTodos = {
|
||||||
|
title: `{{t("Approval todos", { ns: "${NAMESPACE}" })}}`,
|
||||||
|
name: 'approvalRecords',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
type: 'bigInt',
|
||||||
|
name: 'approvalId',
|
||||||
|
interface: 'number',
|
||||||
|
uiSchema: { type: 'number', title: 'ID', 'x-component': 'InputNumber' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'belongsTo',
|
||||||
|
name: 'user',
|
||||||
|
target: 'users',
|
||||||
|
foreignKey: 'userId',
|
||||||
|
interface: 'm2o',
|
||||||
|
uiSchema: {
|
||||||
|
type: 'number',
|
||||||
|
title: `{{t("Assignee", { ns: "${NAMESPACE}" })}}`,
|
||||||
|
'x-component': 'RemoteSelect',
|
||||||
|
'x-component-props': { fieldNames: { label: 'nickname', value: 'id' }, service: { resource: 'users' } },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'belongsTo',
|
||||||
|
name: 'node',
|
||||||
|
target: 'flow_nodes',
|
||||||
|
foreignKey: 'nodeId',
|
||||||
|
interface: 'm2o',
|
||||||
|
isAssociation: true,
|
||||||
|
uiSchema: {
|
||||||
|
type: 'number',
|
||||||
|
title: `{{t("Task node", { 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: "workflow" })}}',
|
||||||
|
'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: "workflow" })}}',
|
||||||
|
'x-component': 'Select',
|
||||||
|
enum: approvalStatusOptions,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
name: 'comment',
|
||||||
|
interface: 'markdown',
|
||||||
|
uiSchema: { type: 'string', 'x-component': 'Markdown', title: `{{t("Comment", { ns: "${NAMESPACE}" })}}` },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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("Updated at")}}',
|
||||||
|
'x-component': 'DatePicker',
|
||||||
|
'x-component-props': { showTime: true },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
@ -0,0 +1,76 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import {
|
||||||
|
BlockRequestContext_deprecated,
|
||||||
|
CollectionProvider_deprecated,
|
||||||
|
FormActiveFieldsProvider,
|
||||||
|
FormBlockContext,
|
||||||
|
FormProvider,
|
||||||
|
FormV2,
|
||||||
|
RecordProvider,
|
||||||
|
useAPIClient,
|
||||||
|
useAssociationNames,
|
||||||
|
useDesignable,
|
||||||
|
} from '@nocobase/client';
|
||||||
|
import { RecursionField, createForm, useField, useFieldSchema } from '@nocobase/schema';
|
||||||
|
import { Fragment, useContext, useMemo, useRef } from 'react';
|
||||||
|
import { useContextApprovalExecution } from '../usage/approval-block/common/Pd.ApprovalExecution';
|
||||||
|
|
||||||
|
export const FormBlockProvider = (props) => {
|
||||||
|
const context = useContextApprovalExecution();
|
||||||
|
const fieldSchema = useFieldSchema();
|
||||||
|
const field = useField();
|
||||||
|
const formBlockRef = useRef(null);
|
||||||
|
const { getAssociationAppends } = useAssociationNames();
|
||||||
|
const { appends, updateAssociationValues } = getAssociationAppends();
|
||||||
|
// @ts-ignore
|
||||||
|
const snapshot = context?.snapshot;
|
||||||
|
const { findComponent } = useDesignable();
|
||||||
|
const ContainerFormComp = findComponent(field.component?.[0]) || Fragment;
|
||||||
|
const form = useMemo(() => createForm({ initialValues: snapshot }), [snapshot]);
|
||||||
|
const params = useMemo(() => ({ ...appends, ...props.params }), [appends, props.params]);
|
||||||
|
const service = useMemo(() => ({ loading: false, data: { data: snapshot } }), [snapshot]);
|
||||||
|
const collectionResource = useAPIClient().resource(props.collection);
|
||||||
|
const blockContext = useContext(BlockRequestContext_deprecated);
|
||||||
|
const formValue = useMemo(
|
||||||
|
() => ({
|
||||||
|
params,
|
||||||
|
form,
|
||||||
|
field,
|
||||||
|
service,
|
||||||
|
updateAssociationValues,
|
||||||
|
formBlockRef,
|
||||||
|
}),
|
||||||
|
[field, form, params, service, updateAssociationValues],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CollectionProvider_deprecated dataSource={props.dataSource} collection={props.collection}>
|
||||||
|
{/* @ts-ignore */}
|
||||||
|
<RecordProvider record={snapshot}>
|
||||||
|
<FormActiveFieldsProvider name="form">
|
||||||
|
<BlockRequestContext_deprecated.Provider
|
||||||
|
value={{
|
||||||
|
block: 'form',
|
||||||
|
props: props,
|
||||||
|
field: field,
|
||||||
|
service: service,
|
||||||
|
resource: collectionResource,
|
||||||
|
__parent: blockContext,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FormBlockContext.Provider value={formValue}>
|
||||||
|
<ContainerFormComp {...field.componentProps}>
|
||||||
|
<FormV2.Templates style={{ marginBottom: 18 }} form={form} />
|
||||||
|
<FormProvider form={form}>
|
||||||
|
<div ref={formBlockRef}>
|
||||||
|
<RecursionField schema={fieldSchema} onlyRenderProperties />
|
||||||
|
</div>
|
||||||
|
</FormProvider>
|
||||||
|
</ContainerFormComp>
|
||||||
|
</FormBlockContext.Provider>
|
||||||
|
</BlockRequestContext_deprecated.Provider>
|
||||||
|
</FormActiveFieldsProvider>
|
||||||
|
</RecordProvider>
|
||||||
|
</CollectionProvider_deprecated>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,12 @@
|
|||||||
|
import { Plugin } from '@nocobase/client';
|
||||||
|
import PluginKitApprovalInstruction from './instruction';
|
||||||
|
import PluginKitApprovalTrigger from './trigger';
|
||||||
|
|
||||||
|
export default class PluginKitApprovalConfiguration extends Plugin {
|
||||||
|
async afterAdd() {
|
||||||
|
this.pm.add(PluginKitApprovalTrigger);
|
||||||
|
this.pm.add(PluginKitApprovalInstruction);
|
||||||
|
}
|
||||||
|
async beforeLoad() {}
|
||||||
|
async load() {}
|
||||||
|
}
|
@ -0,0 +1,3 @@
|
|||||||
|
import { createContext } from 'react';
|
||||||
|
|
||||||
|
export const ContextApproverConfig = createContext({}) as any;
|
@ -0,0 +1,60 @@
|
|||||||
|
import { css, useCompile } from '@nocobase/client';
|
||||||
|
import { Branch, NodeDefaultView, useFlowContext, useStyles } from '@nocobase/plugin-workflow/client';
|
||||||
|
import { Tag } from 'antd';
|
||||||
|
import React from 'react';
|
||||||
|
import { APPROVAL_ACTION_STATUS, approvalStatusConfigObj } from '../../constants';
|
||||||
|
|
||||||
|
// 审批节点组件
|
||||||
|
export const ApprovalInstructionNode = ({ data }) => {
|
||||||
|
const { styles } = useStyles();
|
||||||
|
const { config } = data;
|
||||||
|
return (
|
||||||
|
<NodeDefaultView data={data}>
|
||||||
|
{config.branchMode ? (
|
||||||
|
<div className={styles.nodeSubtreeClass}>
|
||||||
|
<div className={styles.branchBlockClass}>
|
||||||
|
<BranchListComp data={data} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</NodeDefaultView>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Child Component; 审批节点分支
|
||||||
|
const BranchListComp = (props) => {
|
||||||
|
const { data } = props;
|
||||||
|
const compile = useCompile();
|
||||||
|
const { nodes } = useFlowContext();
|
||||||
|
|
||||||
|
const { id, config } = data;
|
||||||
|
const isAllowReturned = config?.actions?.includes(APPROVAL_ACTION_STATUS.RETURNED);
|
||||||
|
const baseStatusArr = [APPROVAL_ACTION_STATUS.REJECTED, APPROVAL_ACTION_STATUS.APPROVED];
|
||||||
|
const statusArr = isAllowReturned ? [...baseStatusArr, APPROVAL_ACTION_STATUS.RETURNED] : baseStatusArr;
|
||||||
|
|
||||||
|
const isEntry = (targetStatus) => nodes.find((node) => node.upstreamId === id && node.branchIndex === targetStatus);
|
||||||
|
const isEnd = (targetStatus) =>
|
||||||
|
targetStatus === APPROVAL_ACTION_STATUS.RETURNED ||
|
||||||
|
(targetStatus === APPROVAL_ACTION_STATUS.REJECTED && config.endOnReject);
|
||||||
|
|
||||||
|
return statusArr.map((targetStatus) => (
|
||||||
|
<Branch
|
||||||
|
key={targetStatus}
|
||||||
|
branchIndex={targetStatus}
|
||||||
|
from={data}
|
||||||
|
entry={isEntry(targetStatus)}
|
||||||
|
end={isEnd(targetStatus)}
|
||||||
|
controller={
|
||||||
|
<Tag
|
||||||
|
color={approvalStatusConfigObj[targetStatus].color}
|
||||||
|
className={css`
|
||||||
|
position: relative;
|
||||||
|
margin: 1rem 0 0 0;
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
{compile(approvalStatusConfigObj[targetStatus].label)}
|
||||||
|
</Tag>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
};
|
@ -0,0 +1,58 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { PlusOutlined } from '@ant-design/icons';
|
||||||
|
import { ArrayItems } from '@formily/antd-v5';
|
||||||
|
import { useWorkflowExecuted } from '@nocobase/plugin-workflow/client';
|
||||||
|
import { Button, Popover, Space } from 'antd';
|
||||||
|
import { useCallback, useState } from 'react';
|
||||||
|
import { useTranslation } from '../../../locale';
|
||||||
|
|
||||||
|
// 添加审批人(选择/查询)
|
||||||
|
export const AssigneesAddition = () => {
|
||||||
|
const isWorkflowExecuted = useWorkflowExecuted();
|
||||||
|
|
||||||
|
const array = ArrayItems.useArray();
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const onSlecet = useCallback(() => {
|
||||||
|
array.field.push('');
|
||||||
|
setIsOpen(false);
|
||||||
|
}, [array.field]);
|
||||||
|
const onQuery = useCallback(() => {
|
||||||
|
array.field.push({ filter: {} });
|
||||||
|
setIsOpen(false);
|
||||||
|
}, [array.field]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Popover
|
||||||
|
open={!isWorkflowExecuted && isOpen}
|
||||||
|
onOpenChange={setIsOpen}
|
||||||
|
content={<PopoverContent onSlecet={onSlecet} onQuery={onQuery} />}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
className="ant-formily-array-base-addition"
|
||||||
|
icon={<PlusOutlined />}
|
||||||
|
type="dashed"
|
||||||
|
block={true}
|
||||||
|
disabled={isWorkflowExecuted}
|
||||||
|
>
|
||||||
|
{t('Add assignee')}
|
||||||
|
</Button>
|
||||||
|
</Popover>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Child Component
|
||||||
|
const PopoverContent = ({ onSlecet, onQuery }) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
return (
|
||||||
|
<Space direction="vertical" size="small">
|
||||||
|
<Button type="text" onClick={onSlecet}>
|
||||||
|
{t('Select assignees')}
|
||||||
|
</Button>
|
||||||
|
<Button type="text" onClick={onQuery}>
|
||||||
|
{t('Query assignees')}
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,62 @@
|
|||||||
|
import { RemoteSelect, SchemaComponent, Variable, useCollectionFilterOptions, useToken } from '@nocobase/client';
|
||||||
|
import { FilterDynamicComponent, useWorkflowVariableOptions } from '@nocobase/plugin-workflow/client';
|
||||||
|
import { useField } from '@nocobase/schema';
|
||||||
|
import { default as React } from 'react';
|
||||||
|
|
||||||
|
// 添加审批人-选择器
|
||||||
|
export const AssigneesSelect = (props) => {
|
||||||
|
if (typeof props.value === 'object' && props.value) {
|
||||||
|
return <AssigneesSelectCustom {...props} />;
|
||||||
|
} else {
|
||||||
|
return <AssigneesSelectNormal {...props} />;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function isUserKeyField(field) {
|
||||||
|
if (field.isForeignKey) {
|
||||||
|
return field.target === 'users';
|
||||||
|
} else {
|
||||||
|
return field.collectionName === 'users' && field.name === 'id';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const AssigneesSelectNormal = ({ value, onChange }) => {
|
||||||
|
const scope = useWorkflowVariableOptions({ types: [isUserKeyField] });
|
||||||
|
return (
|
||||||
|
<Variable.Input scope={scope} value={value} onChange={onChange}>
|
||||||
|
<RemoteSelect
|
||||||
|
fieldNames={{ label: 'nickname', value: 'id' }}
|
||||||
|
service={{ resource: 'users' }}
|
||||||
|
manual={false}
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
/>
|
||||||
|
</Variable.Input>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const AssigneesSelectCustom = () => {
|
||||||
|
const field = useField();
|
||||||
|
const currentFormFields = useCollectionFilterOptions('users');
|
||||||
|
const { token } = useToken();
|
||||||
|
return (
|
||||||
|
<div style={{ border: `1px dashed ${token.colorBorder}`, padding: token.paddingSM }}>
|
||||||
|
<SchemaComponent
|
||||||
|
basePath={field.address}
|
||||||
|
schema={{
|
||||||
|
type: 'void',
|
||||||
|
properties: {
|
||||||
|
filter: {
|
||||||
|
type: 'object',
|
||||||
|
'x-component': 'Filter',
|
||||||
|
'x-component-props': {
|
||||||
|
options: currentFormFields,
|
||||||
|
dynamicComponent: FilterDynamicComponent,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,22 @@
|
|||||||
|
import { QuestionCircleOutlined } from '@ant-design/icons';
|
||||||
|
import { css, useCompile } from '@nocobase/client';
|
||||||
|
import { Tooltip } from 'antd';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
export const ContentTooltip = ({ content, tooltip }) => {
|
||||||
|
const compile = useCompile();
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<span
|
||||||
|
className={css`
|
||||||
|
& + .anticon {
|
||||||
|
margin-left: 0.25em;
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
{compile(content)}
|
||||||
|
</span>
|
||||||
|
{tooltip && <Tooltip title={compile(tooltip)}>{<QuestionCircleOutlined style={{ color: '#666' }} />}</Tooltip>}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,76 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
import React from 'react';
|
||||||
|
import { QuestionCircleOutlined } from '@ant-design/icons';
|
||||||
|
import { InputNumber, css } from '@nocobase/client';
|
||||||
|
import { Radio, Tooltip } from 'antd';
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
import { useTranslation } from '../../../locale';
|
||||||
|
|
||||||
|
// 协商模式
|
||||||
|
export const NegotiationConfig = ({ value, onChange }) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const percentSign = value > 0 && value < 1 ? '%' : value;
|
||||||
|
const onChangeRadio = useCallback(
|
||||||
|
({ target }) => {
|
||||||
|
if (target.value !== percentSign) {
|
||||||
|
onChange(target.value === '%' ? 0.5 : target.value);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[percentSign, onChange],
|
||||||
|
);
|
||||||
|
const onChangeInput = useCallback(
|
||||||
|
(val) => {
|
||||||
|
onChange(val / 100);
|
||||||
|
},
|
||||||
|
[onChange],
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<fieldset
|
||||||
|
className={css`
|
||||||
|
.ant-radio-group {
|
||||||
|
.anticon {
|
||||||
|
margin-left: 0.5em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
<Radio.Group key="radio" value={percentSign} onChange={onChangeRadio}>
|
||||||
|
<Radio key="0" value="0">
|
||||||
|
<Tooltip title={t('The approval or rejection by anyone of them is the result.')} placement="bottom">
|
||||||
|
<span>{t('Or')}</span>
|
||||||
|
<QuestionCircleOutlined style={{ color: '#999' }} />
|
||||||
|
</Tooltip>
|
||||||
|
</Radio>
|
||||||
|
<Radio key="1" value="1">
|
||||||
|
<Tooltip
|
||||||
|
title={t("If it's approved by all, it's approved. If it's rejected by anyone, it's rejected.")}
|
||||||
|
placement="bottom"
|
||||||
|
>
|
||||||
|
<span>{t('And')}</span>
|
||||||
|
<QuestionCircleOutlined style={{ color: '#999' }} />
|
||||||
|
</Tooltip>
|
||||||
|
</Radio>
|
||||||
|
<Radio key="%" value="%">
|
||||||
|
<Tooltip
|
||||||
|
title={t('Approved if the approval rate is greater than the set percentage, otherwise rejected.')}
|
||||||
|
placement="bottom"
|
||||||
|
>
|
||||||
|
<span>{t('Voting')}</span>
|
||||||
|
<QuestionCircleOutlined style={{ color: '#999' }} />
|
||||||
|
</Tooltip>
|
||||||
|
</Radio>
|
||||||
|
</Radio.Group>
|
||||||
|
{percentSign === '%' ? (
|
||||||
|
<InputNumber
|
||||||
|
addonBefore=">"
|
||||||
|
min="1"
|
||||||
|
max="99"
|
||||||
|
defaultValue="50"
|
||||||
|
value={value * 100}
|
||||||
|
onChange={onChangeInput}
|
||||||
|
addonAfter="%"
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</fieldset>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,14 @@
|
|||||||
|
import { useActionContext } from '@nocobase/client';
|
||||||
|
import React from 'react';
|
||||||
|
import { SchemaConfigButton } from '../../trigger/VC.ConfigButton';
|
||||||
|
import { ContextApproverConfig } from '../Pd.ContextApproverConfig';
|
||||||
|
|
||||||
|
// 审批人操作界面->进入配置按钮
|
||||||
|
export const SchemaConfigButtonApprover = (props) => {
|
||||||
|
const { setFormValueChanged } = useActionContext();
|
||||||
|
return (
|
||||||
|
<ContextApproverConfig.Provider value={{ setFormValueChanged }}>
|
||||||
|
<SchemaConfigButton {...props} />
|
||||||
|
</ContextApproverConfig.Provider>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,34 @@
|
|||||||
|
import { SchemaInitializer } from '@nocobase/client';
|
||||||
|
import { NAMESPACE } from '../../../locale';
|
||||||
|
import { APPROVAL_ACTION_STATUS } from '../../../constants';
|
||||||
|
import { ApprovalActionConfigComponent } from './VC.ApprovalActionConfig';
|
||||||
|
|
||||||
|
export const ApproverActionConfigInitializer = new SchemaInitializer({
|
||||||
|
name: 'ApprovalProcessAddActionButton',
|
||||||
|
title: '{{t("Configure actions")}}',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
name: `action-${APPROVAL_ACTION_STATUS.APPROVED}`,
|
||||||
|
type: 'item',
|
||||||
|
title: `{{t("Approve", { ns: "${NAMESPACE}" })}}`,
|
||||||
|
Component: ApprovalActionConfigComponent,
|
||||||
|
action: APPROVAL_ACTION_STATUS.APPROVED,
|
||||||
|
actionProps: { type: 'primary' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: `action-${APPROVAL_ACTION_STATUS.REJECTED}`,
|
||||||
|
type: 'item',
|
||||||
|
title: `{{t("Reject", { ns: "${NAMESPACE}" })}}`,
|
||||||
|
Component: ApprovalActionConfigComponent,
|
||||||
|
action: APPROVAL_ACTION_STATUS.REJECTED,
|
||||||
|
actionProps: { danger: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: `action-${APPROVAL_ACTION_STATUS.RETURNED}`,
|
||||||
|
type: 'item',
|
||||||
|
title: `{{t("Return", { ns: "${NAMESPACE}" })}}`,
|
||||||
|
Component: ApprovalActionConfigComponent,
|
||||||
|
action: APPROVAL_ACTION_STATUS.RETURNED,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
@ -0,0 +1,83 @@
|
|||||||
|
import { SchemaInitializer, gridRowColWrap, usePlugin } from '@nocobase/client';
|
||||||
|
import PluginWorkflow, {
|
||||||
|
useAvailableUpstreams,
|
||||||
|
useFlowContext,
|
||||||
|
useNodeContext,
|
||||||
|
useTrigger,
|
||||||
|
} from '@nocobase/plugin-workflow/client';
|
||||||
|
import { NAMESPACE } from '../../../locale';
|
||||||
|
import { ApproverAddBlockComponent } from './VC.ApproverAddBlock';
|
||||||
|
import { ApproverAddBlockKit } from './VC.ApproverAddBlockKit';
|
||||||
|
|
||||||
|
export const ApproverAddBlockInitializer = new SchemaInitializer({
|
||||||
|
name: 'ApprovalProcessAddBlockButton',
|
||||||
|
wrap: gridRowColWrap,
|
||||||
|
title: "{{t('Add block')}}",
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
name: 'approval',
|
||||||
|
type: 'itemGroup',
|
||||||
|
title: `{{t("Approval blocks", { ns: "${NAMESPACE}" })}}`,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
name: 'detail',
|
||||||
|
type: 'item',
|
||||||
|
title: '{{t("Details")}}',
|
||||||
|
Component: ApproverAddBlockComponent,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'actions',
|
||||||
|
type: 'item',
|
||||||
|
title: '{{t("Actions")}}',
|
||||||
|
Component: ApproverAddBlockKit,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'itemGroup',
|
||||||
|
name: 'dataBlocks',
|
||||||
|
title: '{{t("Data blocks")}}',
|
||||||
|
checkChildrenLength: true,
|
||||||
|
useChildren() {
|
||||||
|
const workflowPlugin = usePlugin(PluginWorkflow);
|
||||||
|
const { workflow } = useFlowContext();
|
||||||
|
const trigger = useTrigger();
|
||||||
|
const currentNodeContext = useNodeContext();
|
||||||
|
const nodes = useAvailableUpstreams(currentNodeContext);
|
||||||
|
const triggerInitializers = [trigger.useInitializers?.call(trigger, workflow.config)].filter(Boolean);
|
||||||
|
const nodeBlockInitializers = nodes
|
||||||
|
.map((node) => {
|
||||||
|
const instruction = workflowPlugin.instructions.get(node.type);
|
||||||
|
return instruction?.useInitializers?.call(instruction, node);
|
||||||
|
})
|
||||||
|
.filter(Boolean);
|
||||||
|
return [
|
||||||
|
...triggerInitializers,
|
||||||
|
...(nodeBlockInitializers.length
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
name: 'nodes',
|
||||||
|
type: 'subMenu',
|
||||||
|
title: '{{t("Node result", { ns: "workflow" })}}',
|
||||||
|
children: nodeBlockInitializers,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
].filter(Boolean);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'others',
|
||||||
|
type: 'itemGroup',
|
||||||
|
title: '{{t("Other blocks")}}',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
name: 'markdown',
|
||||||
|
type: 'item',
|
||||||
|
title: '{{t("Markdown")}}',
|
||||||
|
component: 'MarkdownBlockInitializer',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
@ -0,0 +1,3 @@
|
|||||||
|
export const ActionBarProvider = (props) => {
|
||||||
|
return props.children;
|
||||||
|
};
|
@ -0,0 +1,3 @@
|
|||||||
|
export const ApprovalActionProvider = (props) => {
|
||||||
|
return props.children;
|
||||||
|
};
|
@ -0,0 +1,3 @@
|
|||||||
|
import { createContext } from 'react';
|
||||||
|
|
||||||
|
export const ContextApproverBlock = createContext({});
|
@ -0,0 +1,28 @@
|
|||||||
|
import { ActionInitializer, useSchemaInitializerItem } from '@nocobase/client';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
export const ApprovalActionConfigComponent = () => {
|
||||||
|
const itemConfig = useSchemaInitializerItem();
|
||||||
|
const { action, actionProps = {}, ...restItemConfig } = itemConfig;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ActionInitializer
|
||||||
|
{...restItemConfig}
|
||||||
|
schema={{
|
||||||
|
type: 'void',
|
||||||
|
title: restItemConfig.title,
|
||||||
|
'x-decorator': 'ApprovalActionProvider',
|
||||||
|
'x-decorator-props': {
|
||||||
|
status: action,
|
||||||
|
},
|
||||||
|
'x-component': 'Action',
|
||||||
|
'x-component-props': {
|
||||||
|
...actionProps,
|
||||||
|
useAction: '{{ useSubmit }}',
|
||||||
|
},
|
||||||
|
'x-designer': 'Action.Designer',
|
||||||
|
'x-action': `${action}`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,67 @@
|
|||||||
|
import {
|
||||||
|
BlockRequestContext_deprecated,
|
||||||
|
CollectionProvider_deprecated,
|
||||||
|
FormActiveFieldsProvider,
|
||||||
|
FormBlockContext,
|
||||||
|
FormV2,
|
||||||
|
RecordProvider,
|
||||||
|
useAPIClient,
|
||||||
|
useAssociationNames,
|
||||||
|
useDesignable,
|
||||||
|
} from '@nocobase/client';
|
||||||
|
import { RecursionField, createForm, useField, useFieldSchema } from '@nocobase/schema';
|
||||||
|
import React, { Fragment, useContext, useMemo, useRef } from 'react';
|
||||||
|
|
||||||
|
export const ApprovalFormBlockProvider = (props) => {
|
||||||
|
const fieldSchema = useFieldSchema();
|
||||||
|
const field = useField();
|
||||||
|
const formBlockRef = useRef(null);
|
||||||
|
const { getAssociationAppends } = useAssociationNames();
|
||||||
|
const { appends, updateAssociationValues } = getAssociationAppends();
|
||||||
|
const { findComponent } = useDesignable();
|
||||||
|
const ContainerFormComp = findComponent(field.component?.[0]) || Fragment;
|
||||||
|
const form = useMemo(() => createForm({}), []);
|
||||||
|
const params = useMemo(() => ({ ...appends, ...props.params }), [appends, props.params]);
|
||||||
|
const service = useMemo(() => ({ loading: false, data: { data: {} } }), []);
|
||||||
|
const collectionResource = useAPIClient().resource(props.collection);
|
||||||
|
const blockContext = useContext(BlockRequestContext_deprecated);
|
||||||
|
const formValue = useMemo(
|
||||||
|
() => ({
|
||||||
|
params,
|
||||||
|
form,
|
||||||
|
field,
|
||||||
|
service,
|
||||||
|
updateAssociationValues,
|
||||||
|
formBlockRef,
|
||||||
|
}),
|
||||||
|
[field, form, params, service, updateAssociationValues],
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<CollectionProvider_deprecated collection={props.collection}>
|
||||||
|
{/* @ts-ignore */}
|
||||||
|
<RecordProvider record={{}}>
|
||||||
|
<FormActiveFieldsProvider name="form">
|
||||||
|
<BlockRequestContext_deprecated.Provider
|
||||||
|
value={{
|
||||||
|
block: 'form',
|
||||||
|
props: props,
|
||||||
|
field: field,
|
||||||
|
service: service,
|
||||||
|
resource: collectionResource,
|
||||||
|
__parent: blockContext,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FormBlockContext.Provider value={formValue}>
|
||||||
|
<ContainerFormComp {...field.componentProps}>
|
||||||
|
<FormV2.Templates style={{ marginBottom: 18 }} form={form} />
|
||||||
|
<div ref={formBlockRef}>
|
||||||
|
<RecursionField schema={fieldSchema} onlyRenderProperties={true} />
|
||||||
|
</div>
|
||||||
|
</ContainerFormComp>
|
||||||
|
</FormBlockContext.Provider>
|
||||||
|
</BlockRequestContext_deprecated.Provider>
|
||||||
|
</FormActiveFieldsProvider>
|
||||||
|
</RecordProvider>
|
||||||
|
</CollectionProvider_deprecated>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,43 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import {
|
||||||
|
SchemaInitializerItem,
|
||||||
|
createReadPrettyFormBlockSchema,
|
||||||
|
parseCollectionName,
|
||||||
|
useRecordCollectionDataSourceItems,
|
||||||
|
useSchemaInitializer,
|
||||||
|
useSchemaInitializerItem,
|
||||||
|
useSchemaTemplateManager,
|
||||||
|
} from '@nocobase/client';
|
||||||
|
import { useFlowContext } from '@nocobase/plugin-workflow/client';
|
||||||
|
import _ from 'lodash';
|
||||||
|
|
||||||
|
export const ApproverAddBlockComponent = () => {
|
||||||
|
const { workflow } = useFlowContext();
|
||||||
|
const [dataSourceName, collection] = parseCollectionName(workflow.config.collection);
|
||||||
|
const itemConfig = useSchemaInitializerItem();
|
||||||
|
const { insert } = useSchemaInitializer();
|
||||||
|
const { getTemplateSchemaByMode } = useSchemaTemplateManager();
|
||||||
|
const items = useRecordCollectionDataSourceItems('FormItem');
|
||||||
|
|
||||||
|
const onClick = async ({ item }) => {
|
||||||
|
const template = item.template ? await getTemplateSchemaByMode(item) : null;
|
||||||
|
const targetSchema = createReadPrettyFormBlockSchema({
|
||||||
|
actionInitializers: null,
|
||||||
|
resource: collection,
|
||||||
|
collection,
|
||||||
|
dataSource: dataSourceName,
|
||||||
|
template,
|
||||||
|
settings: 'blockSettings:singleDataDetails',
|
||||||
|
});
|
||||||
|
|
||||||
|
delete targetSchema['x-acl-action-props'];
|
||||||
|
delete targetSchema['x-acl-action'];
|
||||||
|
|
||||||
|
const [firstPropertyKey] = Object.keys(targetSchema.properties);
|
||||||
|
|
||||||
|
_.set(targetSchema.properties[firstPropertyKey], 'x-component-props.useProps', '{{useApprovalDetailBlockProps}}');
|
||||||
|
|
||||||
|
insert(targetSchema);
|
||||||
|
};
|
||||||
|
return <SchemaInitializerItem {...itemConfig} onClick={onClick} items={items} />;
|
||||||
|
};
|
@ -0,0 +1,52 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { InitializerWithSwitch, createFormBlockSchema, useSchemaInitializerItem } from '@nocobase/client';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import { NAMESPACE } from '../../../locale';
|
||||||
|
import { APPROVAL_ACTION_STATUS } from '../../../constants';
|
||||||
|
import { flatSchemaArray } from '../../../constants';
|
||||||
|
|
||||||
|
// 创建区块-操作switch
|
||||||
|
export const ApproverAddBlockKit = () => {
|
||||||
|
const itemConfig = useSchemaInitializerItem();
|
||||||
|
const formSchema = createFormBlockSchema({
|
||||||
|
actionInitializers: 'ApprovalProcessAddActionButton',
|
||||||
|
actions: {
|
||||||
|
approve: {
|
||||||
|
type: 'void',
|
||||||
|
title: `{{t("Approve", { ns: "${NAMESPACE}" })}}`,
|
||||||
|
'x-decorator': 'ApprovalActionProvider',
|
||||||
|
'x-decorator-props': { status: APPROVAL_ACTION_STATUS.APPROVED },
|
||||||
|
'x-component': 'Action',
|
||||||
|
'x-component-props': { type: 'primary', htmlType: 'submit', useAction: '{{ useSubmit }}' },
|
||||||
|
'x-designer': 'Action.Designer',
|
||||||
|
'x-designer-props': {},
|
||||||
|
'x-action': `${APPROVAL_ACTION_STATUS.APPROVED}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
resource: 'approvalRecords',
|
||||||
|
collection: 'approvalRecords',
|
||||||
|
});
|
||||||
|
|
||||||
|
delete formSchema['x-acl-action-props'];
|
||||||
|
delete formSchema['x-acl-action'];
|
||||||
|
formSchema['x-decorator'] = 'ApprovalFormBlockProvider';
|
||||||
|
|
||||||
|
const [firstKey] = Object.keys(formSchema.properties);
|
||||||
|
const firstProperty = formSchema.properties[firstKey];
|
||||||
|
|
||||||
|
_.set(firstProperty, 'x-component-props.useProps', '{{useApprovalFormBlockProps}}');
|
||||||
|
|
||||||
|
const [firstSchema] = flatSchemaArray(
|
||||||
|
formSchema.properties[firstKey],
|
||||||
|
(property) => property['x-component'] === 'ActionBar',
|
||||||
|
);
|
||||||
|
|
||||||
|
firstSchema['x-decorator'] = 'ActionBarProvider';
|
||||||
|
|
||||||
|
_.set(firstSchema, 'x-component-props.style', { marginTop: '1.5em', flexWrap: 'wrap' });
|
||||||
|
|
||||||
|
formSchema['x-block'] = 'action-form';
|
||||||
|
const item = { ...itemConfig, schema: formSchema };
|
||||||
|
|
||||||
|
return <InitializerWithSwitch {...itemConfig} item={item} type={'x-block'} schema={formSchema} />;
|
||||||
|
};
|
@ -0,0 +1,95 @@
|
|||||||
|
import { SchemaComponent, useAPIClient, useFormBlockContext, useRequest } from '@nocobase/client';
|
||||||
|
import { uid } from '@nocobase/utils/client';
|
||||||
|
import { default as React, useCallback, useContext } from 'react';
|
||||||
|
import { useForm } from '@nocobase/schema';
|
||||||
|
import { Spin } from 'antd';
|
||||||
|
import { flatSchemaArray } from '../../../constants';
|
||||||
|
|
||||||
|
import { ApprovalFormBlockProvider } from './VC.ApprovalFormBlockProvider';
|
||||||
|
import { FormBlockProvider } from '../../../common/Pd.FormBlock';
|
||||||
|
import { ActionBarProvider } from './Pd.ActionBarProvider';
|
||||||
|
import { ApprovalActionProvider } from './Pd.ApprovalActionProvider';
|
||||||
|
import { DetailsBlockProvider, SimpleDesigner } from '@nocobase/plugin-workflow/client';
|
||||||
|
import { ContextApproverBlock } from './Pd.SchemaConfigButtonContext';
|
||||||
|
import { useApprovalFormBlockProps } from './useApprovalFormBlockProps';
|
||||||
|
import { ContextApproverConfig } from '../Pd.ContextApproverConfig';
|
||||||
|
|
||||||
|
export const ApproverBlock = ({ value: srcID, onChange }) => {
|
||||||
|
const apiClient = useAPIClient();
|
||||||
|
// const workflowPlugin = usePlugin(PluginWorkflow);
|
||||||
|
const { values, setValuesIn } = useForm();
|
||||||
|
const { setFormValueChanged } = useContext(ContextApproverConfig) as any;
|
||||||
|
// const current = useNodeContext();
|
||||||
|
// const nodes = useAvailableUpstreams(current);
|
||||||
|
const onChangeFunc = useCallback(
|
||||||
|
(data) => {
|
||||||
|
const flatSet = flatSchemaArray(
|
||||||
|
data.toJSON(),
|
||||||
|
(field) => field['x-decorator'] === 'ApprovalFormBlockProvider',
|
||||||
|
).reduce((accSet, curr) => {
|
||||||
|
flatSchemaArray(curr, (field) => field['x-component'] === 'Action').forEach((field) => {
|
||||||
|
accSet.add(Number.parseInt(field['x-action'], 10));
|
||||||
|
});
|
||||||
|
return accSet;
|
||||||
|
}, new Set());
|
||||||
|
|
||||||
|
if (flatSet.size !== values.actions?.length || !values.actions.every((action) => flatSet.has(action))) {
|
||||||
|
setFormValueChanged(true);
|
||||||
|
}
|
||||||
|
setValuesIn('actions', [...flatSet]);
|
||||||
|
},
|
||||||
|
[setFormValueChanged, setValuesIn, values.actions],
|
||||||
|
);
|
||||||
|
|
||||||
|
const { data, loading } = useRequest(async () => {
|
||||||
|
if (srcID) {
|
||||||
|
const { data: serviceData } = await apiClient.request({ url: `uiSchemas:getJsonSchema/${srcID}` });
|
||||||
|
if (serviceData?.data?.['x-uid'] === srcID) {
|
||||||
|
return serviceData.data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const id = srcID ?? uid();
|
||||||
|
const schema = {
|
||||||
|
type: 'void',
|
||||||
|
name: id,
|
||||||
|
'x-uid': id,
|
||||||
|
'x-component': 'Grid',
|
||||||
|
'x-initializer': 'ApprovalProcessAddBlockButton',
|
||||||
|
properties: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
await apiClient.resource('uiSchemas').insert({ values: schema });
|
||||||
|
onChange(id);
|
||||||
|
return schema;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return <Spin />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ContextApproverBlock.Provider value={values}>
|
||||||
|
<SchemaComponent
|
||||||
|
memoized={true}
|
||||||
|
scope={{
|
||||||
|
useSubmit: () => {
|
||||||
|
return { run() {} };
|
||||||
|
},
|
||||||
|
useApprovalFormBlockProps,
|
||||||
|
useDetailsBlockProps: useFormBlockContext,
|
||||||
|
}}
|
||||||
|
components={{
|
||||||
|
FormBlockProvider,
|
||||||
|
ApprovalFormBlockProvider,
|
||||||
|
ActionBarProvider,
|
||||||
|
ApprovalActionProvider,
|
||||||
|
DetailsBlockProvider,
|
||||||
|
SimpleDesigner,
|
||||||
|
}}
|
||||||
|
// @ts-ignore
|
||||||
|
schema={data}
|
||||||
|
onChange={onChangeFunc}
|
||||||
|
/>
|
||||||
|
</ContextApproverBlock.Provider>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,65 @@
|
|||||||
|
import {
|
||||||
|
ExtendCollectionsProvider,
|
||||||
|
RecordProvider,
|
||||||
|
SchemaComponent,
|
||||||
|
SchemaComponentContext,
|
||||||
|
css,
|
||||||
|
} from '@nocobase/client';
|
||||||
|
import { useFlowContext } from '@nocobase/plugin-workflow/client';
|
||||||
|
import { uid } from '@nocobase/utils/client';
|
||||||
|
import React, { useContext, useState } from 'react';
|
||||||
|
import { CollectionApprovalTodos } from '../../../common/Cn.ApprovalTodos';
|
||||||
|
import { NAMESPACE } from '../../../locale';
|
||||||
|
import { ApproverBlock } from './VC.ApproverBlock';
|
||||||
|
|
||||||
|
// 审批人操作界面
|
||||||
|
export const ApproverInterfaceComponent = () => {
|
||||||
|
const context = useContext(SchemaComponentContext);
|
||||||
|
const [, setId] = useState(uid());
|
||||||
|
const { workflow } = useFlowContext();
|
||||||
|
const commentFields = CollectionApprovalTodos.fields.filter((field) => field.name === 'comment');
|
||||||
|
return (
|
||||||
|
<ExtendCollectionsProvider
|
||||||
|
collections={[
|
||||||
|
{
|
||||||
|
...CollectionApprovalTodos,
|
||||||
|
fields: commentFields,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<SchemaComponentContext.Provider
|
||||||
|
value={{
|
||||||
|
...context,
|
||||||
|
refresh: () => setId(uid()),
|
||||||
|
designable: !workflow.executed,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* @ts-ignore */}
|
||||||
|
<RecordProvider record={{}} parent={false}>
|
||||||
|
<SchemaComponent
|
||||||
|
components={{ SchemaContent: ApproverBlock }}
|
||||||
|
schema={{
|
||||||
|
name: 'drawer',
|
||||||
|
type: 'void',
|
||||||
|
title: `{{t("Approver's interface", { ns: "${NAMESPACE}" })}}`,
|
||||||
|
'x-component': 'Action.Drawer',
|
||||||
|
'x-component-props': {
|
||||||
|
className: css`
|
||||||
|
.ant-drawer-body {
|
||||||
|
background: var(--nb-box-bg);
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
properties: {
|
||||||
|
applyDetail: {
|
||||||
|
type: 'string',
|
||||||
|
'x-component': 'SchemaContent',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</RecordProvider>
|
||||||
|
</SchemaComponentContext.Provider>
|
||||||
|
</ExtendCollectionsProvider>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,6 @@
|
|||||||
|
import { useFormBlockContext } from '@nocobase/client';
|
||||||
|
|
||||||
|
export function useApprovalFormBlockProps(e) {
|
||||||
|
const { form } = useFormBlockContext();
|
||||||
|
return { form };
|
||||||
|
}
|
@ -0,0 +1,17 @@
|
|||||||
|
import { Plugin } from '@nocobase/client';
|
||||||
|
import PluginWorkflow from '@nocobase/plugin-workflow/client';
|
||||||
|
import { ApprovalInstruction } from './node.ApprovalInstruction';
|
||||||
|
import { ApproverActionConfigInitializer } from './approver-interface/Iz.ApproverActionConfig';
|
||||||
|
import { ApproverAddBlockInitializer } from './approver-interface/Iz.ApproverAddBlock';
|
||||||
|
|
||||||
|
export default class PluginKitApprovalInstruction extends Plugin {
|
||||||
|
async afterAdd() {}
|
||||||
|
async beforeLoad() {}
|
||||||
|
async load() {
|
||||||
|
const workflowPlugin = this.app.pm.get(PluginWorkflow);
|
||||||
|
workflowPlugin.registerInstruction('approval', ApprovalInstruction);
|
||||||
|
|
||||||
|
this.app.schemaInitializerManager.add(ApproverActionConfigInitializer);
|
||||||
|
this.app.schemaInitializerManager.add(ApproverAddBlockInitializer);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,196 @@
|
|||||||
|
import { ArrayItems } from '@formily/antd-v5';
|
||||||
|
import { css } from '@nocobase/client';
|
||||||
|
import { Instruction, RadioWithTooltip } from '@nocobase/plugin-workflow/client';
|
||||||
|
import { uid } from '@nocobase/utils/client';
|
||||||
|
import React from 'react';
|
||||||
|
import { APPROVAL_ACTION_STATUS } from '../../constants';
|
||||||
|
import { NAMESPACE } from '../../locale';
|
||||||
|
import { AssigneesAddition } from './approval-config/VC.AssigneesAddition';
|
||||||
|
import { AssigneesSelect } from './approval-config/VC.AssigneesSelect';
|
||||||
|
import { ContentTooltip } from './approval-config/VC.ContentTooltip';
|
||||||
|
import { NegotiationConfig } from './approval-config/VC.NegotiationConfig';
|
||||||
|
import { ApproverInterfaceComponent } from './approver-interface/VC.ApproverInterface';
|
||||||
|
import { SchemaConfigButtonApprover } from './approval-config/VC.SchemaConfigButtonApprover';
|
||||||
|
import { ApprovalInstructionNode } from './VC.ApprovalInstructionNode';
|
||||||
|
import { isApprovalReturnFunc } from './utils';
|
||||||
|
|
||||||
|
// 工作流节点 nodes - 人工处理->审批
|
||||||
|
export class ApprovalInstruction extends Instruction {
|
||||||
|
title = `{{t("Approval", { ns: "${NAMESPACE}" })}}`;
|
||||||
|
type = 'approval';
|
||||||
|
group = 'manual';
|
||||||
|
description = `{{t("Manual approval operations within the approval process, the approver can approve in the global approval block or in the approval block of a single record.", { ns: "${NAMESPACE}" })}}`;
|
||||||
|
// 审批节点类别
|
||||||
|
options = [
|
||||||
|
{
|
||||||
|
label: `{{t('Passthrough mode', { ns: "${NAMESPACE}" })}}`,
|
||||||
|
key: 'false',
|
||||||
|
value() {
|
||||||
|
return { branchMode: false, applyDetail: uid() };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: `{{t('Branch mode', { ns: "${NAMESPACE}" })}}`,
|
||||||
|
key: 'true',
|
||||||
|
value() {
|
||||||
|
return { branchMode: true, applyDetail: uid() };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
// 审批节点表单设置
|
||||||
|
fieldset = {
|
||||||
|
branchMode: {
|
||||||
|
type: 'boolean',
|
||||||
|
title: `{{t("Pass mode", { ns: "${NAMESPACE}" })}}`,
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'Radio.Group',
|
||||||
|
'x-component-props': { disabled: true },
|
||||||
|
enum: [
|
||||||
|
{
|
||||||
|
value: false,
|
||||||
|
label: (
|
||||||
|
<ContentTooltip
|
||||||
|
content={`{{t('Passthrough mode', { ns: "${NAMESPACE}" })}}`}
|
||||||
|
tooltip={`{{t('When rejected or returned, the workflow will be terminated immediately.', { ns: "${NAMESPACE}" })}}`}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: true,
|
||||||
|
label: (
|
||||||
|
<ContentTooltip
|
||||||
|
content={`{{t('Branch mode', { ns: "${NAMESPACE}" })}}`}
|
||||||
|
tooltip={`{{t('Could run different branch based on result.', { ns: "${NAMESPACE}" })}}`}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
assignees: {
|
||||||
|
type: 'array',
|
||||||
|
title: `{{t("Assignees", { 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': 'AssigneesSelect' },
|
||||||
|
remove: { type: 'void', 'x-decorator': 'FormItem', 'x-component': 'ArrayItems.Remove' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: true,
|
||||||
|
properties: {
|
||||||
|
add: {
|
||||||
|
type: 'void',
|
||||||
|
title: `{{t("Add assignee", { ns: "${NAMESPACE}" })}}`,
|
||||||
|
'x-component': 'AssigneesAddition',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
negotiation: {
|
||||||
|
type: 'number',
|
||||||
|
title: `{{t("Negotiation mode", { ns: "${NAMESPACE}" })}}`,
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'NegotiationConfig',
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
order: {
|
||||||
|
type: 'boolean',
|
||||||
|
title: `{{t("Order", { ns: "${NAMESPACE}" })}}`,
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'RadioWithTooltip',
|
||||||
|
'x-component-props': {
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
label: `{{t("Parallelly", { ns: "${NAMESPACE}" })}}`,
|
||||||
|
value: false,
|
||||||
|
tooltip: `{{t("Multiple approvers can approve in any order.", { ns: "${NAMESPACE}" })}}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: `{{t("Sequentially", { ns: "${NAMESPACE}" })}}`,
|
||||||
|
value: true,
|
||||||
|
tooltip: `{{t("Multiple approvers in sequential order.", { ns: "${NAMESPACE}" })}}`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
endOnReject: {
|
||||||
|
type: 'boolean',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'Checkbox',
|
||||||
|
'x-content': `{{t("End the workflow after rejection branch", { ns: "${NAMESPACE}" })}}`,
|
||||||
|
description: `{{t("When checked, the workflow will terminate when the rejection branch ends.", { ns: "${NAMESPACE}" })}}`,
|
||||||
|
'x-reactions': [
|
||||||
|
{
|
||||||
|
dependencies: ['.branchMode'],
|
||||||
|
fulfill: {
|
||||||
|
state: {
|
||||||
|
visible: '{{$deps[0]}}',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
applyDetail: {
|
||||||
|
type: 'void',
|
||||||
|
title: `{{t("Approver's interface", { ns: "${NAMESPACE}" })}}`,
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'SchemaConfigButtonApprover',
|
||||||
|
properties: {
|
||||||
|
applyDetail: {
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'SchemaConfig',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
// 审批节点组件
|
||||||
|
Component = ApprovalInstructionNode;
|
||||||
|
components = {
|
||||||
|
ArrayItems,
|
||||||
|
SchemaConfigButtonApprover,
|
||||||
|
SchemaConfig: ApproverInterfaceComponent,
|
||||||
|
AssigneesSelect,
|
||||||
|
NegotiationConfig,
|
||||||
|
RadioWithTooltip,
|
||||||
|
AssigneesAddition,
|
||||||
|
};
|
||||||
|
|
||||||
|
isAvailable({ workflow, upstream, branchIndex }) {
|
||||||
|
const isApproval = workflow.type === 'approval';
|
||||||
|
const isNotApprovalReturn = !isApprovalReturnFunc(
|
||||||
|
upstream,
|
||||||
|
branchIndex,
|
||||||
|
(currU, currB) => currU?.type === 'approval' && currB === APPROVAL_ACTION_STATUS.RETURNED,
|
||||||
|
);
|
||||||
|
return isApproval && isNotApprovalReturn;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,20 @@
|
|||||||
|
export function isApprovalReturnFunc(upstream, branchIndex = null, filter) {
|
||||||
|
for (let currU = upstream, currB = branchIndex; currU; currU = currU.upstream) {
|
||||||
|
const currentUpstream = findCurrentUpstream(currU, currB);
|
||||||
|
if (filter(currentUpstream, currB)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
currB = currU.branchIndex;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function findCurrentUpstream(upstream, branchIndex = null) {
|
||||||
|
for (let currU = upstream, currB = branchIndex; currU; currU = currU.upstream) {
|
||||||
|
if (branchIndex != null) {
|
||||||
|
return currU;
|
||||||
|
}
|
||||||
|
currB = currU.branchIndex;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
@ -0,0 +1,20 @@
|
|||||||
|
import { ActionContextProvider } from '@nocobase/client';
|
||||||
|
import { Button } from 'antd';
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { useTranslation } from '../../locale';
|
||||||
|
|
||||||
|
// 发起人操作界面->进入配置按钮
|
||||||
|
export function SchemaConfigButton(props) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [visible, setVisible] = useState(false);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Button type="primary" onClick={() => setVisible(true)} disabled={false}>
|
||||||
|
{t('Go to configure')}
|
||||||
|
</Button>
|
||||||
|
<ActionContextProvider value={{ visible, setVisible, formValueChanged: false }}>
|
||||||
|
{props.children}
|
||||||
|
</ActionContextProvider>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,20 @@
|
|||||||
|
import { Plugin } from '@nocobase/client';
|
||||||
|
import PluginWorkflow from '@nocobase/plugin-workflow/client';
|
||||||
|
import { ApprovalTrigger } from './node.ApprovalTrigger';
|
||||||
|
import { LauncherActionConfigInitializer } from './launcher-interface/Iz.LauncherActionConfig';
|
||||||
|
import { LauncherAddBlockButtonIntializer } from './launcher-interface/Iz.LauncherAddBlockButton';
|
||||||
|
|
||||||
|
export default class PluginKitApprovalTrigger extends Plugin {
|
||||||
|
async afterAdd() {}
|
||||||
|
async beforeLoad() {}
|
||||||
|
async load() {
|
||||||
|
const workflowPlugin = this.app.pm.get(PluginWorkflow);
|
||||||
|
workflowPlugin.registerTrigger('approval', ApprovalTrigger);
|
||||||
|
// workflowPlugin.registerInstruction('approval', ApprovalInstruction);
|
||||||
|
|
||||||
|
this.app.schemaInitializerManager.add(LauncherActionConfigInitializer);
|
||||||
|
this.app.schemaInitializerManager.add(LauncherAddBlockButtonIntializer);
|
||||||
|
// this.app.schemaInitializerManager.add(ApproverActionConfigInitializer);
|
||||||
|
// this.app.schemaInitializerManager.add(ApproverAddBlockInitializer);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,28 @@
|
|||||||
|
import { SchemaInitializer } from '@nocobase/client';
|
||||||
|
import { NAMESPACE } from '../../../locale';
|
||||||
|
import { APPROVAL_STATUS } from '../../../constants';
|
||||||
|
import { LauncherActionConfigComponent } from './VC.LauncherActionConfig';
|
||||||
|
|
||||||
|
// 区块-配置操作
|
||||||
|
export const LauncherActionConfigInitializer = new SchemaInitializer({
|
||||||
|
name: 'ApprovalApplyAddActionButton',
|
||||||
|
title: '{{t("Configure actions")}}',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
name: 'submit',
|
||||||
|
type: 'item',
|
||||||
|
title: '{{t("Submit")}}',
|
||||||
|
Component: LauncherActionConfigComponent,
|
||||||
|
action: APPROVAL_STATUS.SUBMITTED,
|
||||||
|
actionProps: { type: 'primary' },
|
||||||
|
disabled: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'save',
|
||||||
|
type: 'item',
|
||||||
|
title: `{{t("Save draft", { ns: "${NAMESPACE}" })}}`,
|
||||||
|
Component: LauncherActionConfigComponent,
|
||||||
|
action: APPROVAL_STATUS.DRAFT,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
@ -0,0 +1,38 @@
|
|||||||
|
import { SchemaInitializer, gridRowColWrap } from '@nocobase/client';
|
||||||
|
import { NAMESPACE } from '../../../locale';
|
||||||
|
import { LauncherAddBlockButtonComponent } from './VC.LauncherBlockButton';
|
||||||
|
|
||||||
|
// 创建区块
|
||||||
|
export const LauncherAddBlockButtonIntializer = new SchemaInitializer({
|
||||||
|
name: 'ApprovalApplyAddBlockButton',
|
||||||
|
wrap: gridRowColWrap,
|
||||||
|
title: '{{t("Add block")}}',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
name: 'forms',
|
||||||
|
type: 'itemGroup',
|
||||||
|
title: '{{t("Form")}}',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
name: 'form',
|
||||||
|
type: 'item',
|
||||||
|
title: `{{t("Apply form", { ns: "${NAMESPACE}" })}}`,
|
||||||
|
Component: LauncherAddBlockButtonComponent,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'others',
|
||||||
|
type: 'itemGroup',
|
||||||
|
title: '{{t("Other blocks")}}',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
name: 'markdown',
|
||||||
|
type: 'item',
|
||||||
|
title: '{{t("Markdown")}}',
|
||||||
|
Component: 'MarkdownBlockInitializer',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
@ -0,0 +1,33 @@
|
|||||||
|
import { css } from '@nocobase/client';
|
||||||
|
import { NAMESPACE } from '../../../locale';
|
||||||
|
|
||||||
|
// 发起人操作界面
|
||||||
|
export const getSchemaLauncherInterface = ({ values, dataSource, name }) => ({
|
||||||
|
name: values.collection,
|
||||||
|
type: 'void',
|
||||||
|
properties: {
|
||||||
|
drawer: {
|
||||||
|
type: 'void',
|
||||||
|
title: `{{t("Initiator's interface", { ns: "${NAMESPACE}" })}}`,
|
||||||
|
'x-component': 'Action.Drawer',
|
||||||
|
'x-component-props': {
|
||||||
|
className: css`
|
||||||
|
.ant-drawer-body {
|
||||||
|
background: const(--nb-box-bg);
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
properties: {
|
||||||
|
applyForm: {
|
||||||
|
type: 'string',
|
||||||
|
'x-decorator': 'CollectionProvider_deprecated',
|
||||||
|
'x-decorator-props': {
|
||||||
|
dataSource,
|
||||||
|
name,
|
||||||
|
},
|
||||||
|
'x-component': 'SchemaAddBlock',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
@ -0,0 +1,31 @@
|
|||||||
|
import { ActionInitializer, useSchemaInitializerItem } from '@nocobase/client';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
// 区块-配置操作
|
||||||
|
export const LauncherActionConfigComponent = () => {
|
||||||
|
const itemConfig = useSchemaInitializerItem();
|
||||||
|
const { action, actionProps = {}, ...restItemConfig } = itemConfig;
|
||||||
|
return (
|
||||||
|
<ActionInitializer
|
||||||
|
{...restItemConfig}
|
||||||
|
schema={{
|
||||||
|
type: 'void',
|
||||||
|
title: restItemConfig.title,
|
||||||
|
'x-decorator': 'ApplyActionStatusProvider',
|
||||||
|
'x-decorator-props': {
|
||||||
|
value: action,
|
||||||
|
},
|
||||||
|
'x-component': 'Action',
|
||||||
|
'x-component-props': {
|
||||||
|
...actionProps,
|
||||||
|
useAction: '{{ useSubmit }}',
|
||||||
|
},
|
||||||
|
'x-designer': 'Action.Designer',
|
||||||
|
'x-action': `${action}`,
|
||||||
|
'x-action-settings': {
|
||||||
|
assignedValues: {},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,79 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import {
|
||||||
|
SchemaInitializerItem,
|
||||||
|
createFormBlockSchema,
|
||||||
|
useCollection_deprecated,
|
||||||
|
useRecordCollectionDataSourceItems,
|
||||||
|
useSchemaInitializer,
|
||||||
|
useSchemaInitializerItem,
|
||||||
|
useSchemaTemplateManager,
|
||||||
|
} from '@nocobase/client';
|
||||||
|
import { NAMESPACE } from '../../../locale';
|
||||||
|
import { flatSchemaArray } from '../../../constants';
|
||||||
|
import { APPROVAL_STATUS } from '../../../constants';
|
||||||
|
|
||||||
|
// 创建区块-Initializer的component
|
||||||
|
export const LauncherAddBlockButtonComponent = () => {
|
||||||
|
const itemConfig = useSchemaInitializerItem();
|
||||||
|
const { insert } = useSchemaInitializer();
|
||||||
|
const collection = useCollection_deprecated();
|
||||||
|
const { getTemplateSchemaByMode } = useSchemaTemplateManager();
|
||||||
|
const items = useRecordCollectionDataSourceItems('FormItem');
|
||||||
|
|
||||||
|
const onClick = async ({ item }) => {
|
||||||
|
const template = item.template ? await getTemplateSchemaByMode(item) : null;
|
||||||
|
const {
|
||||||
|
['x-acl-action-props']: deleteA,
|
||||||
|
['x-acl-action']: deleteB,
|
||||||
|
...formSchema
|
||||||
|
} = createFormBlockSchema({
|
||||||
|
actionInitializers: 'ApprovalApplyAddActionButton',
|
||||||
|
actions: {
|
||||||
|
submit: {
|
||||||
|
type: 'void',
|
||||||
|
title: '{{t("Submit")}}',
|
||||||
|
'x-decorator': 'ApplyActionStatusProvider',
|
||||||
|
'x-decorator-props': { value: APPROVAL_STATUS.SUBMITTED },
|
||||||
|
'x-component': 'Action',
|
||||||
|
'x-component-props': { type: 'primary', htmlType: 'submit', useAction: '{{ useSubmit }}' },
|
||||||
|
'x-designer': 'Action.Designer',
|
||||||
|
'x-designer-props': {},
|
||||||
|
'x-action': `${APPROVAL_STATUS.SUBMITTED}`,
|
||||||
|
'x-action-settings': { removable: false, assignedValues: {} },
|
||||||
|
},
|
||||||
|
withdraw: {
|
||||||
|
type: 'void',
|
||||||
|
title: `{{t("Withdraw", { ns: "${NAMESPACE}" })}}`,
|
||||||
|
'x-decorator': 'WithdrawActionProvider',
|
||||||
|
'x-component': 'Action',
|
||||||
|
'x-component-props': {
|
||||||
|
confirm: {
|
||||||
|
title: `{{t('Withdraw', { ns: "${NAMESPACE}" })}}`,
|
||||||
|
content: `{{t('Are you sure you want to withdraw it?', { ns: "${NAMESPACE}" })}}`,
|
||||||
|
},
|
||||||
|
useAction: '{{ useWithdrawAction }}',
|
||||||
|
},
|
||||||
|
'x-designer': 'WithdrawActionDesigner',
|
||||||
|
'x-action': 'withdraw',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
dataSource: collection.dataSource,
|
||||||
|
resource: collection.name,
|
||||||
|
collection: collection.name,
|
||||||
|
template: template,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [key] = Object.keys(formSchema.properties);
|
||||||
|
const [targetSchema] = flatSchemaArray(
|
||||||
|
formSchema.properties[key],
|
||||||
|
(property) => property['x-component'] === 'ActionBar',
|
||||||
|
);
|
||||||
|
|
||||||
|
targetSchema['x-decorator'] = 'ActionBarProvider';
|
||||||
|
targetSchema['x-component-props'].style = { marginTop: '1.5em', flexWrap: 'wrap' };
|
||||||
|
|
||||||
|
insert(formSchema);
|
||||||
|
};
|
||||||
|
|
||||||
|
return <SchemaInitializerItem {...itemConfig} onClick={onClick} items={items} />;
|
||||||
|
};
|
@ -0,0 +1,21 @@
|
|||||||
|
import { SchemaComponent, parseCollectionName } from '@nocobase/client';
|
||||||
|
import { useForm } from '@nocobase/schema';
|
||||||
|
import React from 'react';
|
||||||
|
import { SchemaAddBlock } from './VC.SchemaAddBlock';
|
||||||
|
import { getSchemaLauncherInterface } from './Sm.LauncherInterface';
|
||||||
|
|
||||||
|
// 触发器-发起人的操作界面
|
||||||
|
export const LauncherInterface = () => {
|
||||||
|
const { values } = useForm();
|
||||||
|
const [dataSource, name] = parseCollectionName(values.collection);
|
||||||
|
const schema = getSchemaLauncherInterface({ values, dataSource, name });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SchemaComponent
|
||||||
|
components={{
|
||||||
|
SchemaAddBlock,
|
||||||
|
}}
|
||||||
|
schema={schema}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,101 @@
|
|||||||
|
import {
|
||||||
|
SchemaComponent,
|
||||||
|
SchemaComponentContext,
|
||||||
|
SchemaComponentProvider,
|
||||||
|
useAPIClient,
|
||||||
|
useFormBlockProps,
|
||||||
|
useRequest,
|
||||||
|
} from '@nocobase/client';
|
||||||
|
import { useFlowContext } from '@nocobase/plugin-workflow/client';
|
||||||
|
import { uid } from '@nocobase/utils/client';
|
||||||
|
import { Spin } from 'antd';
|
||||||
|
import React, { useContext } from 'react';
|
||||||
|
|
||||||
|
// 发起人操作界面-创建区块
|
||||||
|
export const SchemaAddBlock = ({ value, onChange }) => {
|
||||||
|
const api = useAPIClient();
|
||||||
|
const { workflow } = useFlowContext();
|
||||||
|
const { components } = useContext(SchemaComponentContext);
|
||||||
|
|
||||||
|
// TODO:
|
||||||
|
// 获取对应数据表的表单Schema
|
||||||
|
const { data, loading } = useRequest(() =>
|
||||||
|
E(this, null, function* () {
|
||||||
|
let m;
|
||||||
|
if (value) {
|
||||||
|
const { data: h } = yield api.request({ url: `uiSchemas:getJsonSchema/${value}` });
|
||||||
|
if (((m = h.data) == null ? void 0 : m['x-uid']) === value) return h.data;
|
||||||
|
}
|
||||||
|
const l = uid(),
|
||||||
|
d = {
|
||||||
|
type: 'void',
|
||||||
|
name: l,
|
||||||
|
'x-uid': l,
|
||||||
|
'x-component': 'Grid',
|
||||||
|
'x-initializer': 'ApprovalApplyAddBlockButton',
|
||||||
|
properties: {},
|
||||||
|
};
|
||||||
|
return yield api.resource('uiSchemas').insert({ values: d }), onChange(l), d;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return <Spin />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SchemaComponentProvider components={components} designable={!workflow.executed}>
|
||||||
|
<SchemaComponent
|
||||||
|
memoized={true}
|
||||||
|
components={{
|
||||||
|
ActionBarProvider,
|
||||||
|
ApplyActionStatusProvider,
|
||||||
|
WithdrawActionProvider,
|
||||||
|
}}
|
||||||
|
scope={{
|
||||||
|
useSubmit,
|
||||||
|
useWithdrawAction,
|
||||||
|
useFormBlockProps,
|
||||||
|
}}
|
||||||
|
// @ts-ignore ugly
|
||||||
|
schema={data}
|
||||||
|
/>
|
||||||
|
</SchemaComponentProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
function useSubmit() {
|
||||||
|
return { run() {} };
|
||||||
|
}
|
||||||
|
function useWithdrawAction() {
|
||||||
|
return { run() {} };
|
||||||
|
}
|
||||||
|
function ActionBarProvider(props) {
|
||||||
|
return props.children;
|
||||||
|
}
|
||||||
|
function ApplyActionStatusProvider(props) {
|
||||||
|
return props.children;
|
||||||
|
}
|
||||||
|
function WithdrawActionProvider(props) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
// TODO:
|
||||||
|
const E = (f, o, p) =>
|
||||||
|
// eslint-disable-next-line promise/param-names
|
||||||
|
new Promise((r, G) => {
|
||||||
|
const u = ($) => {
|
||||||
|
try {
|
||||||
|
A(p.next($));
|
||||||
|
} catch (U) {
|
||||||
|
G(U);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
y = ($) => {
|
||||||
|
try {
|
||||||
|
A(p.throw($));
|
||||||
|
} catch (U) {
|
||||||
|
G(U);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
A = ($) => ($.done ? r($.value) : Promise.resolve($.value).then(u, y));
|
||||||
|
A((p = p.apply(f, o)).next());
|
||||||
|
});
|
@ -6,20 +6,22 @@ import {
|
|||||||
useWorkflowAnyExecuted,
|
useWorkflowAnyExecuted,
|
||||||
} from '@nocobase/plugin-workflow/client';
|
} from '@nocobase/plugin-workflow/client';
|
||||||
import { useForm } from '@nocobase/schema';
|
import { useForm } from '@nocobase/schema';
|
||||||
import { NAMESPACE } from '../locale';
|
import { SchemaConfigButton } from './VC.ConfigButton';
|
||||||
import { T, Vo, zo, useTranslation, w, x } from './refined';
|
import { NAMESPACE, usePluginTranslation } from '../../locale';
|
||||||
import { usePluginTranslation } from '../locale';
|
import { LauncherInterface } from './launcher-interface/VC.LauncherInterface';
|
||||||
|
|
||||||
|
// 工作流节点-审批触发器节点
|
||||||
export class ApprovalTrigger extends Trigger {
|
export class ApprovalTrigger extends Trigger {
|
||||||
sync = false;
|
sync = false;
|
||||||
title = `{{t('Approval event', { ns: "${NAMESPACE}" })}}`;
|
title = `{{t('Approval event', { ns: "${NAMESPACE}" })}}`;
|
||||||
description = `{{t("Triggered when an approval request is initiated through an action button or API. Dedicated to the approval process, with exclusive approval node and block for managing documents and tracking processing processes.", { ns: "${NAMESPACE}" })}}`;
|
description = `{{t("Triggered when an approval request is initiated through an action button or API. Dedicated to the approval process, with exclusive approval node and block for managing documents and tracking processing processes.", { ns: "${NAMESPACE}" })}}`;
|
||||||
|
|
||||||
|
// 触发器配置表
|
||||||
fieldset = {
|
fieldset = {
|
||||||
collection: {
|
collection: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
title: '{{t("Collection")}}',
|
title: '{{t("Collection")}}',
|
||||||
required: !0,
|
required: true,
|
||||||
'x-decorator': 'FormItem',
|
'x-decorator': 'FormItem',
|
||||||
'x-component': 'DataSourceCollectionCascader',
|
'x-component': 'DataSourceCollectionCascader',
|
||||||
'x-disabled': '{{ useWorkflowAnyExecuted() }}',
|
'x-disabled': '{{ useWorkflowAnyExecuted() }}',
|
||||||
@ -34,17 +36,17 @@ export class ApprovalTrigger extends Trigger {
|
|||||||
options: [
|
options: [
|
||||||
{
|
{
|
||||||
label: `{{t("Initiate and approve in data blocks only", { ns: "${NAMESPACE}" })}}`,
|
label: `{{t("Initiate and approve in data blocks only", { ns: "${NAMESPACE}" })}}`,
|
||||||
value: !1,
|
value: false,
|
||||||
tooltip: `{{t("Actions from any form block can be bound to this workflow for initiating approvals, and the approval process can be handled and tracked in the approval block of a single record which is typically applicable to business data.", { ns: "${NAMESPACE}" })}}`,
|
tooltip: `{{t("Actions from any form block can be bound to this workflow for initiating approvals, and the approval process can be handled and tracked in the approval block of a single record which is typically applicable to business data.", { ns: "${NAMESPACE}" })}}`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: `{{t("Initiate and approve in both data blocks and global approval blocks", { ns: "${NAMESPACE}" })}}`,
|
label: `{{t("Initiate and approve in both data blocks and global approval blocks", { ns: "${NAMESPACE}" })}}`,
|
||||||
value: !0,
|
value: true,
|
||||||
tooltip: `{{t("In addition to data blocks, a global approval block can also be used to initiates and processes approvals, which typically applies to administrative data.", { ns: "${NAMESPACE}" })}}`,
|
tooltip: `{{t("In addition to data blocks, a global approval block can also be used to initiates and processes approvals, which typically applies to administrative data.", { ns: "${NAMESPACE}" })}}`,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
default: !1,
|
default: false,
|
||||||
},
|
},
|
||||||
withdrawable: {
|
withdrawable: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
@ -59,8 +61,23 @@ export class ApprovalTrigger extends Trigger {
|
|||||||
description: `{{t("For initiating approvals, or viewing and manipulating initiated approvals.", { ns: "${NAMESPACE}" })}}`,
|
description: `{{t("For initiating approvals, or viewing and manipulating initiated approvals.", { ns: "${NAMESPACE}" })}}`,
|
||||||
'x-decorator': 'FormItem',
|
'x-decorator': 'FormItem',
|
||||||
'x-component': 'SchemaConfigButton',
|
'x-component': 'SchemaConfigButton',
|
||||||
'x-reactions': [{ dependencies: ['collection'], fulfill: { state: { visible: '{{!!$deps[0]}}' } } }],
|
'x-reactions': [
|
||||||
properties: { applyForm: { type: 'void', 'x-component': 'SchemaConfig', default: null } },
|
{
|
||||||
|
dependencies: ['collection'],
|
||||||
|
fulfill: {
|
||||||
|
state: {
|
||||||
|
visible: '{{!!$deps[0]}}',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
properties: {
|
||||||
|
applyForm: {
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'SchemaInitiatorForm',
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
appends: {
|
appends: {
|
||||||
type: 'array',
|
type: 'array',
|
||||||
@ -71,40 +88,59 @@ export class ApprovalTrigger extends Trigger {
|
|||||||
'x-component': 'AppendsTreeSelect',
|
'x-component': 'AppendsTreeSelect',
|
||||||
'x-component-props': {
|
'x-component-props': {
|
||||||
title: 'Preload associations',
|
title: 'Preload associations',
|
||||||
multiple: !0,
|
multiple: true,
|
||||||
useCollection() {
|
useCollection() {
|
||||||
const { values: n } = useForm();
|
const { values } = useForm();
|
||||||
return n == null ? void 0 : n.collection;
|
return values?.collection;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
'x-reactions': [{ dependencies: ['collection'], fulfill: { state: { visible: '{{!!$deps[0]}}' } } }],
|
'x-reactions': [
|
||||||
|
{
|
||||||
|
dependencies: ['collection'],
|
||||||
|
fulfill: {
|
||||||
|
state: {
|
||||||
|
visible: '{{!!$deps[0]}}',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
scope = { useWorkflowAnyExecuted: useWorkflowAnyExecuted };
|
scope = { useWorkflowAnyExecuted };
|
||||||
components = { SchemaConfigButton: Vo, SchemaConfig: zo, RadioWithTooltip: RadioWithTooltip };
|
components = {
|
||||||
isActionTriggerable = (n, a) => ['create', 'update'].includes(a.action) && !a.direct;
|
SchemaConfigButton,
|
||||||
|
SchemaInitiatorForm: LauncherInterface,
|
||||||
|
RadioWithTooltip,
|
||||||
|
};
|
||||||
|
|
||||||
useVariables(n, a) {
|
isActionTriggerable = (config, context) => {
|
||||||
var m;
|
return ['create', 'update'].includes(context.action) && !context.direct;
|
||||||
const s = useCompile(),
|
};
|
||||||
{ getCollectionFields: i } = useCollectionManager_deprecated(),
|
|
||||||
{ t: v } = usePluginTranslation(),
|
useVariables(config, options) {
|
||||||
l = [
|
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||||
{
|
const compile = useCompile();
|
||||||
collectionName: n.collection,
|
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||||
name: 'data',
|
const { getCollectionFields } = useCollectionManager_deprecated();
|
||||||
type: 'hasOne',
|
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||||
target: n.collection,
|
const { t } = usePluginTranslation();
|
||||||
uiSchema: { title: v('Trigger data', { ns: 'workflow' }) },
|
const rootFields = [
|
||||||
},
|
{
|
||||||
];
|
collectionName: config.collection,
|
||||||
return getCollectionFieldOptions(
|
name: 'data',
|
||||||
w(x({ appends: ['data', ...(((m = n.appends) == null ? void 0 : m.map((h) => `data.${h}`)) || [])] }, a), {
|
type: 'hasOne',
|
||||||
fields: l,
|
target: config.collection,
|
||||||
compile: s,
|
uiSchema: { title: t('Trigger data', { ns: 'workflow' }) },
|
||||||
getCollectionFields: i,
|
},
|
||||||
}),
|
];
|
||||||
);
|
return getCollectionFieldOptions({
|
||||||
|
// depth,
|
||||||
|
appends: ['data', ...(config.appends?.map((item) => `data.${item}`) || [])],
|
||||||
|
...options,
|
||||||
|
fields: rootFields,
|
||||||
|
compile,
|
||||||
|
getCollectionFields,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -0,0 +1,92 @@
|
|||||||
|
import { JOB_STATUS } from '@nocobase/plugin-workflow/client';
|
||||||
|
import { NAMESPACE, lang } from './locale';
|
||||||
|
// 审批状态
|
||||||
|
export const APPROVAL_STATUS = {
|
||||||
|
DRAFT: 0,
|
||||||
|
RETURNED: 1,
|
||||||
|
SUBMITTED: 2,
|
||||||
|
PROCESSING: 3,
|
||||||
|
APPROVED: 4,
|
||||||
|
REJECTED: -1,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const APPROVAL_ACTION_STATUS = {
|
||||||
|
ASSIGNED: null,
|
||||||
|
PENDING: 0,
|
||||||
|
RETURNED: 1,
|
||||||
|
APPROVED: 2,
|
||||||
|
REJECTED: -1,
|
||||||
|
CANCELED: -2,
|
||||||
|
WITHDRAWN: -3,
|
||||||
|
};
|
||||||
|
export const approvalStatusOptions = [
|
||||||
|
{ value: APPROVAL_ACTION_STATUS.ASSIGNED, label: `{{t("Assigned", { ns: "${NAMESPACE}" })}}`, color: 'blue' },
|
||||||
|
{ value: APPROVAL_ACTION_STATUS.PENDING, label: `{{t("Pending", { ns: "${NAMESPACE}" })}}`, color: 'gold' },
|
||||||
|
{ value: APPROVAL_ACTION_STATUS.RETURNED, label: `{{t("Returned", { ns: "${NAMESPACE}" })}}`, color: 'purple' },
|
||||||
|
{ value: APPROVAL_ACTION_STATUS.APPROVED, label: `{{t("Approved", { ns: "${NAMESPACE}" })}}`, color: 'green' },
|
||||||
|
{ value: APPROVAL_ACTION_STATUS.REJECTED, label: `{{t("Rejected", { ns: "${NAMESPACE}" })}}`, color: 'red' },
|
||||||
|
{ value: APPROVAL_ACTION_STATUS.WITHDRAWN, label: `{{t("Withdrawn", { ns: "${NAMESPACE}" })}}` },
|
||||||
|
];
|
||||||
|
export const approvalStatusConfigObj = approvalStatusOptions.reduce(
|
||||||
|
(e, t) =>
|
||||||
|
Object.assign(e, {
|
||||||
|
[t.value]: t,
|
||||||
|
}),
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
export const ApprovalStatusEnums = [
|
||||||
|
{ value: APPROVAL_STATUS.DRAFT, label: `{{t("Draft", { ns: "${NAMESPACE}" })}}`, editable: true },
|
||||||
|
{
|
||||||
|
value: APPROVAL_STATUS.RETURNED,
|
||||||
|
label: `{{t("Returned", { ns: "${NAMESPACE}" })}}`,
|
||||||
|
color: 'purple',
|
||||||
|
editable: true,
|
||||||
|
},
|
||||||
|
{ value: APPROVAL_STATUS.SUBMITTED, label: `{{t("Submitted", { ns: "${NAMESPACE}" })}}`, color: 'cyan' },
|
||||||
|
{ value: APPROVAL_STATUS.PROCESSING, label: `{{t("Processing", { ns: "${NAMESPACE}" })}}`, color: 'gold' },
|
||||||
|
{ value: APPROVAL_STATUS.APPROVED, label: `{{t("Approved", { ns: "${NAMESPACE}" })}}`, color: 'green' },
|
||||||
|
{ value: APPROVAL_STATUS.REJECTED, label: `{{t("Rejected", { ns: "${NAMESPACE}" })}}`, color: 'red' },
|
||||||
|
];
|
||||||
|
export const ApprovalStatusEnumDict = ApprovalStatusEnums.reduce((e, t) => Object.assign(e, { [t.value]: t }), {});
|
||||||
|
export const JobStatusEnums = {
|
||||||
|
[JOB_STATUS.PENDING]: { color: 'gold', label: `{{t('Pending', { ns: "${NAMESPACE}" })}}` },
|
||||||
|
[JOB_STATUS.RESOLVED]: { color: 'green', label: `{{t('Approved', { ns: "${NAMESPACE}" })}}` },
|
||||||
|
[JOB_STATUS.REJECTED]: { color: 'red', label: `{{t('Rejected', { ns: "${NAMESPACE}" })}}` },
|
||||||
|
[JOB_STATUS.RETRY_NEEDED]: { color: 'red', label: `{{t('Returned', { ns: "${NAMESPACE}" })}}` },
|
||||||
|
};
|
||||||
|
export const VoteCategory = { SINGLE: Symbol('single'), ALL: Symbol('all'), VOTE: Symbol('vote') };
|
||||||
|
export const VoteCategoryEnums = [
|
||||||
|
{ value: VoteCategory.SINGLE, label: `{{t("Or", { ns: "${NAMESPACE}" })}}` },
|
||||||
|
{ value: VoteCategory.ALL, label: `{{t("And", { ns: "${NAMESPACE}" })}}` },
|
||||||
|
{
|
||||||
|
value: VoteCategory.VOTE,
|
||||||
|
label: (v: number) => `${lang('Voting')} ( > ${(v * 100).toFixed(0)}%)`,
|
||||||
|
},
|
||||||
|
].reduce((obj, vote) => Object.assign(obj, { [vote.value]: vote }), {});
|
||||||
|
export function voteOption(value: number) {
|
||||||
|
switch (true) {
|
||||||
|
case value === 1:
|
||||||
|
return VoteCategory.ALL;
|
||||||
|
case 0 < value && value < 1:
|
||||||
|
return VoteCategory.VOTE;
|
||||||
|
default:
|
||||||
|
return VoteCategory.SINGLE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export function flatSchemaArray(sourceData, filter, needRecursion = false) {
|
||||||
|
const flatArray = [];
|
||||||
|
if (!sourceData) {
|
||||||
|
return flatArray;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filter(sourceData) && (!needRecursion || !sourceData.properties)) {
|
||||||
|
flatArray.push(sourceData);
|
||||||
|
} else {
|
||||||
|
sourceData.properties &&
|
||||||
|
Object.keys(sourceData.properties).forEach((key) => {
|
||||||
|
flatArray.push(...flatSchemaArray(sourceData.properties[key], filter));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return flatArray;
|
||||||
|
}
|
@ -1,40 +1,13 @@
|
|||||||
import { Plugin } from '@nocobase/client';
|
import { Plugin } from '@nocobase/client';
|
||||||
import { ApprovalInstruction } from './ApprovalInstruction';
|
|
||||||
import { ApprovalTrigger } from './ApprovalTrigger';
|
|
||||||
import { ApprovalProcessAddBlockButton } from './ApprovalProcessAddBlockButton';
|
|
||||||
import { ApprovalProcessAddActionButton } from './ApprovalProcessAddActionButton';
|
|
||||||
import { ApprovalApplyAddBlockButton } from './ApprovalApplyAddBlockButton';
|
|
||||||
import { ApprovalApplyAddActionButton } from './ApprovalApplyAddActionButton';
|
|
||||||
|
|
||||||
import PluginWorkflow from '@nocobase/plugin-workflow/client';
|
import PluginKitApprovalConfiguration from './configuration';
|
||||||
import { NAMESPACE } from '../locale';
|
import PluginKitApprovalUsage from './usage';
|
||||||
import { ApprovalBlock } from './refined';
|
|
||||||
|
|
||||||
export default class ApprovalPlugin extends Plugin {
|
export default class PluginApproval extends Plugin {
|
||||||
async afterAdd() {}
|
async afterAdd() {
|
||||||
async beforeLoad() {}
|
this.pm.add(PluginKitApprovalConfiguration);
|
||||||
async load() {
|
this.pm.add(PluginKitApprovalUsage);
|
||||||
const plugin = this.app.pm.get(PluginWorkflow);
|
|
||||||
plugin.registerTrigger('approval', ApprovalTrigger);
|
|
||||||
plugin.registerInstruction('approval', ApprovalInstruction);
|
|
||||||
this.app.addComponents({ ApprovalBlock });
|
|
||||||
this.app.schemaInitializerManager.add(ApprovalApplyAddActionButton);
|
|
||||||
this.app.schemaInitializerManager.add(ApprovalApplyAddBlockButton);
|
|
||||||
this.app.schemaInitializerManager.add(ApprovalProcessAddActionButton);
|
|
||||||
this.app.schemaInitializerManager.add(ApprovalProcessAddBlockButton);
|
|
||||||
this.app.schemaInitializerManager.get('BlockInitializers').add('otherBlocks.approval', {
|
|
||||||
key: 'approvalBlock',
|
|
||||||
name: 'approvalBlock',
|
|
||||||
type: 'item',
|
|
||||||
title: `{{t("Approval", { ns: "${NAMESPACE}" })}}`,
|
|
||||||
Component: 'ApprovalBlock.BlockInitializer',
|
|
||||||
icon: 'AuditOutlined',
|
|
||||||
});
|
|
||||||
this.app.schemaInitializerManager.get('RecordBlockInitializers').add('otherBlocks.approvalsBlock', {
|
|
||||||
type: 'item',
|
|
||||||
name: 'approvalsBlock',
|
|
||||||
title: `{{t("Related approvals", { ns: "${NAMESPACE}" })}}`,
|
|
||||||
Component: 'ApprovalBlock.RecordApprovals.BlockInitializer',
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
async beforeLoad() {}
|
||||||
|
async load() {}
|
||||||
}
|
}
|
||||||
|
20
packages/plugins/@hera/plugin-approval/src/client/locale.ts
Normal file
20
packages/plugins/@hera/plugin-approval/src/client/locale.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { i18n, tval as nTval } from '@nocobase/client';
|
||||||
|
|
||||||
|
export const NAMESPACE = '@hera/plugin-approval';
|
||||||
|
|
||||||
|
export function usePluginTranslation(): any {
|
||||||
|
return useTranslation();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useTranslation() {
|
||||||
|
const t = (key: string, options = {}) => i18n.t(key, { ns: NAMESPACE, ...options });
|
||||||
|
return { t };
|
||||||
|
}
|
||||||
|
export function lang(key: string, options = {}) {
|
||||||
|
return i18n.t(key, {
|
||||||
|
...options,
|
||||||
|
ns: NAMESPACE,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export const tval = (key: string) => nTval(key, { ns: NAMESPACE });
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,42 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { ExtendCollectionsProvider, TableBlockProvider, useRecord } from '@nocobase/client';
|
||||||
|
import { CollectionApprovalTodos } from '../../common/Cn.ApprovalTodos';
|
||||||
|
import { CollectionApprovals } from '../approval-common/Cn.Approvals';
|
||||||
|
import { CollectionFlowNodes } from '../approval-common/Cn.FlowNodes';
|
||||||
|
import { CollectionWorkflows } from '../approval-common/Cn.Workflows';
|
||||||
|
|
||||||
|
export function ApprovalBlockDecorator({ children, ...props }) {
|
||||||
|
const {
|
||||||
|
collection,
|
||||||
|
action = 'list',
|
||||||
|
params = {
|
||||||
|
filter: {},
|
||||||
|
},
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const record = useRecord();
|
||||||
|
const config = {
|
||||||
|
collection,
|
||||||
|
resource: collection,
|
||||||
|
action,
|
||||||
|
params: {
|
||||||
|
pageSize: 20,
|
||||||
|
sort: ['-createdAt'],
|
||||||
|
...params,
|
||||||
|
filter: record?.id ? { dataKey: `${record.id}`, ...params.filter } : { ...params.filter },
|
||||||
|
},
|
||||||
|
rowKey: 'id',
|
||||||
|
showIndex: true,
|
||||||
|
dragSort: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ExtendCollectionsProvider
|
||||||
|
collections={[CollectionWorkflows, CollectionFlowNodes, CollectionApprovals, CollectionApprovalTodos]}
|
||||||
|
>
|
||||||
|
<TableBlockProvider name={collection} {...config}>
|
||||||
|
{children}
|
||||||
|
</TableBlockProvider>
|
||||||
|
</ExtendCollectionsProvider>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,74 @@
|
|||||||
|
import { TableOutlined } from '@ant-design/icons';
|
||||||
|
import { SchemaInitializerItem, useSchemaInitializer, useSchemaInitializerItem } from '@nocobase/client';
|
||||||
|
import { uid } from '@nocobase/utils/client';
|
||||||
|
import React from 'react';
|
||||||
|
import { NAMESPACE } from '../../locale';
|
||||||
|
import { PathNameMap_ApprovalBlock } from './map';
|
||||||
|
|
||||||
|
// 创建区块-审批(发起/待办)
|
||||||
|
export const ApprovalBlockComponent = () => {
|
||||||
|
const schemaInitializerItem = useSchemaInitializerItem();
|
||||||
|
const { insert } = useSchemaInitializer();
|
||||||
|
return (
|
||||||
|
<SchemaInitializerItem
|
||||||
|
icon={<TableOutlined />}
|
||||||
|
{...schemaInitializerItem}
|
||||||
|
items={[
|
||||||
|
{
|
||||||
|
type: 'item',
|
||||||
|
title: `{{t("Launch", { ns: "${NAMESPACE}" })}}`,
|
||||||
|
'x-component': 'ApprovalBlock.Launch',
|
||||||
|
collection: 'approvals',
|
||||||
|
params: {
|
||||||
|
appends: ['createdBy.nickname', 'workflow.title', 'workflow.enabled'],
|
||||||
|
except: ['data'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'item',
|
||||||
|
title: `{{t("Todos", { ns: "${NAMESPACE}" })}}`,
|
||||||
|
'x-component': 'ApprovalBlock.Todos',
|
||||||
|
collection: 'approvalRecords',
|
||||||
|
params: {
|
||||||
|
appends: [
|
||||||
|
'user.id',
|
||||||
|
'user.nickname',
|
||||||
|
'node.id',
|
||||||
|
'node.title',
|
||||||
|
'job.id',
|
||||||
|
'job.status',
|
||||||
|
'job.result',
|
||||||
|
'workflow.id',
|
||||||
|
'workflow.title',
|
||||||
|
'workflow.enabled',
|
||||||
|
'execution.id',
|
||||||
|
'execution.status',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
onClick={({ item }) => {
|
||||||
|
const id = uid();
|
||||||
|
insert({
|
||||||
|
type: 'void',
|
||||||
|
name: id,
|
||||||
|
'x-uid': id,
|
||||||
|
'x-component': 'CardItem',
|
||||||
|
'x-decorator': PathNameMap_ApprovalBlock.Decorator,
|
||||||
|
'x-decorator-props': {
|
||||||
|
collection: item.collection,
|
||||||
|
action: 'listCentralized',
|
||||||
|
params: item.params,
|
||||||
|
},
|
||||||
|
'x-designer': 'TableBlockDesigner',
|
||||||
|
properties: {
|
||||||
|
block: {
|
||||||
|
type: 'void',
|
||||||
|
'x-component': item['x-component'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,11 @@
|
|||||||
|
import { createContext, useContext } from 'react';
|
||||||
|
|
||||||
|
interface ApprovalExecution {
|
||||||
|
id: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ContextApprovalExecution = createContext<Partial<ApprovalExecution>>({});
|
||||||
|
|
||||||
|
export function useContextApprovalExecution() {
|
||||||
|
return useContext(ContextApprovalExecution);
|
||||||
|
}
|
@ -0,0 +1,10 @@
|
|||||||
|
import { FlowContext } from '@nocobase/plugin-workflow/client';
|
||||||
|
import React, { useContext } from 'react';
|
||||||
|
|
||||||
|
export function useFlowContext() {
|
||||||
|
return useContext(FlowContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FlowContextProvider({ workflow = undefined, children, value }) {
|
||||||
|
return <FlowContext.Provider value={{ workflow, ...value }}>{children}</FlowContext.Provider>;
|
||||||
|
}
|
@ -0,0 +1,11 @@
|
|||||||
|
import { SchemaComponentContext, useSchemaComponentContext } from '@nocobase/client';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
export function SchemaComponentContextProvider({ designable, children }) {
|
||||||
|
const schemaComponentContext = useSchemaComponentContext();
|
||||||
|
return (
|
||||||
|
<SchemaComponentContext.Provider value={{ ...schemaComponentContext, designable }}>
|
||||||
|
{children}
|
||||||
|
</SchemaComponentContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,37 @@
|
|||||||
|
import { Plugin } from '@nocobase/client';
|
||||||
|
import PluginKitApprovalCommon from '../approval-common';
|
||||||
|
import { tval } from '../../locale';
|
||||||
|
import { ViewActionTodos } from './todos/VC.ViewActionTodos';
|
||||||
|
import { ApprovalBlockComponent } from './VC.ApprovalBlock';
|
||||||
|
import { ApprovalBlockTodos } from './todos/VC.ApprovalBlockTodos';
|
||||||
|
import { ViewActionLaunch } from './launch/VC.ViewActionLaunch';
|
||||||
|
import { ApprovalBlockDecorator } from './Dt.ApprovalBlock';
|
||||||
|
import { ApprovalBlockLaunch } from './launch/VC.ApprovalBlockLaunch';
|
||||||
|
|
||||||
|
export default class PluginKitApprovalBlock extends Plugin {
|
||||||
|
async afterAdd() {
|
||||||
|
this.pm.add(PluginKitApprovalCommon);
|
||||||
|
}
|
||||||
|
|
||||||
|
async beforeLoad() {}
|
||||||
|
|
||||||
|
async load() {
|
||||||
|
this.app.addComponents({
|
||||||
|
'ApprovalBlock.Decorator': ApprovalBlockDecorator,
|
||||||
|
'ApprovalBlock.BlockInitializer': ApprovalBlockComponent,
|
||||||
|
'ApprovalBlock.Launch': ApprovalBlockLaunch,
|
||||||
|
'ApprovalBlock.Todos': ApprovalBlockTodos,
|
||||||
|
'ApprovalBlock.ViewActionLaunch': ViewActionLaunch,
|
||||||
|
'ApprovalBlock.ViewActionTodos': ViewActionTodos,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.app.schemaInitializerManager.get('page:addBlock').add('otherBlocks.approval', {
|
||||||
|
key: 'approvalBlock',
|
||||||
|
name: 'approvalBlock',
|
||||||
|
type: 'item',
|
||||||
|
title: tval('Approval'),
|
||||||
|
Component: 'ApprovalBlock.BlockInitializer',
|
||||||
|
icon: 'AuditOutlined',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,15 @@
|
|||||||
|
export interface Approval {
|
||||||
|
id: number;
|
||||||
|
collectionName: string;
|
||||||
|
dataKey: string;
|
||||||
|
workflow: any;
|
||||||
|
executions: any[];
|
||||||
|
approvalExecutions: any[];
|
||||||
|
latestApprovalExecution: any;
|
||||||
|
records: any[];
|
||||||
|
createdById: number;
|
||||||
|
status: number;
|
||||||
|
data: any;
|
||||||
|
applicantRole: any;
|
||||||
|
latestExecutionId?: any;
|
||||||
|
}
|
@ -0,0 +1,12 @@
|
|||||||
|
import { RecordProvider, useRecord } from '@nocobase/client';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
export function RecordDecorator(props) {
|
||||||
|
const { latestExecutionId } = useRecord();
|
||||||
|
return (
|
||||||
|
// @ts-ignore
|
||||||
|
<RecordProvider record={{ id: latestExecutionId }} parent={false}>
|
||||||
|
{props.children}
|
||||||
|
</RecordProvider>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,20 @@
|
|||||||
|
import { useCurrentUserContext } from '@nocobase/client';
|
||||||
|
import { useContextApprovalExecution } from '../common/Pd.ApprovalExecution';
|
||||||
|
import { useApproval } from '../../approval-common/Pd.ApprovalData';
|
||||||
|
import { APPROVAL_STATUS } from '../../../constants';
|
||||||
|
|
||||||
|
export function ActionBarProvider(props) {
|
||||||
|
const { data } = useCurrentUserContext();
|
||||||
|
const { status, createdById, latestExecutionId } = useApproval();
|
||||||
|
const approvalExecution = useContextApprovalExecution();
|
||||||
|
|
||||||
|
const isSameId = data.data.id === createdById;
|
||||||
|
const isSameExcutionId = latestExecutionId === approvalExecution.id;
|
||||||
|
const isExcutionDid = [APPROVAL_STATUS.DRAFT, APPROVAL_STATUS.RETURNED, APPROVAL_STATUS.SUBMITTED].includes(status);
|
||||||
|
|
||||||
|
if (!isSameId || !isSameExcutionId || !isExcutionDid) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return props.children;
|
||||||
|
}
|
@ -0,0 +1,28 @@
|
|||||||
|
import React, { useContext } from 'react';
|
||||||
|
import { useCurrentUserContext } from '@nocobase/client';
|
||||||
|
import { useFlowContext } from '@nocobase/plugin-workflow/client';
|
||||||
|
import { createContext } from 'react';
|
||||||
|
import { useApproval } from '../../approval-common/Pd.ApprovalData';
|
||||||
|
import { APPROVAL_STATUS } from '../../../constants';
|
||||||
|
|
||||||
|
const ContextApprovalStatus = createContext(APPROVAL_STATUS.SUBMITTED);
|
||||||
|
|
||||||
|
export function useContextApprovalStatus() {
|
||||||
|
return useContext(ContextApprovalStatus);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ApplyActionStatusProvider(props) {
|
||||||
|
const { value, children } = props;
|
||||||
|
const { status, createdById } = useApproval();
|
||||||
|
const { workflow } = useFlowContext();
|
||||||
|
const { data } = useCurrentUserContext();
|
||||||
|
const isSameId = data.data.id === createdById;
|
||||||
|
const isEnbled = workflow.enabled;
|
||||||
|
const isStatusDid = [APPROVAL_STATUS.DRAFT, APPROVAL_STATUS.RETURNED].includes(status);
|
||||||
|
|
||||||
|
if (isSameId && isEnbled && isStatusDid) {
|
||||||
|
return <ContextApprovalStatus.Provider value={value}>{children}</ContextApprovalStatus.Provider>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
@ -0,0 +1,20 @@
|
|||||||
|
import { useCurrentUserContext } from '@nocobase/client';
|
||||||
|
import { useFlowContext } from '@nocobase/plugin-workflow/client';
|
||||||
|
import { APPROVAL_STATUS } from '../../../constants';
|
||||||
|
import { useApproval } from '../../approval-common/Pd.ApprovalData';
|
||||||
|
|
||||||
|
export function WithdrawActionProvider({ children }) {
|
||||||
|
const { data } = useCurrentUserContext();
|
||||||
|
const { status, createdById } = useApproval();
|
||||||
|
const { workflow } = useFlowContext();
|
||||||
|
|
||||||
|
const isSameId = data.data.id === createdById;
|
||||||
|
const isEnabledWithdraw = workflow.enabled && workflow.config.withdrawable;
|
||||||
|
const isStatusSubmitted = APPROVAL_STATUS.SUBMITTED === status;
|
||||||
|
|
||||||
|
if (isSameId && isEnabledWithdraw && isStatusSubmitted) {
|
||||||
|
return children;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
@ -0,0 +1,151 @@
|
|||||||
|
// import { PathNameMap_ApprovalBlock } from '../map';
|
||||||
|
import { ApprovalBlock } from '../map';
|
||||||
|
import { NAMESPACE } from '../../../locale';
|
||||||
|
import { css } from '@nocobase/client';
|
||||||
|
|
||||||
|
export const SchemaApprovalBlockLaunch = {
|
||||||
|
type: 'void',
|
||||||
|
name: 'launch',
|
||||||
|
properties: {
|
||||||
|
actions: {
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'ActionBar',
|
||||||
|
'x-component-props': {
|
||||||
|
style: {
|
||||||
|
marginBottom: 16,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
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' },
|
||||||
|
'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-component-props': { icon: 'ReloadOutlined' },
|
||||||
|
'x-align': 'right',
|
||||||
|
},
|
||||||
|
apply: {
|
||||||
|
type: 'void',
|
||||||
|
title: `{{t("Apply new", { ns: "${NAMESPACE}" })}}`,
|
||||||
|
'x-component': 'ApplyButton',
|
||||||
|
'x-align': 'right',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
table: {
|
||||||
|
type: 'array',
|
||||||
|
'x-component': 'TableV2',
|
||||||
|
'x-use-component-props': 'useTableBlockProps',
|
||||||
|
'x-component-props': {
|
||||||
|
rowKey: 'id',
|
||||||
|
tableLayout: 'fixed',
|
||||||
|
className: css`
|
||||||
|
.ant-table-cell {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
properties: {
|
||||||
|
action: {
|
||||||
|
type: 'void',
|
||||||
|
'x-decorator': 'TableV2.Column.Decorator',
|
||||||
|
'x-component': 'TableV2.Column',
|
||||||
|
'x-component-props': { width: 60 },
|
||||||
|
title: '{{t("Actions")}}',
|
||||||
|
properties: {
|
||||||
|
action: {
|
||||||
|
// 'x-component': PathNameMap_ApprovalBlock.ViewActionLaunch,
|
||||||
|
'x-component': 'ApprovalBlock.ViewActionLaunch',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
id: {
|
||||||
|
type: 'void',
|
||||||
|
'x-decorator': 'TableV2.Column.Decorator',
|
||||||
|
'x-component': 'TableV2.Column',
|
||||||
|
'x-component-props': { width: 80 },
|
||||||
|
title: '{{t("ID")}}',
|
||||||
|
properties: {
|
||||||
|
id: {
|
||||||
|
type: 'number',
|
||||||
|
'x-component': 'Input',
|
||||||
|
'x-read-pretty': true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
workflow: {
|
||||||
|
type: 'void',
|
||||||
|
'x-decorator': 'TableV2.Column.Decorator',
|
||||||
|
'x-component': 'TableV2.Column',
|
||||||
|
'x-component-props': {
|
||||||
|
width: 200,
|
||||||
|
},
|
||||||
|
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: {
|
||||||
|
'x-component': 'CollectionField',
|
||||||
|
'x-read-pretty': true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
createdBy: {
|
||||||
|
type: 'void',
|
||||||
|
'x-decorator': 'TableV2.Column.Decorator',
|
||||||
|
'x-component': 'TableV2.Column',
|
||||||
|
'x-component-props': {
|
||||||
|
width: 140,
|
||||||
|
},
|
||||||
|
title: `{{t("Initiator", { ns: "${NAMESPACE}" })}}`,
|
||||||
|
properties: {
|
||||||
|
createdBy: {
|
||||||
|
'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,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
@ -0,0 +1,22 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { SchemaComponent } from '@nocobase/client';
|
||||||
|
import { UserColumn } from '../../approval-common/approval-columns/column.user';
|
||||||
|
import { WorkflowColumn } from '../../approval-common/approval-columns/column.workflow';
|
||||||
|
import { NodeColumn } from '../../approval-common/approval-columns/column.node';
|
||||||
|
import { SchemaApprovalBlockLaunch } from './Sm.ApprovalBlockLaunch';
|
||||||
|
import { ApplyButton } from './apply-button/VC.ApplyButton';
|
||||||
|
|
||||||
|
// 审批-发起: 区块表格
|
||||||
|
export const ApprovalBlockLaunch = () => {
|
||||||
|
return (
|
||||||
|
<SchemaComponent
|
||||||
|
components={{
|
||||||
|
NodeColumn,
|
||||||
|
WorkflowColumn,
|
||||||
|
UserColumn,
|
||||||
|
ApplyButton,
|
||||||
|
}}
|
||||||
|
schema={SchemaApprovalBlockLaunch}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,35 @@
|
|||||||
|
import { SchemaComponent, useRecord } from '@nocobase/client';
|
||||||
|
import React from 'react';
|
||||||
|
import { RecordDecorator } from './Dt.Record';
|
||||||
|
import { ViewActionLaunchContent } from './VC.ViewActionLaunchContent';
|
||||||
|
|
||||||
|
// 审批-发起: 操作-查看
|
||||||
|
export const ViewActionLaunch = ({ popoverComponent = 'Action.Drawer', popoverComponentProps = {} }) => {
|
||||||
|
const record = useRecord();
|
||||||
|
return (
|
||||||
|
<SchemaComponent
|
||||||
|
components={{
|
||||||
|
ViewActionContent: ViewActionLaunchContent,
|
||||||
|
RecordDecorator,
|
||||||
|
}}
|
||||||
|
schema={{
|
||||||
|
name: `view-${record.id}`,
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'Action.Link',
|
||||||
|
title: '{{t("View")}}',
|
||||||
|
properties: {
|
||||||
|
drawer: {
|
||||||
|
type: 'void',
|
||||||
|
'x-component': popoverComponent,
|
||||||
|
'x-component-props': { className: 'nb-action-popup', ...popoverComponentProps },
|
||||||
|
properties: {
|
||||||
|
content: Object.assign({ type: 'void' }, record.approvalId ? {} : { 'x-decorator': 'RecordDecorator' }, {
|
||||||
|
'x-component': 'ViewActionContent',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,153 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import {
|
||||||
|
RemoteSchemaComponent,
|
||||||
|
SchemaComponent,
|
||||||
|
SchemaComponentProvider,
|
||||||
|
useFormBlockContext,
|
||||||
|
useRecord,
|
||||||
|
useRequest,
|
||||||
|
} from '@nocobase/client';
|
||||||
|
import { DetailsBlockProvider, FlowContext } from '@nocobase/plugin-workflow/client';
|
||||||
|
import { useForm } from '@nocobase/schema';
|
||||||
|
import { Result, Spin } from 'antd';
|
||||||
|
import { useContext } from 'react';
|
||||||
|
import { NAMESPACE } from '../../../locale';
|
||||||
|
import { FormBlockProvider } from '../../../common/Pd.FormBlock';
|
||||||
|
import { ContextApprovalExecution } from '../common/Pd.ApprovalExecution';
|
||||||
|
import { ApprovalContext } from '../../approval-common/Pd.ApprovalData';
|
||||||
|
import { ContextWithActionEnabled } from '../../approval-common/Pd.WithActionEnabled';
|
||||||
|
import { useFormBlockProps } from './hooks/useFormBlockProps';
|
||||||
|
import { useSubmit } from './hooks/useSubmit';
|
||||||
|
import { useWithdrawAction } from './hooks/useWithdrawAction';
|
||||||
|
import { useDestroyAction } from './hooks/useDestroyAction';
|
||||||
|
import { ActionBarProvider } from './Pd.ActionBar';
|
||||||
|
import { WithdrawActionProvider } from './Pd.WithdrawAction';
|
||||||
|
import { SchemaComponentContextProvider } from '../common/Pd.SchemaComponent';
|
||||||
|
import { ApplyActionStatusProvider } from './Pd.ApplyActionStatus';
|
||||||
|
import { ApprovalCommon } from '../../approval-common/map';
|
||||||
|
import { FlowContextProvider } from '../common/Pd.FlowContext';
|
||||||
|
|
||||||
|
export const ViewActionLaunchContent = () => {
|
||||||
|
const { id } = useRecord();
|
||||||
|
const { actionEnabled } = useContext(ContextWithActionEnabled);
|
||||||
|
const { loading, data } = useRequest(
|
||||||
|
{
|
||||||
|
resource: 'approvalExecutions',
|
||||||
|
action: 'get',
|
||||||
|
params: {
|
||||||
|
filterByTk: id,
|
||||||
|
appends: [
|
||||||
|
'execution',
|
||||||
|
'execution.jobs',
|
||||||
|
'approval',
|
||||||
|
'approval.workflow',
|
||||||
|
'approval.workflow.nodes',
|
||||||
|
'approval.approvalExecutions',
|
||||||
|
'approval.createdBy.id',
|
||||||
|
'approval.createdBy.nickname',
|
||||||
|
'approval.records',
|
||||||
|
'approval.records.node.title',
|
||||||
|
'approval.records.node.config',
|
||||||
|
'approval.records.job',
|
||||||
|
'approval.records.user.nickname',
|
||||||
|
],
|
||||||
|
except: ['approval.approvalExecutions.snapshot', 'approval.records.snapshot'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ refreshDeps: [id] },
|
||||||
|
) as any;
|
||||||
|
// @ts-ignore
|
||||||
|
const approvalData = data?.data;
|
||||||
|
const { approval, execution, ...approvalValue } = approvalData || {};
|
||||||
|
const { workflow } = approval || {};
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return <Spin />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!approvalData) {
|
||||||
|
return <Result status="error" title="Loading failed" />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FlowContextProvider
|
||||||
|
value={{
|
||||||
|
workflow: workflow,
|
||||||
|
nodes: approval?.nodes,
|
||||||
|
execution: execution,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ApprovalContext.Provider value={approval}>
|
||||||
|
<ContextApprovalExecution.Provider value={approvalValue}>
|
||||||
|
<SchemaComponent
|
||||||
|
components={{
|
||||||
|
SchemaComponentProvider: SchemaComponentProvider,
|
||||||
|
RemoteSchemaComponent: RemoteSchemaComponent,
|
||||||
|
SchemaComponentContextProvider,
|
||||||
|
FormBlockProvider,
|
||||||
|
ActionBarProvider,
|
||||||
|
ApplyActionStatusProvider,
|
||||||
|
WithdrawActionProvider,
|
||||||
|
DetailsBlockProvider,
|
||||||
|
}}
|
||||||
|
scope={{
|
||||||
|
useForm,
|
||||||
|
useSubmit: useSubmit,
|
||||||
|
useFormBlockProps,
|
||||||
|
useDetailsBlockProps: useFormBlockContext,
|
||||||
|
useWithdrawAction,
|
||||||
|
useDestroyAction,
|
||||||
|
}}
|
||||||
|
schema={{
|
||||||
|
name: `view-${approval == null ? void 0 : approval.id}`,
|
||||||
|
type: 'void',
|
||||||
|
properties: {
|
||||||
|
tabs: {
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'Tabs',
|
||||||
|
properties: Object.assign(
|
||||||
|
{
|
||||||
|
detail: {
|
||||||
|
type: 'void',
|
||||||
|
title: `{{t('Application content', { ns: '${NAMESPACE}' })}}`,
|
||||||
|
'x-component': 'Tabs.TabPane',
|
||||||
|
properties: {
|
||||||
|
detail: {
|
||||||
|
type: 'void',
|
||||||
|
'x-decorator': 'SchemaComponentContextProvider',
|
||||||
|
'x-decorator-props': { designable: false },
|
||||||
|
'x-component': 'RemoteSchemaComponent',
|
||||||
|
'x-component-props': {
|
||||||
|
uid: workflow?.config.applyForm,
|
||||||
|
noForm: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
actionEnabled
|
||||||
|
? {}
|
||||||
|
: {
|
||||||
|
process: {
|
||||||
|
type: 'void',
|
||||||
|
title: `{{t('Approval process', { ns: '${NAMESPACE}' })}}`,
|
||||||
|
'x-component': 'Tabs.TabPane',
|
||||||
|
properties: {
|
||||||
|
process: {
|
||||||
|
type: 'void',
|
||||||
|
'x-decorator': 'CardItem',
|
||||||
|
'x-component': ApprovalCommon.PathNameMap.ViewComponent.ApprovalProcess,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</ContextApprovalExecution.Provider>
|
||||||
|
</ApprovalContext.Provider>
|
||||||
|
</FlowContextProvider>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,3 @@
|
|||||||
|
export function ActionBarProvider(props) {
|
||||||
|
return props.children;
|
||||||
|
}
|
@ -0,0 +1,13 @@
|
|||||||
|
import React, { useContext } from 'react';
|
||||||
|
import { createContext } from 'react';
|
||||||
|
import { APPROVAL_STATUS } from '../../../../constants';
|
||||||
|
|
||||||
|
const ContextApprovalStatus = createContext(APPROVAL_STATUS.SUBMITTED);
|
||||||
|
|
||||||
|
export function useContextApprovalStatus() {
|
||||||
|
return useContext(ContextApprovalStatus);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ApplyActionStatusProvider(props) {
|
||||||
|
return <ContextApprovalStatus.Provider value={props.value}>{props.children}</ContextApprovalStatus.Provider>;
|
||||||
|
}
|
@ -0,0 +1,3 @@
|
|||||||
|
export function WithdrawActionProvider() {
|
||||||
|
return null;
|
||||||
|
}
|
@ -0,0 +1,123 @@
|
|||||||
|
import { DownOutlined, PlusOutlined } from '@ant-design/icons';
|
||||||
|
import {
|
||||||
|
ActionContextProvider,
|
||||||
|
CollectionProvider_deprecated,
|
||||||
|
RemoteSchemaComponent,
|
||||||
|
SchemaComponent,
|
||||||
|
SchemaComponentContext,
|
||||||
|
css,
|
||||||
|
parseCollectionName,
|
||||||
|
useAPIClient,
|
||||||
|
} from '@nocobase/client';
|
||||||
|
import { Button, Dropdown } from 'antd';
|
||||||
|
import React, { useCallback, useContext, useEffect, useState } from 'react';
|
||||||
|
import { useTranslation } from '../../../../locale';
|
||||||
|
import { useSubmit } from './hooks/useSubmit';
|
||||||
|
import { useWithdrawAction } from './hooks/useWithdrawAction';
|
||||||
|
import { ActionBarProvider } from './Pd.ActionBar';
|
||||||
|
import { ApplyActionStatusProvider } from './Pd.ActionStatus';
|
||||||
|
import { FlowContextProvider } from '../../common/Pd.FlowContext';
|
||||||
|
import { WithdrawActionProvider } from './Pd.ActionWithdraw';
|
||||||
|
|
||||||
|
// 审批-发起: 发起按钮
|
||||||
|
export const ApplyButton = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const context = useContext(SchemaComponentContext);
|
||||||
|
const apiClient = useAPIClient();
|
||||||
|
const [visible, setVisible] = useState(false);
|
||||||
|
const [items, setItems] = useState([]);
|
||||||
|
const [schema, setSchema] = useState(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
apiClient
|
||||||
|
.resource('workflows')
|
||||||
|
.listApprovalFlows({ filter: { 'config.centralized': true } })
|
||||||
|
.then(({ data }) => {
|
||||||
|
setItems(data.data);
|
||||||
|
})
|
||||||
|
.catch(console.error);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const onClick = useCallback(
|
||||||
|
({ key }) => {
|
||||||
|
const targetItems = items.find((item) => +item.id === +key);
|
||||||
|
const { applyForm } = targetItems?.config ?? {};
|
||||||
|
const [dataSource, name] = parseCollectionName(targetItems.config.collection);
|
||||||
|
|
||||||
|
setSchema({
|
||||||
|
type: 'void',
|
||||||
|
properties: {
|
||||||
|
[`drawer-${targetItems.id}`]: {
|
||||||
|
type: 'void',
|
||||||
|
title: targetItems.title,
|
||||||
|
'x-decorator': 'FlowContextProvider',
|
||||||
|
'x-decorator-props': {
|
||||||
|
workflow: targetItems,
|
||||||
|
},
|
||||||
|
'x-component': 'Action.Drawer',
|
||||||
|
'x-component-props': {
|
||||||
|
className: css`
|
||||||
|
.ant-drawer-body {
|
||||||
|
background: var(--nb-box-bg);
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
properties: {
|
||||||
|
[applyForm]: {
|
||||||
|
type: 'void',
|
||||||
|
'x-decorator': 'CollectionProvider_deprecated',
|
||||||
|
'x-decorator-props': {
|
||||||
|
name,
|
||||||
|
dataSource,
|
||||||
|
},
|
||||||
|
'x-component': 'RemoteSchemaComponent',
|
||||||
|
'x-component-props': {
|
||||||
|
uid: applyForm,
|
||||||
|
noForm: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
setVisible(true);
|
||||||
|
},
|
||||||
|
[items],
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<ActionContextProvider value={{ visible, setVisible }}>
|
||||||
|
<Dropdown
|
||||||
|
menu={{
|
||||||
|
items: items.map(({ id, title }) => ({
|
||||||
|
key: id,
|
||||||
|
label: title,
|
||||||
|
})),
|
||||||
|
onClick,
|
||||||
|
}}
|
||||||
|
disabled={!items.length}
|
||||||
|
>
|
||||||
|
<Button icon={<PlusOutlined />} type="primary">
|
||||||
|
{t('Apply')}
|
||||||
|
<DownOutlined />
|
||||||
|
</Button>
|
||||||
|
</Dropdown>
|
||||||
|
<SchemaComponentContext.Provider value={{ ...context, designable: false }}>
|
||||||
|
<SchemaComponent
|
||||||
|
schema={schema}
|
||||||
|
components={{
|
||||||
|
RemoteSchemaComponent: RemoteSchemaComponent,
|
||||||
|
CollectionProvider_deprecated: CollectionProvider_deprecated,
|
||||||
|
FlowContextProvider: FlowContextProvider,
|
||||||
|
ApplyActionStatusProvider: ApplyActionStatusProvider,
|
||||||
|
ActionBarProvider,
|
||||||
|
WithdrawActionProvider: WithdrawActionProvider,
|
||||||
|
}}
|
||||||
|
scope={{
|
||||||
|
useSubmit: useSubmit,
|
||||||
|
useWithdrawAction,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</SchemaComponentContext.Provider>
|
||||||
|
</ActionContextProvider>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,49 @@
|
|||||||
|
import {
|
||||||
|
joinCollectionName,
|
||||||
|
useAPIClient,
|
||||||
|
useActionContext,
|
||||||
|
useBlockRequestContext,
|
||||||
|
useCollection_deprecated,
|
||||||
|
} from '@nocobase/client';
|
||||||
|
import { useFlowContext } from '@nocobase/plugin-workflow/client';
|
||||||
|
import { useField, useForm } from '@nocobase/schema';
|
||||||
|
import { useContextApprovalStatus } from '../Pd.ActionStatus';
|
||||||
|
|
||||||
|
export function useSubmit() {
|
||||||
|
const from = useForm();
|
||||||
|
const field = useField();
|
||||||
|
const { setVisible } = useActionContext();
|
||||||
|
const { __parent } = useBlockRequestContext();
|
||||||
|
const collection = useCollection_deprecated();
|
||||||
|
const contextWe = useContextApprovalStatus();
|
||||||
|
const apiClient = useAPIClient();
|
||||||
|
const { workflow } = useFlowContext();
|
||||||
|
|
||||||
|
return {
|
||||||
|
run() {
|
||||||
|
return () => {
|
||||||
|
try {
|
||||||
|
from.submit(), (field.data = field.data || {});
|
||||||
|
field.data.loading = true;
|
||||||
|
apiClient.resource('approvals').create({
|
||||||
|
values: {
|
||||||
|
collectionName: joinCollectionName(collection.dataSource, collection.name),
|
||||||
|
data: from.values,
|
||||||
|
status: contextWe,
|
||||||
|
workflowId: workflow.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
setVisible(false);
|
||||||
|
from.reset();
|
||||||
|
field.data.loading = false;
|
||||||
|
const service = __parent.service;
|
||||||
|
if (service) {
|
||||||
|
service.refresh();
|
||||||
|
}
|
||||||
|
} catch (h) {
|
||||||
|
field.data && (field.data.loading = false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
@ -0,0 +1,3 @@
|
|||||||
|
export function useWithdrawAction() {
|
||||||
|
return { run() {} };
|
||||||
|
}
|
@ -0,0 +1,31 @@
|
|||||||
|
import { useAPIClient, useActionContext } from '@nocobase/client';
|
||||||
|
import { useField } from '@nocobase/schema';
|
||||||
|
import { useApproval } from '../../../approval-common/Pd.ApprovalData';
|
||||||
|
import _ from 'lodash';
|
||||||
|
|
||||||
|
export function useDestroyAction() {
|
||||||
|
const field = useField();
|
||||||
|
const { setVisible, setSubmitted } = useActionContext() as any;
|
||||||
|
const approval = useApproval();
|
||||||
|
const apiClient = useAPIClient();
|
||||||
|
|
||||||
|
const run = () => {
|
||||||
|
return () => {
|
||||||
|
try {
|
||||||
|
_.set(field, ['data', 'loading'], true);
|
||||||
|
|
||||||
|
apiClient.resource('approvals').destroy({
|
||||||
|
filterByTk: approval.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
setSubmitted(true);
|
||||||
|
setVisible(false);
|
||||||
|
} catch (err) {
|
||||||
|
_.set(field, ['data', 'loading'], false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
run,
|
||||||
|
};
|
||||||
|
}
|
@ -0,0 +1,37 @@
|
|||||||
|
import { useCurrentUserContext } from '@nocobase/client';
|
||||||
|
import { useFlowContext } from '@nocobase/plugin-workflow/client';
|
||||||
|
import { useForm } from '@nocobase/schema';
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
import { useContextApprovalExecution } from '../../common/Pd.ApprovalExecution';
|
||||||
|
import { useApproval } from '../../../approval-common/Pd.ApprovalData';
|
||||||
|
import { ApprovalStatusEnumDict } from '../../../../constants';
|
||||||
|
|
||||||
|
export function useFormBlockProps() {
|
||||||
|
const approval = useApproval() as any;
|
||||||
|
const approvalExecution = useContextApprovalExecution();
|
||||||
|
const { workflow } = useFlowContext();
|
||||||
|
const form = useForm();
|
||||||
|
const { data } = useCurrentUserContext();
|
||||||
|
|
||||||
|
const { editable } = ApprovalStatusEnumDict[approval.status];
|
||||||
|
|
||||||
|
const needEditable =
|
||||||
|
editable &&
|
||||||
|
approval?.latestExecutionId === approvalExecution.id &&
|
||||||
|
approval?.createdById === data?.data.id &&
|
||||||
|
workflow.enabled;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!approval) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (needEditable) {
|
||||||
|
form.setPattern('editable');
|
||||||
|
} else {
|
||||||
|
form.setPattern('readPretty');
|
||||||
|
}
|
||||||
|
}, [form, approval, needEditable]);
|
||||||
|
|
||||||
|
return { form };
|
||||||
|
}
|
@ -0,0 +1,45 @@
|
|||||||
|
import { useAPIClient, useActionContext } from '@nocobase/client';
|
||||||
|
import { useFlowContext } from '@nocobase/plugin-workflow/client';
|
||||||
|
import { useField, useForm } from '@nocobase/schema';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import { useApproval } from '../../../approval-common/Pd.ApprovalData';
|
||||||
|
import { useContextApprovalStatus } from '../Pd.ApplyActionStatus';
|
||||||
|
|
||||||
|
export function useSubmit() {
|
||||||
|
const from = useForm();
|
||||||
|
const field = useField();
|
||||||
|
const { setVisible, setSubmitted } = useActionContext() as any;
|
||||||
|
const { id } = useApproval();
|
||||||
|
const { workflow } = useFlowContext();
|
||||||
|
const contextApprovalStatus = useContextApprovalStatus();
|
||||||
|
const apiClient = useAPIClient();
|
||||||
|
|
||||||
|
const run = () => {
|
||||||
|
return () => {
|
||||||
|
try {
|
||||||
|
from.submit();
|
||||||
|
|
||||||
|
_.set(field, ['data', 'loading'], true);
|
||||||
|
|
||||||
|
apiClient.resource('approvals').update({
|
||||||
|
filterByTk: id,
|
||||||
|
values: {
|
||||||
|
collectionName: workflow.config.collection,
|
||||||
|
data: from.values,
|
||||||
|
status: contextApprovalStatus,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
setSubmitted(true);
|
||||||
|
setVisible(false);
|
||||||
|
from.reset();
|
||||||
|
_.set(field, ['data', 'loading'], false);
|
||||||
|
} catch (m) {
|
||||||
|
_.set(field, ['data', 'loading'], false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
run,
|
||||||
|
};
|
||||||
|
}
|
@ -0,0 +1,30 @@
|
|||||||
|
import { useAPIClient, useActionContext } from '@nocobase/client';
|
||||||
|
import { useField } from '@nocobase/schema';
|
||||||
|
import { useApproval } from '../../../approval-common/Pd.ApprovalData';
|
||||||
|
|
||||||
|
export function useWithdrawAction() {
|
||||||
|
const field = useField();
|
||||||
|
const { setVisible, setSubmitted } = useActionContext() as any;
|
||||||
|
const approval = useApproval();
|
||||||
|
const api = useAPIClient();
|
||||||
|
return {
|
||||||
|
run() {
|
||||||
|
return async () => {
|
||||||
|
try {
|
||||||
|
field.data = field.data ?? {};
|
||||||
|
field.data.loading = true;
|
||||||
|
await api.resource('approvals').withdraw({
|
||||||
|
filterByTk: approval.id,
|
||||||
|
});
|
||||||
|
setSubmitted(true);
|
||||||
|
setVisible(false);
|
||||||
|
field.data.loading = false;
|
||||||
|
} catch (v) {
|
||||||
|
if (field.data) {
|
||||||
|
field.data.loading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
@ -0,0 +1,31 @@
|
|||||||
|
import { ViewActionTodos } from './todos/VC.ViewActionTodos';
|
||||||
|
import { ApprovalBlockComponent } from './VC.ApprovalBlock';
|
||||||
|
import { ApprovalBlockTodos } from './todos/VC.ApprovalBlockTodos';
|
||||||
|
import { ViewActionLaunch } from './launch/VC.ViewActionLaunch';
|
||||||
|
import { ApprovalBlockDecorator } from './Dt.ApprovalBlock';
|
||||||
|
|
||||||
|
import { ApprovalBlockLaunch } from './launch/VC.ApprovalBlockLaunch';
|
||||||
|
|
||||||
|
// 区块-审批
|
||||||
|
export const ApprovalBlock = () => null;
|
||||||
|
|
||||||
|
export const PathNameMap_ApprovalBlock = {
|
||||||
|
Decorator: 'ApprovalBlock.Decorator',
|
||||||
|
BlockInitializer: 'ApprovalBlock.BlockInitializer',
|
||||||
|
Launch: 'ApprovalBlock.Launch',
|
||||||
|
Todos: 'ApprovalBlock.Todos',
|
||||||
|
ViewActionLaunch: 'ApprovalBlock.ViewActionLaunch',
|
||||||
|
ViewActionTodos: 'ApprovalBlock.ViewActionTodos',
|
||||||
|
};
|
||||||
|
|
||||||
|
ApprovalBlock.Decorator = ApprovalBlockDecorator;
|
||||||
|
|
||||||
|
ApprovalBlock.BlockInitializer = ApprovalBlockComponent;
|
||||||
|
|
||||||
|
ApprovalBlock.Launch = ApprovalBlockLaunch;
|
||||||
|
|
||||||
|
ApprovalBlock.Todos = ApprovalBlockTodos;
|
||||||
|
|
||||||
|
ApprovalBlock.ViewActionLaunch = ViewActionLaunch;
|
||||||
|
|
||||||
|
ApprovalBlock.ViewActionTodos = ViewActionTodos;
|
@ -0,0 +1,92 @@
|
|||||||
|
import {
|
||||||
|
BlockRequestContext_deprecated,
|
||||||
|
CollectionProvider_deprecated,
|
||||||
|
FormActiveFieldsProvider,
|
||||||
|
FormBlockContext,
|
||||||
|
FormV2,
|
||||||
|
RecordProvider,
|
||||||
|
SchemaComponent,
|
||||||
|
useAPIClient,
|
||||||
|
useAssociationNames,
|
||||||
|
useCurrentUserContext,
|
||||||
|
useDesignable,
|
||||||
|
} from '@nocobase/client';
|
||||||
|
import { useFlowContext } from '@nocobase/plugin-workflow/client';
|
||||||
|
import { RecursionField, createForm, useField, useFieldSchema } from '@nocobase/schema';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import React, { Fragment, useContext, useMemo, useRef } from 'react';
|
||||||
|
import { useContextApprovalExecutions } from './Pd.ApprovalExecutions';
|
||||||
|
|
||||||
|
export function ApprovalFormBlockDecorator(props) {
|
||||||
|
const approvalExecutions = useContextApprovalExecutions();
|
||||||
|
const { job } = approvalExecutions;
|
||||||
|
const omitApproval = _.omit(approvalExecutions, ['approval', 'job', 'node', 'snapshot']);
|
||||||
|
const { execution, workflow } = useFlowContext();
|
||||||
|
const fieldSchema = useFieldSchema();
|
||||||
|
const field = useField();
|
||||||
|
const ref = useRef(null);
|
||||||
|
const { getAssociationAppends } = useAssociationNames();
|
||||||
|
const { appends, updateAssociationValues } = getAssociationAppends();
|
||||||
|
const { data } = useCurrentUserContext();
|
||||||
|
const { findComponent } = useDesignable();
|
||||||
|
const component = findComponent(field.component?.[0]) || Fragment;
|
||||||
|
const form = useMemo(() => {
|
||||||
|
return createForm({
|
||||||
|
initialValues: omitApproval,
|
||||||
|
pattern:
|
||||||
|
!workflow.enabled || execution.status || job.status || omitApproval.status == null
|
||||||
|
? 'disabled'
|
||||||
|
: omitApproval.status || data.data?.id !== omitApproval.userId
|
||||||
|
? 'readPretty'
|
||||||
|
: 'editable',
|
||||||
|
});
|
||||||
|
}, [data.data?.id, omitApproval.status, workflow.enabled]);
|
||||||
|
const params = useMemo(() => ({ appends: appends, ...props.params }), [appends, props.params]);
|
||||||
|
const result = useMemo(() => ({ loading: false, data: { data: omitApproval } }), [omitApproval]);
|
||||||
|
const collectionApi = useAPIClient().resource(props.collection);
|
||||||
|
const blockRequestContext = useContext(BlockRequestContext_deprecated);
|
||||||
|
const request = useMemo(
|
||||||
|
() => ({
|
||||||
|
params: params,
|
||||||
|
form: form,
|
||||||
|
field: field,
|
||||||
|
service: result,
|
||||||
|
updateAssociationValues: updateAssociationValues,
|
||||||
|
formBlockRef: ref,
|
||||||
|
}),
|
||||||
|
[field, form, params, result, updateAssociationValues],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!omitApproval.status && omitApproval.userId !== data.data?.id) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CollectionProvider_deprecated collection={props.collection}>
|
||||||
|
{/* @ts-ignore */}
|
||||||
|
<RecordProvider record={{}}>
|
||||||
|
<FormActiveFieldsProvider name="form">
|
||||||
|
<BlockRequestContext_deprecated.Provider
|
||||||
|
value={{
|
||||||
|
block: 'form',
|
||||||
|
props: props,
|
||||||
|
field: field,
|
||||||
|
service: result,
|
||||||
|
resource: collectionApi,
|
||||||
|
__parent: blockRequestContext,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FormBlockContext.Provider value={request}>
|
||||||
|
<SchemaComponent component={component} {...field.componentProps}>
|
||||||
|
<FormV2.Templates key={1} style={{ marginBottom: 18 }} form={form} />
|
||||||
|
<div key={2} ref={ref}>
|
||||||
|
<RecursionField schema={fieldSchema} onlyRenderProperties={true} />
|
||||||
|
</div>
|
||||||
|
</SchemaComponent>
|
||||||
|
</FormBlockContext.Provider>
|
||||||
|
</BlockRequestContext_deprecated.Provider>
|
||||||
|
</FormActiveFieldsProvider>
|
||||||
|
</RecordProvider>
|
||||||
|
</CollectionProvider_deprecated>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,29 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { ActionBarProvider as ClientActionBarProvider, useCompile } from '@nocobase/client';
|
||||||
|
import { str2moment } from '@nocobase/utils/client';
|
||||||
|
import { Space, Tag } from 'antd';
|
||||||
|
import { approvalStatusConfigObj } from '../../../constants';
|
||||||
|
import { useContextApprovalExecutions } from './Pd.ApprovalExecutions';
|
||||||
|
|
||||||
|
export function ActionBarProvider(props) {
|
||||||
|
const { status } = useContextApprovalExecutions();
|
||||||
|
|
||||||
|
if (status) {
|
||||||
|
return <ComponentUserInfo />;
|
||||||
|
} else {
|
||||||
|
return <ClientActionBarProvider {...props} />;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const ComponentUserInfo = () => {
|
||||||
|
const compile = useCompile();
|
||||||
|
const { status, updatedAt, user } = useContextApprovalExecutions() as any;
|
||||||
|
const configObj = approvalStatusConfigObj[status];
|
||||||
|
return (
|
||||||
|
<Space>
|
||||||
|
<Tag color={configObj.color}>{compile(configObj.label)}</Tag>
|
||||||
|
<time>{str2moment(updatedAt).format('YYYY-MM-DD HH:mm:ss')}</time>
|
||||||
|
<Tag>{user.nickname}</Tag>
|
||||||
|
</Space>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,23 @@
|
|||||||
|
import React, { useContext } from 'react';
|
||||||
|
import { createContext } from 'react';
|
||||||
|
import { useContextApprovalExecutions } from './Pd.ApprovalExecutions';
|
||||||
|
|
||||||
|
export interface ApprovalAction {
|
||||||
|
status: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ContextApprovalAction = createContext<Partial<ApprovalAction>>({});
|
||||||
|
|
||||||
|
export function useContextApprovalAction() {
|
||||||
|
return useContext(ContextApprovalAction);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ApprovalActionProvider({ children, ...props }) {
|
||||||
|
const context = useContextApprovalExecutions();
|
||||||
|
|
||||||
|
if (context && context.status !== props.status) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <ContextApprovalAction.Provider value={props}>{children}</ContextApprovalAction.Provider>;
|
||||||
|
}
|
@ -0,0 +1,22 @@
|
|||||||
|
import { createContext, useContext } from 'react';
|
||||||
|
import { Approval } from '../interface';
|
||||||
|
|
||||||
|
interface ApprovalExecutions {
|
||||||
|
id: number;
|
||||||
|
approval: Approval;
|
||||||
|
|
||||||
|
execution: any;
|
||||||
|
status: number;
|
||||||
|
|
||||||
|
snapshot: any;
|
||||||
|
records: any[];
|
||||||
|
|
||||||
|
job?: any;
|
||||||
|
userId?: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ContextApprovalExecutions = createContext<Partial<ApprovalExecutions>>({});
|
||||||
|
|
||||||
|
export function useContextApprovalExecutions() {
|
||||||
|
return useContext(ContextApprovalExecutions);
|
||||||
|
}
|
@ -0,0 +1,149 @@
|
|||||||
|
import { css } from '@nocobase/client';
|
||||||
|
import { NAMESPACE } from '../../../locale';
|
||||||
|
|
||||||
|
export const SchemaApprovalBlockTodos = {
|
||||||
|
type: 'void',
|
||||||
|
name: 'todos',
|
||||||
|
properties: {
|
||||||
|
actions: {
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'ActionBar',
|
||||||
|
'x-component-props': { style: { marginBottom: 16 } },
|
||||||
|
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' },
|
||||||
|
'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-component-props': { icon: 'ReloadOutlined' },
|
||||||
|
'x-align': 'right',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
table: {
|
||||||
|
type: 'array',
|
||||||
|
'x-component': 'TableV2',
|
||||||
|
'x-use-component-props': 'useTableBlockProps',
|
||||||
|
'x-component-props': {
|
||||||
|
rowKey: 'id',
|
||||||
|
tableLayout: 'fixed',
|
||||||
|
className: css`
|
||||||
|
.ant-table-cell {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
properties: {
|
||||||
|
action: {
|
||||||
|
type: 'void',
|
||||||
|
'x-decorator': 'TableV2.Column.Decorator',
|
||||||
|
'x-component': 'TableV2.Column',
|
||||||
|
'x-component-props': { width: 60 },
|
||||||
|
title: '{{t("Actions")}}',
|
||||||
|
properties: {
|
||||||
|
action: {
|
||||||
|
'x-component': 'ApprovalBlock.ViewActionTodos',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
approvalId: {
|
||||||
|
type: 'void',
|
||||||
|
'x-decorator': 'TableV2.Column.Decorator',
|
||||||
|
'x-component': 'TableV2.Column',
|
||||||
|
'x-component-props': {
|
||||||
|
width: 80,
|
||||||
|
},
|
||||||
|
title: '{{t("ID")}}',
|
||||||
|
properties: {
|
||||||
|
approvalId: {
|
||||||
|
type: 'number',
|
||||||
|
'x-component': 'Input',
|
||||||
|
'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,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
node: {
|
||||||
|
type: 'void',
|
||||||
|
'x-decorator': 'TableV2.Column.Decorator',
|
||||||
|
'x-component': 'TableV2.Column',
|
||||||
|
'x-component-props': {
|
||||||
|
width: null,
|
||||||
|
},
|
||||||
|
title: `{{t("Task node", { ns: "${NAMESPACE}" })}}`,
|
||||||
|
properties: {
|
||||||
|
node: {
|
||||||
|
'x-component': 'NodeColumn',
|
||||||
|
'x-read-pretty': true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
type: 'void',
|
||||||
|
'x-decorator': 'TableV2.Column.Decorator',
|
||||||
|
'x-component': 'TableV2.Column',
|
||||||
|
'x-component-props': { width: 100 },
|
||||||
|
title: '{{t("Status", { ns: "workflow" })}}',
|
||||||
|
properties: {
|
||||||
|
status: {
|
||||||
|
'x-component': 'ApprovalRecordStatusColumn',
|
||||||
|
'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,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
@ -0,0 +1,20 @@
|
|||||||
|
import { SchemaComponent } from '@nocobase/client';
|
||||||
|
import React from 'react';
|
||||||
|
import { NodeColumn } from '../../approval-common/approval-columns/column.node';
|
||||||
|
import { ApprovalRecordStatusColumn } from '../../approval-common/approval-columns/column.status';
|
||||||
|
import { UserColumn } from '../../approval-common/approval-columns/column.user';
|
||||||
|
import { WorkflowColumn } from '../../approval-common/approval-columns/column.workflow';
|
||||||
|
import { SchemaApprovalBlockTodos } from './Sm.ApprovalBlockTodos';
|
||||||
|
|
||||||
|
// 审批-待办
|
||||||
|
export const ApprovalBlockTodos = () => (
|
||||||
|
<SchemaComponent
|
||||||
|
components={{
|
||||||
|
NodeColumn,
|
||||||
|
WorkflowColumn,
|
||||||
|
UserColumn,
|
||||||
|
ApprovalRecordStatusColumn,
|
||||||
|
}}
|
||||||
|
schema={SchemaApprovalBlockTodos}
|
||||||
|
/>
|
||||||
|
);
|
@ -0,0 +1,37 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { SchemaComponent, useRecord } from '@nocobase/client';
|
||||||
|
import { ViewActionTodosContent } from './VC.ViewActionTodosContent';
|
||||||
|
|
||||||
|
// 审批-待办: 操作-查看
|
||||||
|
export const ViewActionTodos = ({ popoverComponent = 'Action.Drawer', popoverComponentProps = {} }) => {
|
||||||
|
const record = useRecord();
|
||||||
|
return (
|
||||||
|
<SchemaComponent
|
||||||
|
components={{
|
||||||
|
ViewActionTodosContent,
|
||||||
|
}}
|
||||||
|
schema={{
|
||||||
|
name: `assignee-view-${record.id}`,
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'Action.Link',
|
||||||
|
title: '{{t("View")}}',
|
||||||
|
properties: {
|
||||||
|
drawer: {
|
||||||
|
type: 'void',
|
||||||
|
'x-component': popoverComponent,
|
||||||
|
'x-component-props': {
|
||||||
|
className: 'nb-action-popup',
|
||||||
|
...popoverComponentProps,
|
||||||
|
},
|
||||||
|
properties: {
|
||||||
|
content: {
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'ViewActionTodosContent',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,150 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
import {
|
||||||
|
RemoteSchemaComponent,
|
||||||
|
SchemaComponent,
|
||||||
|
SchemaComponentProvider,
|
||||||
|
useFormBlockContext,
|
||||||
|
useRecord,
|
||||||
|
useRequest,
|
||||||
|
} from '@nocobase/client';
|
||||||
|
import { DetailsBlockProvider, ExecutionContextProvider } from '@nocobase/plugin-workflow/client';
|
||||||
|
import { Result, Spin } from 'antd';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import React, { useContext } from 'react';
|
||||||
|
import { ContextWithActionEnabled } from '../../approval-common/Pd.WithActionEnabled';
|
||||||
|
import { ContextApprovalExecution } from '../common/Pd.ApprovalExecution';
|
||||||
|
import { ContextApprovalExecutions } from './Pd.ApprovalExecutions';
|
||||||
|
import { NAMESPACE, useTranslation } from '../../../locale';
|
||||||
|
import { FormBlockProvider } from '../../../common/Pd.FormBlock';
|
||||||
|
import { ApprovalContext } from '../../approval-common/Pd.ApprovalData';
|
||||||
|
import { ApprovalCommon } from '../../approval-common/map';
|
||||||
|
import { SchemaComponentContextProvider } from '../common/Pd.SchemaComponent';
|
||||||
|
import { useApprovalDetailBlockProps } from './hooks/useApprovalDetailBlockProps';
|
||||||
|
import { useApprovalFormBlockProps } from './hooks/useApprovalFormBlockProps';
|
||||||
|
import { useSubmit } from './hooks/useSubmit';
|
||||||
|
import { ApprovalFormBlockDecorator } from './Dt.ApprovalFormBlock';
|
||||||
|
import { ActionBarProvider } from './Pd.ActionBarProvider';
|
||||||
|
import { ApprovalActionProvider } from './Pd.ApprovalAction';
|
||||||
|
// 审批-待办-查看: 内容
|
||||||
|
export const ViewActionTodosContent = () => {
|
||||||
|
const { id } = useRecord();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { actionEnabled } = useContext(ContextWithActionEnabled);
|
||||||
|
const { loading, data } = useRequest(
|
||||||
|
{
|
||||||
|
resource: 'approvalRecords',
|
||||||
|
action: 'get',
|
||||||
|
params: {
|
||||||
|
filterByTk: id,
|
||||||
|
appends: [
|
||||||
|
'approvalExecution',
|
||||||
|
'node',
|
||||||
|
'job',
|
||||||
|
'workflow',
|
||||||
|
'workflow.nodes',
|
||||||
|
'execution',
|
||||||
|
'execution.jobs',
|
||||||
|
'user',
|
||||||
|
'approval',
|
||||||
|
'approval.createdBy',
|
||||||
|
'approval.approvalExecutions',
|
||||||
|
'approval.createdBy.nickname',
|
||||||
|
'approval.records',
|
||||||
|
'approval.records.node.title',
|
||||||
|
'approval.records.node.config',
|
||||||
|
'approval.records.job',
|
||||||
|
'approval.records.user.nickname',
|
||||||
|
],
|
||||||
|
except: [
|
||||||
|
'approval.data',
|
||||||
|
'approval.approvalExecutions.snapshot',
|
||||||
|
'approval.records.snapshot',
|
||||||
|
'workflow.config',
|
||||||
|
'workflow.options',
|
||||||
|
'nodes.config',
|
||||||
|
],
|
||||||
|
sort: ['-createdAt'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ refreshDeps: [id] },
|
||||||
|
);
|
||||||
|
if (loading) return <Spin />;
|
||||||
|
if (!(data != null && data.data))
|
||||||
|
return <Result status="error" title={t('Submission may be withdrawn, please try refresh the list.')} />;
|
||||||
|
const items = data.data;
|
||||||
|
const { approvalExecution, node, approval, workflow } = items;
|
||||||
|
const { nodes } = workflow;
|
||||||
|
const omitWorkflow = _.omit(workflow, ['nodes']);
|
||||||
|
const { execution } = items;
|
||||||
|
// return null;
|
||||||
|
return (
|
||||||
|
<ExecutionContextProvider workflow={omitWorkflow} nodes={nodes} execution={execution}>
|
||||||
|
<ApprovalContext.Provider value={approval}>
|
||||||
|
<ContextApprovalExecution.Provider value={approvalExecution}>
|
||||||
|
<ContextApprovalExecutions.Provider value={data.data}>
|
||||||
|
<SchemaComponent
|
||||||
|
components={{
|
||||||
|
SchemaComponentProvider: SchemaComponentProvider,
|
||||||
|
RemoteSchemaComponent: RemoteSchemaComponent,
|
||||||
|
SchemaComponentContextProvider: SchemaComponentContextProvider,
|
||||||
|
FormBlockProvider,
|
||||||
|
ActionBarProvider,
|
||||||
|
ApprovalActionProvider,
|
||||||
|
ApprovalFormBlockProvider: ApprovalFormBlockDecorator,
|
||||||
|
DetailsBlockProvider,
|
||||||
|
}}
|
||||||
|
scope={{
|
||||||
|
useApprovalDetailBlockProps,
|
||||||
|
useApprovalFormBlockProps,
|
||||||
|
useDetailsBlockProps: useFormBlockContext,
|
||||||
|
useSubmit,
|
||||||
|
}}
|
||||||
|
schema={{
|
||||||
|
name: `content-${id}`,
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'Tabs',
|
||||||
|
properties: Object.assign(
|
||||||
|
{
|
||||||
|
detail: {
|
||||||
|
type: 'void',
|
||||||
|
title: `{{t('Approval', { ns: '${NAMESPACE}' })}}`,
|
||||||
|
'x-component': 'Tabs.TabPane',
|
||||||
|
properties: {
|
||||||
|
detail: {
|
||||||
|
type: 'void',
|
||||||
|
'x-decorator': 'SchemaComponentContextProvider',
|
||||||
|
'x-decorator-props': { designable: false },
|
||||||
|
'x-component': 'RemoteSchemaComponent',
|
||||||
|
'x-component-props': {
|
||||||
|
uid: node?.config.applyDetail,
|
||||||
|
noForm: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
actionEnabled
|
||||||
|
? {}
|
||||||
|
: {
|
||||||
|
history: {
|
||||||
|
type: 'void',
|
||||||
|
title: `{{t('Approval process', { ns: '${NAMESPACE}' })}}`,
|
||||||
|
'x-component': 'Tabs.TabPane',
|
||||||
|
properties: {
|
||||||
|
history: {
|
||||||
|
type: 'void',
|
||||||
|
'x-decorator': 'CardItem',
|
||||||
|
'x-component': ApprovalCommon.ViewComponent.ApprovalProcess,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</ContextApprovalExecutions.Provider>
|
||||||
|
</ContextApprovalExecution.Provider>
|
||||||
|
</ApprovalContext.Provider>
|
||||||
|
</ExecutionContextProvider>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,14 @@
|
|||||||
|
import { useFormBlockContext } from '@nocobase/client';
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
import { useContextApprovalExecutions } from '../Pd.ApprovalExecutions';
|
||||||
|
|
||||||
|
export function useApprovalDetailBlockProps() {
|
||||||
|
const { snapshot } = useContextApprovalExecutions();
|
||||||
|
const { form } = useFormBlockContext();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
form.setValues(snapshot);
|
||||||
|
}, [form, snapshot]);
|
||||||
|
|
||||||
|
return { form };
|
||||||
|
}
|
@ -0,0 +1,6 @@
|
|||||||
|
import { useFormBlockContext } from '@nocobase/client';
|
||||||
|
|
||||||
|
export function useApprovalFormBlockProps() {
|
||||||
|
const { form } = useFormBlockContext();
|
||||||
|
return { form };
|
||||||
|
}
|
@ -0,0 +1,35 @@
|
|||||||
|
import { useAPIClient, useActionContext } from '@nocobase/client';
|
||||||
|
import { useField, useForm } from '@nocobase/schema';
|
||||||
|
import { useContextApprovalAction } from '../Pd.ApprovalAction';
|
||||||
|
import { useContextApprovalExecutions } from '../Pd.ApprovalExecutions';
|
||||||
|
|
||||||
|
export function useSubmit() {
|
||||||
|
const field = useField();
|
||||||
|
const api = useAPIClient();
|
||||||
|
const form = useForm();
|
||||||
|
const approvalExecutions = useContextApprovalExecutions();
|
||||||
|
const { status } = useContextApprovalAction();
|
||||||
|
const { setVisible, setSubmitted } = useActionContext() as any;
|
||||||
|
return {
|
||||||
|
run() {
|
||||||
|
return async () => {
|
||||||
|
try {
|
||||||
|
if (form.values.status) return;
|
||||||
|
await form.submit(), (field.data = field.data ?? {});
|
||||||
|
field.data.loading = true;
|
||||||
|
await api.resource('approvalRecords').submit({
|
||||||
|
filterByTk: approvalExecutions.id,
|
||||||
|
values: { ...form.values, status },
|
||||||
|
});
|
||||||
|
field.data.loading = false;
|
||||||
|
await form.reset();
|
||||||
|
setSubmitted(true);
|
||||||
|
setVisible(false);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
field.data && (field.data.loading = false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
@ -0,0 +1,63 @@
|
|||||||
|
import { NAMESPACE } from '../../locale';
|
||||||
|
import { ApprovalStatusEnums } from '../../constants';
|
||||||
|
|
||||||
|
export const CollectionApprovals = {
|
||||||
|
title: `{{t("Approval applications", { ns: "${NAMESPACE}" })}}`,
|
||||||
|
name: 'approvals',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
type: 'bigInt',
|
||||||
|
name: 'id',
|
||||||
|
interface: 'number',
|
||||||
|
uiSchema: { type: 'number', title: 'ID', 'x-component': 'InputNumber' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'belongsTo',
|
||||||
|
name: 'workflow',
|
||||||
|
target: 'workflows',
|
||||||
|
foreignKey: 'workflowId',
|
||||||
|
interface: 'm2o',
|
||||||
|
uiSchema: {
|
||||||
|
type: 'number',
|
||||||
|
title: '{{t("Workflow", { ns: "workflow" })}}',
|
||||||
|
'x-component': 'RemoteSelect',
|
||||||
|
'x-component-props': { fieldNames: { label: 'title', value: 'id' }, service: { resource: 'workflows' } },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'belongsTo',
|
||||||
|
name: 'createdBy',
|
||||||
|
target: 'users',
|
||||||
|
foreignKey: 'createdById',
|
||||||
|
interface: 'm2o',
|
||||||
|
uiSchema: {
|
||||||
|
type: 'number',
|
||||||
|
title: `{{t("Initiator", { ns: "${NAMESPACE}" })}}`,
|
||||||
|
'x-component': 'RemoteSelect',
|
||||||
|
'x-component-props': { fieldNames: { label: 'nickname', value: 'id' }, service: { resource: 'users' } },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'integer',
|
||||||
|
name: 'status',
|
||||||
|
interface: 'select',
|
||||||
|
uiSchema: {
|
||||||
|
type: 'number',
|
||||||
|
title: '{{t("Status", { ns: "workflow" })}}',
|
||||||
|
'x-component': 'Select',
|
||||||
|
enum: ApprovalStatusEnums,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'createdAt',
|
||||||
|
type: 'date',
|
||||||
|
interface: 'createdAt',
|
||||||
|
uiSchema: {
|
||||||
|
type: 'datetime',
|
||||||
|
title: '{{t("Created at")}}',
|
||||||
|
'x-component': 'DatePicker',
|
||||||
|
'x-component-props': { showTime: true },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
@ -0,0 +1,28 @@
|
|||||||
|
import { NAMESPACE } from '../../locale';
|
||||||
|
|
||||||
|
export const CollectionFlowNodes = {
|
||||||
|
title: `{{t("Node", { 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' },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
@ -0,0 +1,28 @@
|
|||||||
|
import { NAMESPACE } from '../../locale';
|
||||||
|
|
||||||
|
export const CollectionWorkflows = {
|
||||||
|
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 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'boolean',
|
||||||
|
name: 'enabled',
|
||||||
|
interface: 'select',
|
||||||
|
uiSchema: {
|
||||||
|
type: 'boolean',
|
||||||
|
title: '{{t("Status", { ns: "workflow" })}}',
|
||||||
|
'x-component': 'Select',
|
||||||
|
enum: [
|
||||||
|
{ label: '{{t("On", { ns: "workflow" })}}', value: true },
|
||||||
|
{ label: '{{t("Off", { ns: "workflow" })}}', value: false },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
@ -0,0 +1,14 @@
|
|||||||
|
import { useRecord } from '@nocobase/client';
|
||||||
|
import React, { createContext, useContext } from 'react';
|
||||||
|
import { Approval } from '../approval-block/interface';
|
||||||
|
|
||||||
|
export const ApprovalContext = createContext<Partial<Approval>>({});
|
||||||
|
|
||||||
|
export function useApproval() {
|
||||||
|
return useContext(ApprovalContext);
|
||||||
|
}
|
||||||
|
export function ApprovalDataProvider(props) {
|
||||||
|
const { useData = useRecord } = props;
|
||||||
|
const recordData = useData();
|
||||||
|
return <ApprovalContext.Provider value={recordData}>{props.children}</ApprovalContext.Provider>;
|
||||||
|
}
|
@ -0,0 +1,3 @@
|
|||||||
|
import { createContext } from 'react';
|
||||||
|
|
||||||
|
export const ContextWithActionEnabled = createContext({ actionEnabled: false });
|
@ -0,0 +1,116 @@
|
|||||||
|
import { createStyles, useCurrentUserContext } from '@nocobase/client';
|
||||||
|
import { EXECUTION_STATUS } from '@nocobase/plugin-workflow/client';
|
||||||
|
import { Space, Table } from 'antd';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import React, { useMemo } from 'react';
|
||||||
|
import { useApproval } from './Pd.ApprovalData';
|
||||||
|
import { APPROVAL_ACTION_STATUS, APPROVAL_STATUS } from '../../constants';
|
||||||
|
import { ContextWithActionEnabled } from './Pd.WithActionEnabled';
|
||||||
|
import { usePluginTranslation, useTranslation } from '../../locale';
|
||||||
|
import { getAntdTableColumns } from './process-columns/columns';
|
||||||
|
|
||||||
|
// 审批(发起/待办)区块-查看-审批处理
|
||||||
|
export const ApprovalProcess = (props) => {
|
||||||
|
const { t } = usePluginTranslation();
|
||||||
|
const approvalContext = useApproval();
|
||||||
|
const { styles } = getStyles();
|
||||||
|
const { data } = useCurrentUserContext();
|
||||||
|
|
||||||
|
const results = useMemo(() => getResults({ approval: approvalContext, currentUser: data }), [approvalContext, data]);
|
||||||
|
|
||||||
|
const columns = useMemo(() => getAntdTableColumns({ t, styles }), [t, styles]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ContextWithActionEnabled.Provider value={{ actionEnabled: props.actionEnabled }}>
|
||||||
|
<Space direction="vertical" size="middle" className={styles.layout}>
|
||||||
|
{results.map((item) => (
|
||||||
|
<Table key={item.id} dataSource={item.records} rowKey={'id'} pagination={false} columns={columns} />
|
||||||
|
))}
|
||||||
|
</Space>
|
||||||
|
</ContextWithActionEnabled.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStyles = createStyles(({ css, token }) => ({
|
||||||
|
layout: css`
|
||||||
|
display: flex;
|
||||||
|
`,
|
||||||
|
columnDetail: css`
|
||||||
|
.ant-description-textarea {
|
||||||
|
margin-bottom: 0.5em;
|
||||||
|
}
|
||||||
|
time {
|
||||||
|
display: block;
|
||||||
|
color: ${token.colorTextTertiary};
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
}));
|
||||||
|
|
||||||
|
function getResults({ approval, currentUser }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { workflow, approvalExecutions, records } = approval;
|
||||||
|
|
||||||
|
approvalExecutions.sort((a, b) => Date.parse(a.createdAt) - Date.parse(b.createdAt));
|
||||||
|
|
||||||
|
const approvalExecution = approvalExecutions.reduce(
|
||||||
|
(newObj, curr) =>
|
||||||
|
Object.assign(newObj, {
|
||||||
|
[curr.id]: Object.assign(curr, {
|
||||||
|
records: [
|
||||||
|
{
|
||||||
|
groupCount: 1,
|
||||||
|
node: { title: t('Apply') },
|
||||||
|
user: {
|
||||||
|
...approval.createdBy,
|
||||||
|
id: approval.createdById,
|
||||||
|
},
|
||||||
|
status: curr.status ? APPROVAL_STATUS.SUBMITTED : approval.status,
|
||||||
|
updatedAt: curr.createdAt,
|
||||||
|
execution: { ...curr },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
records
|
||||||
|
.sort((prevRecord, nextRecord) => {
|
||||||
|
const prev = new Date(prevRecord.job?.createdAt);
|
||||||
|
const next = new Date(nextRecord.job?.createdAt);
|
||||||
|
return prev < next ? -1 : prev > next ? 1 : prevRecord.id - nextRecord.id;
|
||||||
|
})
|
||||||
|
.forEach((record) => {
|
||||||
|
const approvalExecutionId = approvalExecution[record.approvalExecutionId];
|
||||||
|
const omitApprovalExecutionId = _.omit(approvalExecutionId, ['records']);
|
||||||
|
(record.workflow = workflow),
|
||||||
|
(record.execution = { ...omitApprovalExecutionId }),
|
||||||
|
approvalExecutionId.records.push(record),
|
||||||
|
approvalExecutionId.jobs || (approvalExecutionId.jobs = {}),
|
||||||
|
approvalExecutionId.jobs[record.jobId]
|
||||||
|
? (approvalExecutionId.jobs[record.jobId].first.groupCount += 1)
|
||||||
|
: ((approvalExecutionId.jobs[record.jobId] = { first: record }),
|
||||||
|
(record.groupCount = 1),
|
||||||
|
(record.statusCount = { [APPROVAL_ACTION_STATUS.APPROVED]: 0, [APPROVAL_ACTION_STATUS.REJECTED]: 0 })),
|
||||||
|
[APPROVAL_ACTION_STATUS.APPROVED, APPROVAL_ACTION_STATUS.REJECTED].includes(record.status) &&
|
||||||
|
(approvalExecutionId.jobs[record.jobId].first.statusCount[record.status] += 1);
|
||||||
|
}),
|
||||||
|
approval.createdById === (currentUser == null ? void 0 : currentUser.data.id) &&
|
||||||
|
approvalExecutions.forEach((approvalExecution) => {
|
||||||
|
approvalExecution.status === EXECUTION_STATUS.CANCELED &&
|
||||||
|
approvalExecution.records.length === 1 &&
|
||||||
|
((approvalExecution.records[0].groupCount = 2),
|
||||||
|
approvalExecution.records.push({
|
||||||
|
user: { nickname: approval.createdBy.nickname },
|
||||||
|
status: APPROVAL_ACTION_STATUS.WITHDRAWN,
|
||||||
|
updatedAt: approvalExecution.updatedAt,
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
const aELength = approvalExecutions.length;
|
||||||
|
|
||||||
|
return approvalExecutions.filter(
|
||||||
|
(approvalExecution, index) =>
|
||||||
|
(aELength - 1 === index &&
|
||||||
|
(!approvalExecution.status || approvalExecution.status === EXECUTION_STATUS.CANCELED)) ||
|
||||||
|
approvalExecution.records.length > 1,
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,17 @@
|
|||||||
|
import { observer, useField, Field } from '@nocobase/schema';
|
||||||
|
|
||||||
|
export const NodeColumn = observer(
|
||||||
|
() => {
|
||||||
|
const field = useField<Field>();
|
||||||
|
const fieldValue = field?.value;
|
||||||
|
|
||||||
|
if (fieldValue?.title) {
|
||||||
|
return fieldValue.title;
|
||||||
|
} else if (fieldValue?.id) {
|
||||||
|
return `#${fieldValue.id}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
},
|
||||||
|
{ displayName: 'NodeColumn' },
|
||||||
|
);
|
@ -0,0 +1,33 @@
|
|||||||
|
import { useCompile, useRecord } from '@nocobase/client';
|
||||||
|
import { Field, observer, useField } from '@nocobase/schema';
|
||||||
|
import { Tag } from 'antd';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import React from 'react';
|
||||||
|
import { APPROVAL_ACTION_STATUS, approvalStatusConfigObj } from '../../../constants';
|
||||||
|
import { useTranslation } from '../../../locale';
|
||||||
|
|
||||||
|
export const ApprovalRecordStatusColumn = observer(
|
||||||
|
() => {
|
||||||
|
const { value } = useField<Field>();
|
||||||
|
const record = useRecord();
|
||||||
|
return <ColumnStatusComponent value={value} record={record} />;
|
||||||
|
},
|
||||||
|
{ displayName: 'ApprovalRecordStatusColumn' },
|
||||||
|
);
|
||||||
|
|
||||||
|
// 审批-待办:状态列:值
|
||||||
|
export const ColumnStatusComponent = (props) => {
|
||||||
|
const { value, record } = props;
|
||||||
|
const omitProps = _.omit(props, ['value', 'record']);
|
||||||
|
const compile = useCompile();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { option = approvalStatusConfigObj[value] } = omitProps;
|
||||||
|
const { workflow, execution, job } = record ?? {};
|
||||||
|
|
||||||
|
return (!(workflow != null && workflow.enabled) || (execution != null && execution.stauts) || job?.status) &&
|
||||||
|
[APPROVAL_ACTION_STATUS.ASSIGNED, APPROVAL_ACTION_STATUS.PENDING].includes(value) ? (
|
||||||
|
<Tag>{t('Unprocessed')}</Tag>
|
||||||
|
) : (
|
||||||
|
<Tag color={option.color}>{compile(option.label)}</Tag>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,16 @@
|
|||||||
|
import { observer, useField, Field } from '@nocobase/schema';
|
||||||
|
|
||||||
|
export const UserColumn = observer(
|
||||||
|
() => {
|
||||||
|
const field = useField<Field>();
|
||||||
|
const fieldValue = field?.value;
|
||||||
|
|
||||||
|
if (fieldValue?.nickname) {
|
||||||
|
return fieldValue.nickname;
|
||||||
|
} else if (fieldValue?.id !== undefined) {
|
||||||
|
return fieldValue.id;
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
},
|
||||||
|
{ displayName: 'UserColumn' },
|
||||||
|
);
|
@ -0,0 +1,28 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { css } from '@nocobase/client';
|
||||||
|
import { observer, useField, Field } from '@nocobase/schema';
|
||||||
|
import { useTranslation } from '../../../locale';
|
||||||
|
|
||||||
|
export const WorkflowColumn = observer(
|
||||||
|
() => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { value } = useField<Field>();
|
||||||
|
const title = value?.title || `#${value?.id}`;
|
||||||
|
|
||||||
|
if (value?.enabled) {
|
||||||
|
return title;
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
className={css`
|
||||||
|
text-decoration: line-through;
|
||||||
|
`}
|
||||||
|
title={t('Disabled')}
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ displayName: 'WorkflowColumn' },
|
||||||
|
);
|
@ -0,0 +1,12 @@
|
|||||||
|
import { Plugin } from '@nocobase/client';
|
||||||
|
import { ApprovalCommon } from './map';
|
||||||
|
|
||||||
|
export default class PluginKitApprovalCommon extends Plugin {
|
||||||
|
async afterAdd() {}
|
||||||
|
async beforeLoad() {}
|
||||||
|
async load() {
|
||||||
|
this.app.addComponents({
|
||||||
|
ApprovalCommon,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,26 @@
|
|||||||
|
import { ApprovalDataProvider } from './Pd.ApprovalData';
|
||||||
|
import { ApprovalProcess } from './VC.ApprovalProcess';
|
||||||
|
|
||||||
|
// 通用,审批
|
||||||
|
export const ApprovalCommon = () => null;
|
||||||
|
|
||||||
|
ApprovalCommon.PathNameMap = {
|
||||||
|
Provider: {
|
||||||
|
ApprovalDataProvider: 'ApprovalCommon.Provider.ApprovalDataProvider',
|
||||||
|
},
|
||||||
|
ViewComponent: {
|
||||||
|
ApprovalProcess: 'ApprovalCommon.ViewComponent.ApprovalProcess',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
ApprovalCommon.Decorator = null;
|
||||||
|
|
||||||
|
ApprovalCommon.Provider = {
|
||||||
|
ApprovalDataProvider,
|
||||||
|
};
|
||||||
|
|
||||||
|
ApprovalCommon.BlockInitializer = null;
|
||||||
|
|
||||||
|
ApprovalCommon.ViewComponent = {
|
||||||
|
ApprovalProcess,
|
||||||
|
};
|
@ -0,0 +1,36 @@
|
|||||||
|
import { renderColumnDetail } from './render.detail';
|
||||||
|
import { renderColumnStatus } from './render.status';
|
||||||
|
import { renderColumnTaskNode } from './render.taskNode';
|
||||||
|
|
||||||
|
export const getAntdTableColumns = ({ t, styles }) => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
title: t('Task node'),
|
||||||
|
dataIndex: ['node', 'title'],
|
||||||
|
onCell(taskNode) {
|
||||||
|
return {
|
||||||
|
rowSpan: taskNode.groupCount ?? 0,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
render: renderColumnTaskNode,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t('User'),
|
||||||
|
dataIndex: ['user', 'nickname'],
|
||||||
|
width: 120,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t('Status', { ns: 'workflow' }),
|
||||||
|
dataIndex: 'status',
|
||||||
|
render: renderColumnStatus,
|
||||||
|
width: 120,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t('Details'),
|
||||||
|
dataIndex: 'updatedAt',
|
||||||
|
render: renderColumnDetail,
|
||||||
|
width: 200,
|
||||||
|
className: styles.columnDetail,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
};
|
@ -0,0 +1,51 @@
|
|||||||
|
import { DatePicker, InputReadPretty, RecordProvider, useCurrentUserContext } from '@nocobase/client';
|
||||||
|
import React, { useContext } from 'react';
|
||||||
|
import { ContextWithActionEnabled } from '../Pd.WithActionEnabled';
|
||||||
|
import { ViewActionLaunch } from '../../approval-block/launch/VC.ViewActionLaunch';
|
||||||
|
import { ViewActionTodos } from '../../approval-block/todos/VC.ViewActionTodos';
|
||||||
|
|
||||||
|
// Component开头的是,不知道起什么名字合适的
|
||||||
|
export function renderColumnDetail(e, props, exist) {
|
||||||
|
return exist ? <ColumnDetailExist {...props} /> : <ColumnDetailNotExist {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exist
|
||||||
|
const ColumnDetailExist = (props) => {
|
||||||
|
const { data } = useCurrentUserContext();
|
||||||
|
const { actionEnabled } = useContext(ContextWithActionEnabled);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{props.status ? (
|
||||||
|
<>
|
||||||
|
{[
|
||||||
|
props.comment ? <InputReadPretty.TextArea value={props.comment} /> : null,
|
||||||
|
<time key="time">{<DatePicker.ReadPretty value={props.updatedAt} showTime={true} />}</time>,
|
||||||
|
]}
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
{actionEnabled && props.userId === data?.data.id ? (
|
||||||
|
// @ts-ignore
|
||||||
|
<RecordProvider record={props} parent={false}>
|
||||||
|
<ViewActionTodos />
|
||||||
|
</RecordProvider>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Not Exist
|
||||||
|
const ColumnDetailNotExist = (props) => {
|
||||||
|
const { data } = useCurrentUserContext();
|
||||||
|
const { actionEnabled } = useContext(ContextWithActionEnabled);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<time key={'time'}>{<DatePicker.ReadPretty value={props.updatedAt} showTime={true} />}</time>
|
||||||
|
{actionEnabled && props.user.id === data?.data.id ? (
|
||||||
|
// @ts-ignore
|
||||||
|
<RecordProvider record={props.execution} parent={false}>
|
||||||
|
<ViewActionLaunch />
|
||||||
|
</RecordProvider>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,22 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { useCompile } from '@nocobase/client';
|
||||||
|
import { Tag } from 'antd';
|
||||||
|
import { approvalStatusConfigObj } from '../../../constants';
|
||||||
|
import { ColumnStatusComponent } from '../approval-columns/column.status';
|
||||||
|
import { ApprovalStatusEnumDict } from '../../../constants';
|
||||||
|
import { APPROVAL_STATUS } from '../../../constants';
|
||||||
|
|
||||||
|
export function renderColumnStatus(value, record, exist) {
|
||||||
|
// return null;
|
||||||
|
const compile = useCompile();
|
||||||
|
if (!exist) {
|
||||||
|
const approvalStatusItem =
|
||||||
|
ApprovalStatusEnumDict[
|
||||||
|
record.status === APPROVAL_STATUS.DRAFT ? APPROVAL_STATUS.DRAFT : APPROVAL_STATUS.SUBMITTED
|
||||||
|
];
|
||||||
|
return <Tag color={approvalStatusItem.color}>{compile(approvalStatusItem.label)}</Tag>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const option = approvalStatusConfigObj[value];
|
||||||
|
return <ColumnStatusComponent value={value} record={record} option={option} />;
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user