feat/approval-0702 (#1264)

Reviewed-on: daoyoucloud/tachybase#1264
Reviewed-by: sealday <zhanglin@daoyoucloud.com>
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-07-03 13:49:47 +08:00 committed by sealday
parent c8f99dd228
commit 0d8bbba1da
20 changed files with 418 additions and 120 deletions

View File

@ -13,9 +13,9 @@ import { AssigneesSelect } from './approval-config/VC.AssigneesSelect';
import { ContentTooltip } from './approval-config/VC.ContentTooltip';
import { NegotiationConfig } from './approval-config/VC.NegotiationConfig';
import { SchemaConfigButtonApprover } from './approval-config/VC.SchemaConfigButtonApprover';
import { ApprovalInstructionNode } from './ApprovalNode.component';
import { ApproverInterfaceComponent } from './approver-interface/ApproverInterface.schema';
import { isApprovalReturnFunc } from './utils';
import { ApprovalInstructionNode } from './VC.ApprovalInstructionNode';
// 工作流节点 nodes - 人工处理->审批
export class ApprovalInstruction extends Instruction {
@ -42,13 +42,6 @@ export class ApprovalInstruction extends Instruction {
];
// 审批节点表单设置
fieldset = {
collection: {
type: 'string',
title: '{{t("Collection")}}',
'x-decorator': 'FormItem',
'x-component': 'DataSourceCollectionCascader',
// 'x-disabled': '{{ useWorkflowAnyExecuted() }}',
},
branchMode: {
type: 'boolean',
title: `{{t("Pass mode", { ns: "${NAMESPACE}" })}}`,

View File

@ -2,7 +2,7 @@ import React from 'react';
import { useActionContext } from '@tachybase/client';
import { ConfigButtonView } from '../../trigger/ConfigButton.view';
import { ContextApproverConfig } from '../Pd.ContextApproverConfig';
import { ContextApproverConfig } from '../ApproverConfig.context';
// 审批人操作界面->进入配置按钮
export const SchemaConfigButtonApprover = (props) => {

View File

@ -1,11 +1,11 @@
import { gridRowColWrap, SchemaInitializer, usePlugin } from '@tachybase/client';
import { gridRowColWrap, SchemaInitializer, useDataSourceManager, usePlugin } from '@tachybase/client';
import { useFlowContext } from '../../../../../FlowContext';
import { useAvailableUpstreams, useNodeContext } from '../../../../../nodes';
import { PluginWorkflow } from '../../../../../Plugin';
import { useTrigger } from '../../../../../triggers';
import { NAMESPACE } from '../../../locale';
import { EditableForm } from './EditableForm.schema';
import { approvalFormOptions, ApprovalFormType } from '../forms/Approval.options';
import { ApproverAddBlockComponent } from './VC.ApproverAddBlock';
import { ApproverAddBlockKit } from './VC.ApproverAddBlockKit';
@ -33,19 +33,6 @@ export const ApproverAddBlockInitializer = new SchemaInitializer({
},
],
},
{
name: 'forms',
type: 'itemGroup',
title: '{{t("Form")}}',
children: [
{
name: 'form',
type: 'item',
title: `{{t("Apply form", { ns: "${NAMESPACE}" })}}`,
Component: EditableForm,
},
],
},
{
type: 'itemGroup',
name: 'dataBlocks',
@ -79,6 +66,22 @@ export const ApproverAddBlockInitializer = new SchemaInitializer({
].filter(Boolean);
},
},
{
type: 'itemGroup',
name: 'form',
title: '{{t("Form")}}',
useChildren() {
const dm = useDataSourceManager();
const allCollections = dm.getAllCollections();
const values = Array.from(approvalFormOptions.getValues());
return values.map((item: ApprovalFormType) => {
// NOTE: 这里通过赋别名,避免 eslint 检查 hooks 语法,无法提交
// eslint-disable-next-line react-hooks/rules-of-hooks
const { useInitializer: getInitializer } = item.config;
return getInitializer({ allCollections });
});
},
},
{
name: 'others',
type: 'itemGroup',

View File

@ -8,7 +8,7 @@ import { Spin } from 'antd';
import { DetailsBlockProvider, SimpleDesigner } from '../../../../../components';
import { FormBlockProvider } from '../../../common/Pd.FormBlock';
import { flatSchemaArray } from '../../../constants';
import { ContextApproverConfig } from '../Pd.ContextApproverConfig';
import { ContextApproverConfig } from '../ApproverConfig.context';
import { ActionBarProvider } from './Pd.ActionBarProvider';
import { ApprovalActionProvider } from './Pd.ApprovalActionProvider';
import { ContextApproverBlock } from './Pd.SchemaConfigButtonContext';

View File

@ -1,45 +0,0 @@
import React from 'react';
import {
createEditFormBlockUISchema,
createFormBlockSchema,
parseCollectionName,
SchemaInitializerItem,
useRecordCollectionDataSourceItems,
useSchemaInitializer,
useSchemaInitializerItem,
useSchemaTemplateManager,
} from '@tachybase/client';
import { useForm } from '@tachybase/schema';
import { APPROVAL_STATUS, flatSchemaArray } from '../../../constants';
export const EditableForm = () => {
const { insert } = useSchemaInitializer();
const { getTemplateSchemaByMode } = useSchemaTemplateManager();
const { values } = useForm();
const [dataSource, collectionName] = parseCollectionName(values.collection);
const itemConfig = useSchemaInitializerItem();
const items = useRecordCollectionDataSourceItems('FormItem');
const onClick = async ({ item }) => {
const template = item.template ? await getTemplateSchemaByMode(item) : null;
const formSchema = createEditFormBlockUISchema({
dataSource: dataSource,
collectionName: collectionName,
templateSchema: 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

@ -1,3 +1,4 @@
export const ApprovalActionProvider = (props) => {
console.log('%c Line:2 🍰 props', 'font-size:18px;color:#4fff4B;background:#7f2b82', props);
return props.children;
};

View File

@ -0,0 +1,102 @@
import React from 'react';
import { CompatibleSchemaInitializer, InitializerWithSwitch, useSchemaInitializerItem } from '@tachybase/client';
import { JOB_STATUS } from '../../../../../constants';
import { APPROVAL_ACTION_STATUS } from '../../../constants';
import { NAMESPACE } from '../../../locale';
/**
* @deprecated
*/
// NOTE: 似乎没用, 但删除又报错, 先保留着
const addActionButton_deprecated = new CompatibleSchemaInitializer({
name: 'ApprovalAddActionButton',
title: '{{t("Configure actions")}}',
items: [
{
name: 'jobStatusResolved',
title: `{{t("Continue the process", { ns: "${NAMESPACE}" })}}`,
Component: ActionInitializer,
action: JOB_STATUS.RESOLVED,
actionProps: {
type: 'primary',
},
},
{
name: 'jobStatusRejected',
title: `{{t("Terminate the process", { ns: "${NAMESPACE}" })}}`,
Component: ActionInitializer,
action: JOB_STATUS.REJECTED,
actionProps: {
danger: true,
},
},
{
name: 'jobStatusPending',
title: `{{t("Save temporarily", { ns: "${NAMESPACE}" })}}`,
Component: ActionInitializer,
action: JOB_STATUS.PENDING,
},
],
});
export const ApprovalAddActionButton = new CompatibleSchemaInitializer(
{
name: 'ApprovalAddActionButton',
title: '{{t("Configure actions")}}',
items: [
{
name: 'approvalStatusResolved',
title: `{{t("Continue the process", { ns: "${NAMESPACE}" })}}`,
Component: ActionInitializer,
statusApproval: APPROVAL_ACTION_STATUS.APPROVED,
actionProps: {
type: 'primary',
},
},
{
name: 'approvalStatusRejected',
title: `{{t("Terminate the process", { ns: "${NAMESPACE}" })}}`,
Component: ActionInitializer,
statusApproval: APPROVAL_ACTION_STATUS.REJECTED,
actionProps: {
danger: true,
},
},
{
name: 'approvalStatusPending',
title: `{{t("Save temporarily", { ns: "${NAMESPACE}" })}}`,
Component: ActionInitializer,
statusApproval: APPROVAL_ACTION_STATUS.PENDING,
},
],
},
addActionButton_deprecated,
);
function ActionInitializer() {
const itemConfig = useSchemaInitializerItem();
const { statusApproval, actionProps, ...others } = itemConfig;
return (
<InitializerWithSwitch
{...others}
item={itemConfig}
schema={{
type: 'void',
title: others.title,
'x-decorator': 'ApprovalActionProvider',
'x-decorator-props': {
status: statusApproval,
},
'x-component': 'Action',
'x-component-props': {
...actionProps,
useAction: `{{ () => useSubmit({source: "updateRecord"}) }}`,
},
'x-designer': 'Action.Designer',
'x-action': `${statusApproval}`,
}}
type="x-action"
/>
);
}

View File

@ -0,0 +1,45 @@
import { SchemaInitializerItemType } from '@tachybase/client';
import { Registry } from '@tachybase/utils/client';
import { JOB_STATUS } from '../../../../../constants';
import updateFormConfig from './update';
type ValueOf<T> = T[keyof T];
export type FormType = {
type: 'create' | 'update' | 'custom';
title: string;
actions: ValueOf<typeof JOB_STATUS>[];
collection:
| string
| {
name: string;
fields: any[];
[key: string]: any;
};
};
export type ApprovalFormType = {
title: string;
config: {
useInitializer: ({ allCollections }?: { allCollections: any[] }) => SchemaInitializerItemType;
initializers?: {
[key: string]: React.FC;
};
components?: {
[key: string]: React.FC;
};
parseFormOptions(root: ISchema): { [key: string]: FormType };
};
block: {
scope?: {
[key: string]: () => any;
};
components?: {
[key: string]: React.FC;
};
};
};
export const approvalFormOptions = new Registry<ApprovalFormType>();
approvalFormOptions.register('updateForm', updateFormConfig as ApprovalFormType);

View File

@ -0,0 +1,67 @@
import React from 'react';
import {
CollectionProvider_deprecated,
createFormBlockSchema,
Plugin,
SchemaInitializerItem,
SchemaInitializerItemType,
useRecordCollectionDataSourceItems,
useSchemaInitializer,
useSchemaInitializerItem,
useSchemaTemplateManager,
} from '@tachybase/client';
import { traverseSchema } from '../../../../../utils';
import { ApprovalAddActionButton } from './AddActionButton.setting';
export class KitApprovalAddActionButton extends Plugin {
async load() {
this.app.schemaInitializerManager.add(ApprovalAddActionButton);
}
}
function InternalFormBlockInitializer({ schema, ...others }) {
const { insert } = useSchemaInitializer();
const { getTemplateSchemaByMode } = useSchemaTemplateManager();
const items = useRecordCollectionDataSourceItems('FormItem') as SchemaInitializerItemType[];
const onClick = async (item) => {
const template = item.template ? await getTemplateSchemaByMode(item) : null;
const result = createFormBlockSchema({
actionInitializers: 'ApprovalAddActionButton',
...schema,
template,
});
delete result['x-acl-action-props'];
delete result['x-acl-action'];
const [formKey] = Object.keys(result.properties);
//获取actionBar的schemakey
const actionKey =
Object.entries(result.properties[formKey].properties).find(([key, f]) => f['x-component'] === 'ActionBar')?.[0] ||
'actions';
result.properties[formKey].properties[actionKey]['x-decorator'] = 'ActionBarProvider';
result.properties[formKey].properties[actionKey]['x-component-props'].style = {
marginTop: '1.5em',
flexWrap: 'wrap',
};
traverseSchema(result, (node) => {
if (node['x-uid']) {
delete node['x-uid'];
}
});
insert(result);
};
return <SchemaInitializerItem {...others} onClick={onClick} items={items} />;
}
export const FormBlockFactory = () => {
const itemConfig = useSchemaInitializerItem();
return (
<CollectionProvider_deprecated
dataSource={itemConfig.schema?.dataSource}
collection={itemConfig.schema?.collection}
>
<InternalFormBlockInitializer {...itemConfig} />
</CollectionProvider_deprecated>
);
};

View File

@ -0,0 +1,109 @@
import { useMemo, useState } from 'react';
import { useMenuSearch } from '@tachybase/client';
import _ from 'lodash';
import { FilterDynamicComponent } from '../../../../../components';
import { NAMESPACE } from '../../../locale';
import { FormBlockFactory } from './FormBlock.factory';
const updateFormConfig = {
title: `{{t("Update record form", { ns: "${NAMESPACE}" })}}`,
config: {
initializers: {},
components: {
FilterDynamicComponent,
},
useInitializer: ({ allCollections }) => {
const childItems = useMemo(() => {
const results = allCollections.map(({ key, displayName, collections }) => ({
key,
name: key,
label: displayName,
type: 'subMenu',
children: collections.map(({ name, title, tableName }) => ({
name: _.camelCase(`updateRecordForm-child-${name}`),
type: 'item',
title: title || tableName,
schema: {
collection: name,
dataSource: key,
formType: 'update',
},
Component: FormBlockFactory,
})),
}));
return results;
}, [allCollections]);
const [openMenuKeys, setOpenMenuKeys] = useState([]);
const searchedChildren = useMenuSearch({ data: childItems, openKeys: openMenuKeys });
return {
name: 'updateRecordForm',
key: 'updateRecordForm',
type: 'subMenu',
title: `{{t("Update record form", { ns: "${NAMESPACE}" })}}`,
componentProps: {
onOpenChange(keys) {
setOpenMenuKeys(keys);
},
},
children: searchedChildren,
};
},
parseFormOptions: (root) => {
const forms = {};
const formBlocks: any[] = findSchema(
root,
(item) => item['x-decorator'] === 'FormBlockProvider' && item['x-decorator-props'].formType === 'update',
);
formBlocks.forEach((formBlock) => {
const [formKey] = Object.keys(formBlock.properties);
const formSchema = formBlock.properties[formKey];
//获取actionBar的schemakey
const actionKey =
Object.entries(formSchema.properties).find(([key, f]) => f['x-component'] === 'ActionBar')?.[0] || 'actions';
forms[formKey] = {
...formBlock['x-decorator-props'],
type: 'update',
title: formBlock['x-component-props']?.title || formKey,
actions: findSchema(formSchema.properties[actionKey], (item) => item['x-component'] === 'Action').map(
(item) => ({
status: item['x-decorator-props'].value,
values: item['x-action-settings']?.assignedValues?.values,
key: item.name,
}),
),
};
});
return forms;
},
},
block: {
scope: {},
components: {},
},
};
// utils
function findSchema(schema, filter, onlyLeaf = false) {
const result = [];
if (!schema) {
return result;
}
if (filter(schema) && (!onlyLeaf || !schema.properties)) {
result.push(schema);
return result;
}
if (schema.properties) {
Object.keys(schema.properties).forEach((key) => {
result.push(...findSchema(schema.properties[key], filter));
});
}
return result;
}
export default updateFormConfig;

View File

@ -1,11 +1,16 @@
import { Plugin } from '@tachybase/client';
import { PluginWorkflow } from '../../../../Plugin';
import { ApprovalInstruction } from './Approval.node';
import { ApproverAddBlockInitializer } from './approver-interface/ApproverAddBlock.initializer';
import { ApproverActionConfigInitializer } from './approver-interface/Iz.ApproverActionConfig';
import { ApproverAddBlockInitializer } from './approver-interface/Iz.ApproverAddBlock';
import { ApprovalInstruction } from './node.ApprovalInstruction';
import { KitApprovalAddActionButton } from './forms/FormBlock.factory';
export class KitApprovalInstruction extends Plugin {
async afterAdd(): Promise<void> {
this.pm.add(KitApprovalAddActionButton);
}
async load() {
const pluginWorkflow = this.app.pm.get(PluginWorkflow);
pluginWorkflow.registerInstruction('approval', ApprovalInstruction);

View File

@ -5,37 +5,7 @@ import { useForm } from '@tachybase/schema';
import { NAMESPACE } from '../../../locale';
import { SchemaAddBlock } from './SchemaAddBlock.component';
// 发起人操作界面
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(--tb-box-bg);
}
`,
},
properties: {
applyForm: {
type: 'string',
'x-decorator': 'CollectionProvider_deprecated',
'x-decorator-props': {
dataSource,
name,
},
'x-component': 'SchemaAddBlock',
},
},
},
},
}); // 触发器-发起人的操作界面
// 触发器-发起人的操作界面
export const LauncherInterface = () => {
const { values } = useForm();
const [dataSource, name] = parseCollectionName(values.collection);
@ -50,3 +20,36 @@ export const LauncherInterface = () => {
/>
);
};
// 发起人操作界面
function getSchemaLauncherInterface({ values, dataSource, name }) {
return {
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(--tb-box-bg);
}
`,
},
properties: {
applyForm: {
type: 'string',
'x-decorator': 'CollectionProvider_deprecated',
'x-decorator-props': {
dataSource,
name,
},
'x-component': 'SchemaAddBlock',
},
},
},
},
};
}

View File

@ -1,7 +1,7 @@
import { JOB_STATUS } from '../../constants';
import { lang, NAMESPACE } from './locale';
// 审批状态
// 审批发起的状态
export const APPROVAL_STATUS = {
DRAFT: 0,
RETURNED: 1,
@ -11,6 +11,7 @@ export const APPROVAL_STATUS = {
REJECTED: -1,
};
// 审批执行的状态
export const APPROVAL_ACTION_STATUS = {
ASSIGNED: null,
PENDING: 0,

View File

@ -16,8 +16,6 @@ export function useDestroyAction() {
return {
async run() {
try {
console.log('%c Line:16 🍓 run', 'font-size:18px;color:#6ec1c2;background:#93c0a4');
_.set(field, ['data', 'loading'], true);
await apiClient.resource('approvals').destroy({

View File

@ -27,7 +27,6 @@ export function useWithdrawAction() {
field.data.loading = false;
refreshTable();
} catch (v) {
console.log('%c Line:33 🌽 v', 'font-size:18px;color:#33a5ff;background:#fca650', v);
if (field.data) {
field.data.loading = false;
}

View File

@ -2,16 +2,12 @@ import React, { createContext, useContext } from 'react';
import { useContextApprovalExecutions } from './Pd.ApprovalExecutions';
export interface ApprovalAction {
interface ApprovalAction {
status: number | null;
}
const ContextApprovalAction = createContext<Partial<ApprovalAction>>({});
export function useContextApprovalAction() {
return useContext(ContextApprovalAction);
}
export function ApprovalActionProvider({ children, ...props }) {
const { status } = useContextApprovalExecutions();
@ -21,3 +17,8 @@ export function ApprovalActionProvider({ children, ...props }) {
return null;
}
export function useContextApprovalAction() {
const context = useContext(ContextApprovalAction);
return context;
}

View File

@ -1,18 +1,20 @@
import { useAPIClient } from '@tachybase/client';
import { useAPIClient, useCollection } from '@tachybase/client';
import { useField, useForm } from '@tachybase/schema';
import { useHandleRefresh } from '../../common/useHandleRefresh';
import { useContextApprovalAction } from '../Pd.ApprovalAction';
import { useContextApprovalExecutions } from '../Pd.ApprovalExecutions';
export function useSubmit() {
export function useSubmit(props: any = {}) {
const { refreshTable } = useHandleRefresh();
const field = useField();
const api = useAPIClient();
const form = useForm();
const collection = useCollection();
const approvalExecutions = useContextApprovalExecutions();
const { status } = useContextApprovalAction();
const { source } = props;
const needUpdateRecord = source === 'updateRecord';
return {
run: async () => {
@ -20,14 +22,27 @@ export function useSubmit() {
if (form.values.status) {
return;
}
await form.submit();
field.data = field.data ?? {};
field.data.loading = true;
if (needUpdateRecord) {
const collectionName = collection.name;
const targetId = form.values.id;
await api.resource(collectionName).update({
filterByTk: targetId,
values: form.values,
});
}
await api.resource('approvalRecords').submit({
filterByTk: approvalExecutions.id,
values: { ...form.values, status },
values: {
status,
needUpdateRecord,
data: form.values,
},
});
field.data.loading = false;

View File

@ -234,6 +234,7 @@ const approvalRecords = {
async submit(context, next) {
const repository = utils.getRepositoryFromParams(context);
const { filterByTk, values } = context.action.params;
const { data, status, needUpdateRecord } = values || {};
const { currentUser } = context.state;
if (!currentUser) {
return context.throw(401);
@ -254,13 +255,13 @@ const approvalRecords = {
approvalRecord.execution.status ||
approvalRecord.job.status ||
approvalRecord.status !== APPROVAL_ACTION_STATUS.PENDING ||
!(approvalRecord.node.config.actions ?? []).includes(values.status)
(!needUpdateRecord && !(approvalRecord.node.config.actions ?? []).includes(status))
) {
return context.throw(400);
}
await approvalRecord.update({
status: values.status,
comment: values.comment,
status: status,
comment: data.comment,
snapshot: approvalRecord.approval.data,
summary: approvalRecord.approval.summary,
collectionName: approvalRecord.approval.collectionName,