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:
bai.zixv 2024-04-23 20:51:33 +08:00 committed by sealday
parent 090db400a4
commit 1b59cb2a6b
111 changed files with 4201 additions and 2702 deletions

View File

@ -6,7 +6,9 @@ import { useActionContext } from './hooks';
export const ActionContext = createContext<ActionContextProps>({});
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();
return (
<ActionContext.Provider value={{ ...contextProps, ...props, ...props?.value }}>

View File

@ -1,5 +1,5 @@
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';
export const useSchemaOptionsContext = () => {
@ -7,7 +7,7 @@ export const useSchemaOptionsContext = () => {
return options || {};
};
export const SchemaComponentOptions: React.FC<ISchemaComponentOptionsProps> = memo((props) => {
export const SchemaComponentOptions: React.FC<PropsWithChildren<ISchemaComponentOptionsProps>> = memo((props) => {
const { children } = props;
const options = useSchemaOptionsContext();
const components = useMemo(() => {

View File

@ -1,7 +1,7 @@
import { createForm } from '@nocobase/schema';
import { FormProvider, Schema } 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 { SchemaComponentContext } from '../context';
import { ISchemaComponentProvider } from '../types';
@ -42,7 +42,7 @@ const Registry = {
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 [uidValue, setUid] = useState(uid());
const [formId, setFormId] = useState(uid());

View File

@ -3,6 +3,9 @@
"displayName": "Workflow: Approval Center",
"version": "0.1.13",
"description": "Can configure a universal approval process to complete the complete approval process in the approval center.",
"keywords": [
"Workflow"
],
"main": "dist/server/index.js",
"devDependencies": {
"@ant-design/icons": "5.x",

View File

@ -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,
},
],
});

View File

@ -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' }],
},
],
});

View File

@ -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)
);
}
}

View File

@ -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,
},
],
});

View File

@ -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' }],
},
],
});

View File

@ -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 },
},
},
],
};

View File

@ -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>
);
};

View File

@ -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() {}
}

View File

@ -0,0 +1,3 @@
import { createContext } from 'react';
export const ContextApproverConfig = createContext({}) as any;

View File

@ -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>
}
/>
));
};

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -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>}
</>
);
};

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -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,
},
],
});

View File

@ -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',
},
],
},
],
});

View File

@ -0,0 +1,3 @@
export const ActionBarProvider = (props) => {
return props.children;
};

View File

@ -0,0 +1,3 @@
export const ApprovalActionProvider = (props) => {
return props.children;
};

View File

@ -0,0 +1,3 @@
import { createContext } from 'react';
export const ContextApproverBlock = createContext({});

View File

@ -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}`,
}}
/>
);
};

View File

@ -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>
);
};

View File

@ -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} />;
};

View File

@ -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} />;
};

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -0,0 +1,6 @@
import { useFormBlockContext } from '@nocobase/client';
export function useApprovalFormBlockProps(e) {
const { form } = useFormBlockContext();
return { form };
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -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>
</>
);
}

View File

@ -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);
}
}

View File

@ -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,
},
],
});

View File

@ -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',
},
],
},
],
});

View File

@ -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',
},
},
},
},
});

View File

@ -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: {},
},
}}
/>
);
};

View File

@ -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} />;
};

View File

@ -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}
/>
);
};

View File

@ -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());
});

View File

@ -6,20 +6,22 @@ import {
useWorkflowAnyExecuted,
} from '@nocobase/plugin-workflow/client';
import { useForm } from '@nocobase/schema';
import { NAMESPACE } from '../locale';
import { T, Vo, zo, useTranslation, w, x } from './refined';
import { usePluginTranslation } from '../locale';
import { SchemaConfigButton } from './VC.ConfigButton';
import { NAMESPACE, usePluginTranslation } from '../../locale';
import { LauncherInterface } from './launcher-interface/VC.LauncherInterface';
// 工作流节点-审批触发器节点
export class ApprovalTrigger extends Trigger {
sync = false;
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}" })}}`;
// 触发器配置表
fieldset = {
collection: {
type: 'string',
title: '{{t("Collection")}}',
required: !0,
required: true,
'x-decorator': 'FormItem',
'x-component': 'DataSourceCollectionCascader',
'x-disabled': '{{ useWorkflowAnyExecuted() }}',
@ -34,17 +36,17 @@ export class ApprovalTrigger extends Trigger {
options: [
{
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}" })}}`,
},
{
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}" })}}`,
},
],
},
default: !1,
default: false,
},
withdrawable: {
type: 'boolean',
@ -59,8 +61,23 @@ export class ApprovalTrigger extends Trigger {
description: `{{t("For initiating approvals, or viewing and manipulating initiated approvals.", { ns: "${NAMESPACE}" })}}`,
'x-decorator': 'FormItem',
'x-component': 'SchemaConfigButton',
'x-reactions': [{ dependencies: ['collection'], fulfill: { state: { visible: '{{!!$deps[0]}}' } } }],
properties: { applyForm: { type: 'void', 'x-component': 'SchemaConfig', default: null } },
'x-reactions': [
{
dependencies: ['collection'],
fulfill: {
state: {
visible: '{{!!$deps[0]}}',
},
},
},
],
properties: {
applyForm: {
type: 'void',
'x-component': 'SchemaInitiatorForm',
default: null,
},
},
},
appends: {
type: 'array',
@ -71,40 +88,59 @@ export class ApprovalTrigger extends Trigger {
'x-component': 'AppendsTreeSelect',
'x-component-props': {
title: 'Preload associations',
multiple: !0,
multiple: true,
useCollection() {
const { values: n } = useForm();
return n == null ? void 0 : n.collection;
const { values } = useForm();
return values?.collection;
},
},
'x-reactions': [{ dependencies: ['collection'], fulfill: { state: { visible: '{{!!$deps[0]}}' } } }],
'x-reactions': [
{
dependencies: ['collection'],
fulfill: {
state: {
visible: '{{!!$deps[0]}}',
},
},
},
],
},
};
scope = { useWorkflowAnyExecuted: useWorkflowAnyExecuted };
components = { SchemaConfigButton: Vo, SchemaConfig: zo, RadioWithTooltip: RadioWithTooltip };
isActionTriggerable = (n, a) => ['create', 'update'].includes(a.action) && !a.direct;
scope = { useWorkflowAnyExecuted };
components = {
SchemaConfigButton,
SchemaInitiatorForm: LauncherInterface,
RadioWithTooltip,
};
useVariables(n, a) {
var m;
const s = useCompile(),
{ getCollectionFields: i } = useCollectionManager_deprecated(),
{ t: v } = usePluginTranslation(),
l = [
isActionTriggerable = (config, context) => {
return ['create', 'update'].includes(context.action) && !context.direct;
};
useVariables(config, options) {
// eslint-disable-next-line react-hooks/rules-of-hooks
const compile = useCompile();
// eslint-disable-next-line react-hooks/rules-of-hooks
const { getCollectionFields } = useCollectionManager_deprecated();
// eslint-disable-next-line react-hooks/rules-of-hooks
const { t } = usePluginTranslation();
const rootFields = [
{
collectionName: n.collection,
collectionName: config.collection,
name: 'data',
type: 'hasOne',
target: n.collection,
uiSchema: { title: v('Trigger data', { ns: 'workflow' }) },
target: config.collection,
uiSchema: { title: t('Trigger data', { ns: 'workflow' }) },
},
];
return getCollectionFieldOptions(
w(x({ appends: ['data', ...(((m = n.appends) == null ? void 0 : m.map((h) => `data.${h}`)) || [])] }, a), {
fields: l,
compile: s,
getCollectionFields: i,
}),
);
return getCollectionFieldOptions({
// depth,
appends: ['data', ...(config.appends?.map((item) => `data.${item}`) || [])],
...options,
fields: rootFields,
compile,
getCollectionFields,
});
}
}

View File

@ -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;
}

View File

@ -1,40 +1,13 @@
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 { NAMESPACE } from '../locale';
import { ApprovalBlock } from './refined';
import PluginKitApprovalConfiguration from './configuration';
import PluginKitApprovalUsage from './usage';
export default class ApprovalPlugin extends Plugin {
async afterAdd() {}
export default class PluginApproval extends Plugin {
async afterAdd() {
this.pm.add(PluginKitApprovalConfiguration);
this.pm.add(PluginKitApprovalUsage);
}
async beforeLoad() {}
async load() {
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 load() {}
}

View 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

View File

@ -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>
);
}

View File

@ -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'],
},
},
});
}}
/>
);
};

View File

@ -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);
}

View File

@ -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>;
}

View File

@ -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>
);
}

View File

@ -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',
});
}
}

View File

@ -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;
}

View File

@ -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>
);
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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,
},
},
},
},
},
},
};

View File

@ -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}
/>
);
};

View File

@ -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',
}),
},
},
},
}}
/>
);
};

View File

@ -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>
);
};

View File

@ -0,0 +1,3 @@
export function ActionBarProvider(props) {
return props.children;
}

View File

@ -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>;
}

View File

@ -0,0 +1,3 @@
export function WithdrawActionProvider() {
return null;
}

View File

@ -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>
);
};

View File

@ -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);
}
};
},
};
}

View File

@ -0,0 +1,3 @@
export function useWithdrawAction() {
return { run() {} };
}

View File

@ -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,
};
}

View File

@ -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 };
}

View File

@ -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,
};
}

View File

@ -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;
}
}
};
},
};
}

View File

@ -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;

View File

@ -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>
);
}

View File

@ -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>
);
};

View File

@ -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>;
}

View File

@ -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);
}

View File

@ -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,
},
},
},
},
},
},
};

View File

@ -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}
/>
);

View File

@ -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',
},
},
},
},
}}
/>
);
};

View File

@ -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>
);
};

View File

@ -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 };
}

View File

@ -0,0 +1,6 @@
import { useFormBlockContext } from '@nocobase/client';
export function useApprovalFormBlockProps() {
const { form } = useFormBlockContext();
return { form };
}

View File

@ -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);
}
};
},
};
}

View File

@ -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 },
},
},
],
};

View File

@ -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' },
},
],
};

View File

@ -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 },
],
},
},
],
};

View File

@ -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>;
}

View File

@ -0,0 +1,3 @@
import { createContext } from 'react';
export const ContextWithActionEnabled = createContext({ actionEnabled: false });

View File

@ -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,
);
}

View File

@ -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' },
);

View File

@ -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>
);
};

View File

@ -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' },
);

View File

@ -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' },
);

View File

@ -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,
});
}
}

View File

@ -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,
};

View File

@ -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,
},
];
};

View File

@ -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}
</>
);
};

View File

@ -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