feat/approval-summary (#1015)

Reviewed-on: daoyoucloud/tachybase#1015
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-05-22 15:17:09 +08:00 committed by sealday
parent b338c51923
commit cfa95bfe1e
41 changed files with 450 additions and 190 deletions

View File

@ -17,6 +17,7 @@ export type AppendsTreeSelectProps = {
multiple?: boolean; multiple?: boolean;
filter?(field): boolean; filter?(field): boolean;
collection?: string; collection?: string;
needLeaf?: boolean;
useCollection?(props: Pick<AppendsTreeSelectProps, 'collection'>): string; useCollection?(props: Pick<AppendsTreeSelectProps, 'collection'>): string;
rootOption?: { rootOption?: {
label: string; label: string;
@ -56,9 +57,16 @@ function trueFilter(field) {
return true; return true;
} }
function getCollectionFieldOptions(this: CallScope, collection, parentNode?): TreeOptionType[] { function getCollectionFieldOptions(
this: CallScope,
collection,
parentNode?,
options = { needLeaf: false },
): TreeOptionType[] {
const { needLeaf } = options;
const [dataSourceName, collectionName] = parseCollectionName(collection); const [dataSourceName, collectionName] = parseCollectionName(collection);
const fields = this.getCollectionFields(collectionName, dataSourceName).filter(isAssociation); const rawFields = this.getCollectionFields(collectionName, dataSourceName);
const fields = needLeaf ? rawFields : rawFields.filter(isAssociation);
const boundLoadChildren = loadChildren.bind(this); const boundLoadChildren = loadChildren.bind(this);
return fields.filter(this.filter).map((field) => { return fields.filter(this.filter).map((field) => {
const key = parentNode ? `${parentNode.value ? `${parentNode.value}.` : ''}${field.name}` : field.name; const key = parentNode ? `${parentNode.value ? `${parentNode.value}.` : ''}${field.name}` : field.name;
@ -88,6 +96,7 @@ export const AppendsTreeSelect: React.FC<TreeSelectProps & AppendsTreeSelectProp
filter = trueFilter, filter = trueFilter,
rootOption, rootOption,
loadData: propsLoadData, loadData: propsLoadData,
needLeaf = false,
...restProps ...restProps
} = props; } = props;
const compile = useCompile(); const compile = useCompile();
@ -133,7 +142,9 @@ export const AppendsTreeSelect: React.FC<TreeSelectProps & AppendsTreeSelectProp
const tData = const tData =
propsLoadData === null propsLoadData === null
? [] ? []
: getCollectionFieldOptions.call({ compile, getCollectionFields, filter }, collectionString, parentNode); : getCollectionFieldOptions.call({ compile, getCollectionFields, filter }, collectionString, parentNode, {
needLeaf,
});
const map = tData.reduce((result, item) => Object.assign(result, { [item.value]: item }), {}); const map = tData.reduce((result, item) => Object.assign(result, { [item.value]: item }), {});
if (parentNode) { if (parentNode) {

View File

@ -1,5 +1,5 @@
import { approvalStatusOptions } from '../constants'; import { approvalStatusOptions } from '../constants';
import { NAMESPACE } from '../locale'; import { NAMESPACE, tval } from '../locale';
export const CollectionApprovalTodos = { export const CollectionApprovalTodos = {
title: `{{t("Approval todos", { ns: "${NAMESPACE}" })}}`, title: `{{t("Approval todos", { ns: "${NAMESPACE}" })}}`,
@ -9,7 +9,11 @@ export const CollectionApprovalTodos = {
type: 'bigInt', type: 'bigInt',
name: 'approvalId', name: 'approvalId',
interface: 'number', interface: 'number',
uiSchema: { type: 'number', title: 'ID', 'x-component': 'InputNumber' }, uiSchema: {
type: 'number',
title: 'ID',
'x-component': 'InputNumber',
},
}, },
{ {
type: 'belongsTo', type: 'belongsTo',
@ -90,5 +94,22 @@ export const CollectionApprovalTodos = {
'x-component-props': { showTime: true }, 'x-component-props': { showTime: true },
}, },
}, },
{
type: 'string',
name: 'summaryString',
interface: 'input',
uiSchema: {
type: 'string',
title: tval('Summary'),
'x-component': 'ApprovalsSummary',
'x-component-props': {
style: {
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
},
},
},
},
], ],
}; };

View File

@ -1,6 +1,6 @@
import { useActionContext } from '@tachybase/client'; import { useActionContext } from '@tachybase/client';
import React from 'react'; import React from 'react';
import { SchemaConfigButton } from '../../trigger/VC.ConfigButton'; import { ConfigButtonView } from '../../trigger/ConfigButton.view';
import { ContextApproverConfig } from '../Pd.ContextApproverConfig'; import { ContextApproverConfig } from '../Pd.ContextApproverConfig';
// 审批人操作界面->进入配置按钮 // 审批人操作界面->进入配置按钮
@ -8,7 +8,7 @@ export const SchemaConfigButtonApprover = (props) => {
const { setFormValueChanged } = useActionContext(); const { setFormValueChanged } = useActionContext();
return ( return (
<ContextApproverConfig.Provider value={{ setFormValueChanged }}> <ContextApproverConfig.Provider value={{ setFormValueChanged }}>
<SchemaConfigButton {...props} /> <ConfigButtonView {...props} />
</ContextApproverConfig.Provider> </ContextApproverConfig.Provider>
); );
}; };

View File

@ -4,12 +4,10 @@ import { ApprovalInstruction } from './node.ApprovalInstruction';
import { ApproverActionConfigInitializer } from './approver-interface/Iz.ApproverActionConfig'; import { ApproverActionConfigInitializer } from './approver-interface/Iz.ApproverActionConfig';
import { ApproverAddBlockInitializer } from './approver-interface/Iz.ApproverAddBlock'; import { ApproverAddBlockInitializer } from './approver-interface/Iz.ApproverAddBlock';
export default class PluginKitApprovalInstruction extends Plugin { export class KitApprovalInstruction extends Plugin {
async afterAdd() {}
async beforeLoad() {}
async load() { async load() {
const workflowPlugin = this.app.pm.get(PluginWorkflow); const pluginWorkflow = this.app.pm.get(PluginWorkflow);
workflowPlugin.registerInstruction('approval', ApprovalInstruction); pluginWorkflow.registerInstruction('approval', ApprovalInstruction);
this.app.schemaInitializerManager.add(ApproverActionConfigInitializer); this.app.schemaInitializerManager.add(ApproverActionConfigInitializer);
this.app.schemaInitializerManager.add(ApproverAddBlockInitializer); this.app.schemaInitializerManager.add(ApproverAddBlockInitializer);

View File

@ -0,0 +1,10 @@
import { Plugin } from '@tachybase/client';
import { KitApprovalInstruction } from './instruction/plugin';
import { KitApprovalTrigger } from './trigger/plugin';
export default class KitApprovalConfiguration extends Plugin {
async afterAdd() {
this.pm.add(KitApprovalTrigger);
this.pm.add(KitApprovalInstruction);
}
}

View File

@ -3,13 +3,14 @@ import {
CollectionBlockInitializer, CollectionBlockInitializer,
RadioWithTooltip, RadioWithTooltip,
Trigger, Trigger,
UseVariableOptions,
getCollectionFieldOptions, getCollectionFieldOptions,
useWorkflowAnyExecuted, useWorkflowAnyExecuted,
} from '@tachybase/plugin-workflow/client'; } from '@tachybase/plugin-workflow/client';
import { useForm } from '@tachybase/schema'; import { useForm } from '@tachybase/schema';
import { SchemaConfigButton } from './VC.ConfigButton'; import { ConfigButtonView } from './ConfigButton.view';
import { NAMESPACE, tval, usePluginTranslation } from '../../locale'; import { NAMESPACE, tval, usePluginTranslation } from '../../locale';
import { LauncherInterface } from './launcher-interface/VC.LauncherInterface'; import { LauncherInterface } from './launcher-interface/LauncherInterface.schema';
// 工作流节点-审批触发器节点 // 工作流节点-审批触发器节点
export class ApprovalTrigger extends Trigger { export class ApprovalTrigger extends Trigger {
@ -17,6 +18,13 @@ export class ApprovalTrigger extends Trigger {
title = `{{t('Approval event', { ns: "${NAMESPACE}" })}}`; title = `{{t('Approval event', { ns: "${NAMESPACE}" })}}`;
description = `{{t("Triggered when an approval request is initiated through an action button or API. Dedicated to the approval process, with exclusive approval node and block for managing documents and tracking processing processes.", { ns: "${NAMESPACE}" })}}`; description = `{{t("Triggered when an approval request is initiated through an action button or API. Dedicated to the approval process, with exclusive approval node and block for managing documents and tracking processing processes.", { ns: "${NAMESPACE}" })}}`;
scope = { useWorkflowAnyExecuted };
components = {
ConfigButtonView,
LauncherInterface,
RadioWithTooltip,
};
// 触发器配置表 // 触发器配置表
fieldset = { fieldset = {
collection: { collection: {
@ -61,7 +69,7 @@ export class ApprovalTrigger extends Trigger {
title: `{{t("Initiator's interface", { ns: "${NAMESPACE}" })}}`, title: `{{t("Initiator's interface", { ns: "${NAMESPACE}" })}}`,
description: `{{t("For initiating approvals, or viewing and manipulating initiated approvals.", { ns: "${NAMESPACE}" })}}`, description: `{{t("For initiating approvals, or viewing and manipulating initiated approvals.", { ns: "${NAMESPACE}" })}}`,
'x-decorator': 'FormItem', 'x-decorator': 'FormItem',
'x-component': 'SchemaConfigButton', 'x-component': 'ConfigButtonView',
'x-reactions': [ 'x-reactions': [
{ {
dependencies: ['collection'], dependencies: ['collection'],
@ -75,7 +83,7 @@ export class ApprovalTrigger extends Trigger {
properties: { properties: {
applyForm: { applyForm: {
type: 'void', type: 'void',
'x-component': 'SchemaInitiatorForm', 'x-component': 'LauncherInterface',
default: null, default: null,
}, },
}, },
@ -106,20 +114,39 @@ export class ApprovalTrigger extends Trigger {
}, },
], ],
}, },
summary: {
type: 'array',
title: '{{t("Select fields to display in the approval summary", { ns: "workflow" })}}',
'x-decorator': 'FormItem',
// TODO: 需要更换为能支持同时选择普通字段和关联字段的组件.目前只能选择关联字段.走通业务逻辑为先
'x-component': 'AppendsTreeSelect',
'x-component-props': {
title: 'Preload associations',
multiple: true,
needLeaf: true,
useCollection() {
const { values } = useForm();
return values?.collection;
},
},
'x-reactions': [
{
dependencies: ['collection'],
fulfill: {
state: {
visible: '{{!!$deps[0]}}',
},
},
},
],
},
}; };
scope = { useWorkflowAnyExecuted }; isActionTriggerable = (config: any, context: { action: string; direct: any }) => {
components = {
SchemaConfigButton,
SchemaInitiatorForm: LauncherInterface,
RadioWithTooltip,
};
isActionTriggerable = (config, context) => {
return ['create', 'update'].includes(context.action) && !context.direct; return ['create', 'update'].includes(context.action) && !context.direct;
}; };
useVariables(config, options) { useVariables(config: { collection: any; appends: any[]; summary: any[] }, options: UseVariableOptions) {
// eslint-disable-next-line react-hooks/rules-of-hooks // eslint-disable-next-line react-hooks/rules-of-hooks
const compile = useCompile(); const compile = useCompile();
// eslint-disable-next-line react-hooks/rules-of-hooks // eslint-disable-next-line react-hooks/rules-of-hooks
@ -135,17 +162,25 @@ export class ApprovalTrigger extends Trigger {
uiSchema: { title: t('Trigger data', { ns: 'workflow' }) }, uiSchema: { title: t('Trigger data', { ns: 'workflow' }) },
}, },
]; ];
return getCollectionFieldOptions({
const configAppends = config.appends?.map((item: any) => `data.${item}`) || [];
const collectionFieldOptions = getCollectionFieldOptions({
// depth, // depth,
appends: ['data', ...(config.appends?.map((item) => `data.${item}`) || [])], appends: ['data', ...configAppends],
...options, ...options,
fields: rootFields, fields: rootFields,
compile, compile,
getCollectionFields, getCollectionFields,
}); });
console.log(
'%c Line:176 🍷 collectionFieldOptions',
'font-size:18px;color:#33a5ff;background:#3f7cff',
collectionFieldOptions,
);
return collectionFieldOptions;
} }
useInitializers(config): SchemaInitializerItemType | null { useInitializers(config: { collection: any }): SchemaInitializerItemType | null {
if (!config.collection) { if (!config.collection) {
return null; return null;
} }

View File

@ -4,7 +4,7 @@ import React, { useState } from 'react';
import { useTranslation } from '../../locale'; import { useTranslation } from '../../locale';
// 发起人操作界面->进入配置按钮 // 发起人操作界面->进入配置按钮
export function SchemaConfigButton(props) { export const ConfigButtonView = (props) => {
const { t } = useTranslation(); const { t } = useTranslation();
const [visible, setVisible] = useState(false); const [visible, setVisible] = useState(false);
return ( return (
@ -17,4 +17,4 @@ export function SchemaConfigButton(props) {
</ActionContextProvider> </ActionContextProvider>
</> </>
); );
} };

View File

@ -1,7 +1,7 @@
import { SchemaInitializer } from '@tachybase/client'; import { SchemaInitializer } from '@tachybase/client';
import { NAMESPACE } from '../../../locale'; import { NAMESPACE } from '../../../locale';
import { APPROVAL_STATUS } from '../../../constants'; import { APPROVAL_STATUS } from '../../../constants';
import { LauncherActionConfigComponent } from './VC.LauncherActionConfig'; import { LauncherActionConfigComponent } from './LauncherActionConfig.component';
// 区块-配置操作 // 区块-配置操作
export const LauncherActionConfigInitializer = new SchemaInitializer({ export const LauncherActionConfigInitializer = new SchemaInitializer({

View File

@ -1,6 +1,6 @@
import { SchemaInitializer, gridRowColWrap } from '@tachybase/client'; import { SchemaInitializer, gridRowColWrap } from '@tachybase/client';
import { NAMESPACE } from '../../../locale'; import { NAMESPACE } from '../../../locale';
import { LauncherAddBlockButtonComponent } from './VC.LauncherBlockButton'; import { LauncherAddBlockButtonComponent } from './LauncherBlockButton.component';
// 创建区块 // 创建区块
export const LauncherAddBlockButtonIntializer = new SchemaInitializer({ export const LauncherAddBlockButtonIntializer = new SchemaInitializer({

View File

@ -1,8 +1,11 @@
import { css } from '@tachybase/client'; import { SchemaComponent, css, parseCollectionName } from '@tachybase/client';
import { useForm } from '@tachybase/schema';
import React from 'react';
import { NAMESPACE } from '../../../locale'; import { NAMESPACE } from '../../../locale';
import { SchemaAddBlock } from './SchemaAddBlock.component';
// 发起人操作界面 // 发起人操作界面
export const getSchemaLauncherInterface = ({ values, dataSource, name }) => ({ const getSchemaLauncherInterface = ({ values, dataSource, name }) => ({
name: values.collection, name: values.collection,
type: 'void', type: 'void',
properties: { properties: {
@ -30,4 +33,19 @@ export const getSchemaLauncherInterface = ({ values, dataSource, name }) => ({
}, },
}, },
}, },
}); }); // 触发器-发起人的操作界面
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

@ -1,21 +0,0 @@
import { SchemaComponent, parseCollectionName } from '@tachybase/client';
import { useForm } from '@tachybase/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

@ -1,15 +1,15 @@
import { Plugin } from '@tachybase/client'; import { Plugin } from '@tachybase/client';
import PluginWorkflow from '@tachybase/plugin-workflow/client'; import PluginWorkflow from '@tachybase/plugin-workflow/client';
import { ApprovalTrigger } from './node.ApprovalTrigger'; import { ApprovalTrigger } from './Approval.trigger';
import { LauncherActionConfigInitializer } from './launcher-interface/Iz.LauncherActionConfig'; import { LauncherActionConfigInitializer } from './launcher-interface/LauncherActionConfig.initializer';
import { LauncherAddBlockButtonIntializer } from './launcher-interface/Iz.LauncherAddBlockButton'; import { LauncherAddBlockButtonIntializer } from './launcher-interface/LauncherAddBlockButton.initializer';
export class PluginKitApprovalTrigger extends Plugin { export class KitApprovalTrigger extends Plugin {
async afterAdd() {} async afterAdd() {}
async beforeLoad() {} async beforeLoad() {}
async load() { async load() {
const workflowPlugin = this.app.pm.get(PluginWorkflow); const pluginWorkflow = this.app.pm.get(PluginWorkflow);
workflowPlugin.registerTrigger('approval', ApprovalTrigger); pluginWorkflow.registerTrigger('approval', ApprovalTrigger);
this.app.schemaInitializerManager.add(LauncherActionConfigInitializer); this.app.schemaInitializerManager.add(LauncherActionConfigInitializer);
this.app.schemaInitializerManager.add(LauncherAddBlockButtonIntializer); this.app.schemaInitializerManager.add(LauncherAddBlockButtonIntializer);

View File

@ -1,12 +1,12 @@
import { Plugin } from '@tachybase/client'; import { Plugin } from '@tachybase/client';
import { PluginKitApprovalConfiguration } from './configuration'; import KitApprovalConfiguration from './configuration/plugin';
import { PluginKitApprovalUsage } from './usage'; import { KitApprovalUsage } from './usage/plugin';
export default class PluginApproval extends Plugin { export default class PluginApproval extends Plugin {
async afterAdd() { async afterAdd() {
this.pm.add(PluginKitApprovalConfiguration); this.pm.add(KitApprovalConfiguration);
this.pm.add(PluginKitApprovalUsage); this.pm.add(KitApprovalUsage);
} }
async beforeLoad() {} async beforeLoad() {}
async load() {} async load() {}

View File

@ -1,17 +1,17 @@
import React from 'react';
import { ExtendCollectionsProvider, TableBlockProvider, useRecord } from '@tachybase/client'; import { ExtendCollectionsProvider, TableBlockProvider, useRecord } from '@tachybase/client';
import React from 'react';
import { CollectionApprovalTodos } from '../../common/Cn.ApprovalTodos'; import { CollectionApprovalTodos } from '../../common/Cn.ApprovalTodos';
import { CollectionApprovals } from '../approval-common/Approvals.collection'; import { CollectionApprovals } from '../approval-common/Approvals.collection';
import { CollectionFlowNodes } from '../approval-common/FlowNodes.collection'; import { CollectionFlowNodes } from '../approval-common/FlowNodes.collection';
import { CollectionWorkflows } from '../approval-common/Workflows.collection'; import { CollectionWorkflows } from '../approval-common/Workflows.collection';
export function ApprovalBlockDecorator({ children, ...props }) { export const ApprovalBlockProvider = ({ children, ...props }) => {
const { const {
collection, collection,
action = 'list',
params = { params = {
filter: {}, filter: {},
}, },
action = 'list',
} = props; } = props;
const record = useRecord(); const record = useRecord();
@ -23,7 +23,12 @@ export function ApprovalBlockDecorator({ children, ...props }) {
pageSize: 20, pageSize: 20,
sort: ['-createdAt'], sort: ['-createdAt'],
...params, ...params,
filter: record?.id ? { dataKey: `${record.id}`, ...params.filter } : { ...params.filter }, filter: record?.id
? {
dataKey: `${record.id}`,
...params.filter,
}
: { ...params.filter },
}, },
rowKey: 'id', rowKey: 'id',
showIndex: true, showIndex: true,
@ -39,4 +44,4 @@ export function ApprovalBlockDecorator({ children, ...props }) {
</TableBlockProvider> </TableBlockProvider>
</ExtendCollectionsProvider> </ExtendCollectionsProvider>
); );
} };

View File

@ -0,0 +1,93 @@
import { Plugin } from '@tachybase/client';
import { TableOutlined } from '@ant-design/icons';
import { SchemaInitializerItem, useSchemaInitializer, useSchemaInitializerItem } from '@tachybase/client';
import { uid } from '@tachybase/utils/client';
import React from 'react';
import { NAMESPACE } from '../../locale';
import { ApprovalBlockLaunch } from './launch/VC.ApprovalBlockLaunch';
import { ApprovalBlockProvider } from './ApprovalBlock.provider';
import { ApprovalBlockTodos } from './todos/VC.ApprovalBlockTodos';
export class SCApprovalBlock extends Plugin {
async load() {
this.app.addComponents({
'ApprovalBlock.Decorator': ApprovalBlockProvider,
'ApprovalBlock.Launch': ApprovalBlockLaunch,
'ApprovalBlock.Todos': ApprovalBlockTodos,
});
}
}
const schemaItems = [
{
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',
],
},
},
];
const getSchemaInsert = ({ item }) => {
const id = uid();
const { collection, params, ['x-component']: xcomponent } = item;
return {
type: 'void',
name: id,
'x-uid': id,
'x-designer': 'TableBlockDesigner',
'x-decorator': 'ApprovalBlock.Decorator',
'x-decorator-props': {
collection,
params,
action: 'listCentralized',
},
'x-component': 'CardItem',
properties: {
block: {
type: 'void',
'x-component': xcomponent,
},
},
};
};
// 创建区块-审批(发起/待办)
export const ApprovalBlockComponent = () => {
const schemaInitializerItem = useSchemaInitializerItem();
const { insert } = useSchemaInitializer();
const onClick = ({ item }) => {
const schema = getSchemaInsert({ item });
insert(schema);
};
return (
<SchemaInitializerItem icon={<TableOutlined />} {...schemaInitializerItem} items={schemaItems} onClick={onClick} />
);
};

View File

@ -1,73 +0,0 @@
import { TableOutlined } from '@ant-design/icons';
import { SchemaInitializerItem, useSchemaInitializer, useSchemaInitializerItem } from '@tachybase/client';
import { uid } from '@tachybase/utils/client';
import React from 'react';
import { NAMESPACE } from '../../locale';
// 创建区块-审批(发起/待办)
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': '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

@ -1,15 +0,0 @@
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

@ -1,4 +1,4 @@
import { NAMESPACE } from '../../../locale'; import { NAMESPACE, tval } from '../../../locale';
import { css } from '@tachybase/client'; import { css } from '@tachybase/client';
export const SchemaApprovalBlockLaunch = { export const SchemaApprovalBlockLaunch = {
@ -142,6 +142,19 @@ export const SchemaApprovalBlockLaunch = {
}, },
}, },
}, },
summaryString: {
type: 'void',
'x-decorator': 'TableV2.Column.Decorator',
'x-component': 'TableV2.Column',
title: tval('Approval Summary'),
properties: {
summaryString: {
type: 'string',
'x-component': 'CollectionField',
'x-read-pretty': true,
},
},
},
}, },
}, },
}, },

View File

@ -1,26 +1,21 @@
import { Plugin } from '@tachybase/client'; import { Plugin } from '@tachybase/client';
import { PluginKitApprovalCommon } from '../approval-common/plugin';
import { tval } from '../../locale'; import { tval } from '../../locale';
import { ViewActionTodos } from './todos/VC.ViewActionTodos'; import { PluginKitApprovalCommon } from '../approval-common/plugin';
import { ApprovalBlockComponent } from './VC.ApprovalBlock'; import { ApprovalBlockComponent, SCApprovalBlock } from './ApprovalBlock.schema';
import { ApprovalBlockTodos } from './todos/VC.ApprovalBlockTodos';
import { ViewActionLaunch } from './launch/VC.ViewActionLaunch'; import { ViewActionLaunch } from './launch/VC.ViewActionLaunch';
import { ApprovalBlockDecorator } from './Dt.ApprovalBlock'; import { ViewActionTodos } from './todos/VC.ViewActionTodos';
import { ApprovalBlockLaunch } from './launch/VC.ApprovalBlockLaunch';
export class PluginKitApprovalBlock extends Plugin { export class KitApprovalBlock extends Plugin {
async afterAdd() { async afterAdd() {
this.pm.add(PluginKitApprovalCommon); this.pm.add(PluginKitApprovalCommon);
this.pm.add(SCApprovalBlock);
} }
async beforeLoad() {} async beforeLoad() {}
async load() { async load() {
this.app.addComponents({ this.app.addComponents({
'ApprovalBlock.Decorator': ApprovalBlockDecorator,
'ApprovalBlock.BlockInitializer': ApprovalBlockComponent, 'ApprovalBlock.BlockInitializer': ApprovalBlockComponent,
'ApprovalBlock.Launch': ApprovalBlockLaunch,
'ApprovalBlock.Todos': ApprovalBlockTodos,
'ApprovalBlock.ViewActionLaunch': ViewActionLaunch, 'ApprovalBlock.ViewActionLaunch': ViewActionLaunch,
'ApprovalBlock.ViewActionTodos': ViewActionTodos, 'ApprovalBlock.ViewActionTodos': ViewActionTodos,
}); });

View File

@ -1,5 +1,19 @@
import { createContext, useContext } from 'react'; import { createContext, useContext } from 'react';
import { Approval } from '../interface'; 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;
}
interface ApprovalExecutions { interface ApprovalExecutions {
id: number; id: number;

View File

@ -1,5 +1,5 @@
import { css } from '@tachybase/client'; import { css } from '@tachybase/client';
import { NAMESPACE } from '../../../locale'; import { NAMESPACE, tval } from '../../../locale';
export const SchemaApprovalBlockTodos = { export const SchemaApprovalBlockTodos = {
type: 'void', type: 'void',
@ -143,6 +143,19 @@ export const SchemaApprovalBlockTodos = {
}, },
}, },
}, },
summaryString: {
type: 'void',
'x-decorator': 'TableV2.Column.Decorator',
'x-component': 'TableV2.Column',
title: tval('Approval Summary'),
properties: {
summaryString: {
type: 'string',
'x-component': 'CollectionField',
'x-read-pretty': true,
},
},
},
}, },
}, },
}, },

View File

@ -1,6 +1,6 @@
import { useRecord } from '@tachybase/client'; import { useRecord } from '@tachybase/client';
import React, { createContext, useContext } from 'react'; import React, { createContext, useContext } from 'react';
import { Approval } from '../approval-block/interface'; import { Approval } from '../approval-block/todos/Pd.ApprovalExecutions';
export const ApprovalContext = createContext<Partial<Approval>>({}); export const ApprovalContext = createContext<Partial<Approval>>({});

View File

@ -1,4 +1,4 @@
import { NAMESPACE } from '../../locale'; import { NAMESPACE, tval } from '../../locale';
import { ApprovalStatusEnums } from '../../constants'; import { ApprovalStatusEnums } from '../../constants';
export const CollectionApprovals = { export const CollectionApprovals = {
@ -9,7 +9,11 @@ export const CollectionApprovals = {
type: 'bigInt', type: 'bigInt',
name: 'id', name: 'id',
interface: 'number', interface: 'number',
uiSchema: { type: 'number', title: 'ID', 'x-component': 'InputNumber' }, uiSchema: {
type: 'number',
title: 'ID',
'x-component': 'InputNumber',
},
}, },
{ {
type: 'belongsTo', type: 'belongsTo',
@ -34,7 +38,15 @@ export const CollectionApprovals = {
type: 'number', type: 'number',
title: `{{t("Initiator", { ns: "${NAMESPACE}" })}}`, title: `{{t("Initiator", { ns: "${NAMESPACE}" })}}`,
'x-component': 'RemoteSelect', 'x-component': 'RemoteSelect',
'x-component-props': { fieldNames: { label: 'nickname', value: 'id' }, service: { resource: 'users' } }, 'x-component-props': {
fieldNames: {
label: 'nickname',
value: 'id',
},
service: {
resource: 'users',
},
},
}, },
}, },
{ {
@ -59,5 +71,22 @@ export const CollectionApprovals = {
'x-component-props': { showTime: true }, 'x-component-props': { showTime: true },
}, },
}, },
{
type: 'string',
name: 'summaryString',
interface: 'input',
uiSchema: {
type: 'string',
title: tval('Summary'),
'x-component': 'ApprovalsSummary',
'x-component-props': {
style: {
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
},
},
},
},
], ],
}; };

View File

@ -0,0 +1,16 @@
import React from 'react';
export const ApprovalsSummary = (props) => {
const { value = '', style } = props;
const valueArray = value.split(',');
return (
<div>
{valueArray.map((val) => (
<div style={style} key={val}>
{val}
</div>
))}
</div>
);
};

View File

@ -13,8 +13,18 @@ export const CollectionFlowNodes = {
title: 'ID', title: 'ID',
'x-component': 'RemoteSelect', 'x-component': 'RemoteSelect',
'x-component-props': { 'x-component-props': {
fieldNames: { label: 'title', value: 'id' }, fieldNames: {
service: { resource: 'flow_nodes', params: { filter: { type: 'manual' } } }, label: 'title',
value: 'id',
},
service: {
resource: 'flow_nodes',
params: {
filter: {
type: 'manual',
},
},
},
}, },
}, },
}, },
@ -22,7 +32,11 @@ export const CollectionFlowNodes = {
type: 'string', type: 'string',
name: 'title', name: 'title',
interface: 'input', interface: 'input',
uiSchema: { type: 'string', title: '{{t("Title")}}', 'x-component': 'Input' }, uiSchema: {
type: 'string',
title: '{{t("Title")}}',
'x-component': 'Input',
},
}, },
], ],
}; };

View File

@ -8,7 +8,12 @@ export const CollectionWorkflows = {
type: 'string', type: 'string',
name: 'title', name: 'title',
interface: 'input', interface: 'input',
uiSchema: { title: '{{t("Name")}}', type: 'string', 'x-component': 'Input', required: true }, uiSchema: {
title: '{{t("Name")}}',
type: 'string',
'x-component': 'Input',
required: true,
},
}, },
{ {
type: 'boolean', type: 'boolean',
@ -19,8 +24,14 @@ export const CollectionWorkflows = {
title: '{{t("Status", { ns: "workflow" })}}', title: '{{t("Status", { ns: "workflow" })}}',
'x-component': 'Select', 'x-component': 'Select',
enum: [ enum: [
{ label: '{{t("On", { ns: "workflow" })}}', value: true }, {
{ label: '{{t("Off", { ns: "workflow" })}}', value: false }, label: '{{t("On", { ns: "workflow" })}}',
value: true,
},
{
label: '{{t("Off", { ns: "workflow" })}}',
value: false,
},
], ],
}, },
}, },

View File

@ -1,6 +1,7 @@
import { Plugin } from '@tachybase/client'; import { Plugin } from '@tachybase/client';
import { ApprovalDataProvider } from './ApprovalData.provider'; import { ApprovalDataProvider } from './ApprovalData.provider';
import { ApprovalProcess } from './ApprovalProcess.view'; import { ApprovalProcess } from './ApprovalProcess.view';
import { ApprovalsSummary } from './ApprovalsSummary.view';
export class PluginKitApprovalCommon extends Plugin { export class PluginKitApprovalCommon extends Plugin {
async afterAdd() {} async afterAdd() {}
@ -8,8 +9,10 @@ export class PluginKitApprovalCommon extends Plugin {
async load() { async load() {
this.app.addComponents({ this.app.addComponents({
// ApprovalCommon, // ApprovalCommon,
'ApprovalCommon.Provider.ApprovalDataProvider': ApprovalDataProvider, 'ApprovalCommon.Provider.ApprovalDataProvider': ApprovalDataProvider,
'ApprovalCommon.ViewComponent.ApprovalProcess': ApprovalProcess, 'ApprovalCommon.ViewComponent.ApprovalProcess': ApprovalProcess,
ApprovalsSummary: ApprovalsSummary,
}); });
} }
} }

View File

@ -2,6 +2,7 @@ import { renderColumnDetail } from './render.detail';
import { renderColumnStatus } from './render.status'; import { renderColumnStatus } from './render.status';
import { renderColumnTaskNode } from './render.taskNode'; import { renderColumnTaskNode } from './render.taskNode';
// 审批处理-表格行配置
export const getAntdTableColumns = ({ t, styles }) => { export const getAntdTableColumns = ({ t, styles }) => {
return [ return [
{ {

View File

@ -1,11 +1,11 @@
import { Plugin } from '@tachybase/client'; import { Plugin } from '@tachybase/client';
import { PluginKitApprovalRecordBlock } from './approval-record-block/plugin'; import { PluginKitApprovalRecordBlock } from './approval-record-block/plugin';
import { PluginKitApprovalBlock } from './approval-block'; import { KitApprovalBlock } from './approval-block/plugin';
export class PluginKitApprovalUsage extends Plugin { export class KitApprovalUsage extends Plugin {
async afterAdd() { async afterAdd() {
this.pm.add(PluginKitApprovalRecordBlock); this.pm.add(PluginKitApprovalRecordBlock);
this.pm.add(PluginKitApprovalBlock); this.pm.add(KitApprovalBlock);
} }
async beforeLoad() {} async beforeLoad() {}
async load() {} async load() {}

View File

@ -12,6 +12,7 @@
"Initiator": "Initiator", "Initiator": "Initiator",
"Application content": "Application content", "Application content": "Application content",
"Approval process": "Approval process", "Approval process": "Approval process",
"Approval Summary": "Approval Summary",
"History": "History", "History": "History",
"Approval ID": "Approval ID", "Approval ID": "Approval ID",
"Node": "Node", "Node": "Node",
@ -61,5 +62,6 @@
"End on reject": "End on reject", "End on reject": "End on reject",
"If checked, the workflow will be terminated after rejection branch processed.": "If checked, the workflow will be terminated after rejection branch processed.", "If checked, the workflow will be terminated after rejection branch processed.": "If checked, the workflow will be terminated after rejection branch processed.",
"Disabled": "Disabled", "Disabled": "Disabled",
"Trigger data": "Trigger data" "Trigger data": "Trigger data",
"Select fields to display in the approval summary": "Select fields to display in the approval summary"
} }

View File

@ -13,6 +13,7 @@
"Initiator": "发起人", "Initiator": "发起人",
"Application content": "申请内容", "Application content": "申请内容",
"Approval process": "审批处理", "Approval process": "审批处理",
"Approval Summary": "审批摘要",
"No data yet": "暂无数据", "No data yet": "暂无数据",
"Approval ID": "单据编号", "Approval ID": "单据编号",
"Current status": "当前状态", "Current status": "当前状态",
@ -76,5 +77,6 @@
"When checked, the workflow will terminate when the rejection branch ends.": "勾选后,否决分支结束后工作流将终止。", "When checked, the workflow will terminate when the rejection branch ends.": "勾选后,否决分支结束后工作流将终止。",
"Disabled": "已失效", "Disabled": "已失效",
"Submission may be withdrawn, please try refresh the list.": "提交可能已被撤回,请尝试刷新列表。", "Submission may be withdrawn, please try refresh the list.": "提交可能已被撤回,请尝试刷新列表。",
"Trigger data": "触发器数据" "Trigger data": "触发器数据",
"Select fields to display in the approval summary": "选择审批摘要显示字段"
} }

View File

@ -127,7 +127,7 @@ export default class ApprovalInstruction extends Instruction {
filter: { filter: {
'executions.id': processor.execution.id, 'executions.id': processor.execution.id,
}, },
fields: ['id', 'status', 'data'], fields: ['id', 'status', 'data', 'summaryString'],
appends: ['approvalExecutions'], appends: ['approvalExecutions'],
except: ['data'], except: ['data'],
}); });
@ -145,6 +145,7 @@ export default class ApprovalInstruction extends Instruction {
index, index,
status: node.config.order && index ? APPROVAL_ACTION_STATUS.ASSIGNED : APPROVAL_ACTION_STATUS.PENDING, status: node.config.order && index ? APPROVAL_ACTION_STATUS.ASSIGNED : APPROVAL_ACTION_STATUS.PENDING,
snapshot: approvalExecution.snapshot, snapshot: approvalExecution.snapshot,
summaryString: approval.summaryString,
})), })),
{ {
transaction: processor.transaction, transaction: processor.transaction,

View File

@ -5,6 +5,7 @@ import { parseCollectionName } from '@tachybase/data-source-manager';
import { EXECUTION_STATUS, Trigger, toJSON, JOB_STATUS } from '@tachybase/plugin-workflow'; import { EXECUTION_STATUS, Trigger, toJSON, JOB_STATUS } from '@tachybase/plugin-workflow';
import { APPROVAL_ACTION_STATUS, APPROVAL_STATUS } from './constants'; import { APPROVAL_ACTION_STATUS, APPROVAL_STATUS } from './constants';
import { UiSchemaRepository } from '@nocobase/plugin-ui-schema-storage'; import { UiSchemaRepository } from '@nocobase/plugin-ui-schema-storage';
import { getSummaryString } from './tools';
const ExecutionStatusMap = { const ExecutionStatusMap = {
[EXECUTION_STATUS.RESOLVED]: APPROVAL_STATUS.APPROVED, [EXECUTION_STATUS.RESOLVED]: APPROVAL_STATUS.APPROVED,
@ -51,6 +52,10 @@ export default class ApprovalTrigger extends Trigger {
data: toJSON(data), data: toJSON(data),
approvalId: approval.id, approvalId: approval.id,
applicantRoleName: approval.applicantRoleName, applicantRoleName: approval.applicantRoleName,
summaryString: getSummaryString({
summaryConfig: workflow.config.summary,
data,
}),
}, },
{ transaction }, { transaction },
); );
@ -60,13 +65,14 @@ export default class ApprovalTrigger extends Trigger {
if (workflow.type !== ApprovalTrigger.TYPE) { if (workflow.type !== ApprovalTrigger.TYPE) {
return; return;
} }
const { approvalId, data } = execution.context; const { approvalId, data, summaryString } = execution.context;
const approvalExecution = await this.workflow.db.getRepository('approvalExecutions').create({ const approvalExecution = await this.workflow.db.getRepository('approvalExecutions').create({
values: { values: {
approvalId, approvalId,
executionId: execution.id, executionId: execution.id,
status: execution.status, status: execution.status,
snapshot: data, snapshot: data,
summaryString: summaryString,
}, },
transaction, transaction,
}); });
@ -192,6 +198,7 @@ export default class ApprovalTrigger extends Trigger {
// updatedById: currentUser.id, // updatedById: currentUser.id,
workflowId: workflow.id, workflowId: workflow.id,
workflowKey: workflow.key, workflowKey: workflow.key,
summaryString: '12,11,15',
}, },
context, context,
}); });
@ -216,7 +223,20 @@ export default class ApprovalTrigger extends Trigger {
if (dataSourceName !== dataSourceHeader) { if (dataSourceName !== dataSourceHeader) {
continue; continue;
} }
(Array.isArray(data) ? data : [data]).forEach(async (row) => { (Array.isArray(data) ? data : [data]).forEach(async (row) => {
let dataCurrent = {};
if (row.id) {
// XXX: 丑陋的实现, 应该从 data 直接获取的就是有值的 data, 走通优先.
const { repository } = this.workflow.app.dataSourceManager.dataSources
.get(dataSourceName)
.collectionManager.getCollection(collectionName);
const curretSummaryConfig = workflow.config?.summary || [];
dataCurrent = await repository.findOne({
filterByTk: data.id,
appends: [...workflow.config.appends],
});
}
let payload = row; let payload = row;
if (trigger[1]) { if (trigger[1]) {
const paths = trigger[1].split('.'); const paths = trigger[1].split('.');
@ -235,6 +255,8 @@ export default class ApprovalTrigger extends Trigger {
if (!collection || collection.model !== payload.constructor) { if (!collection || collection.model !== payload.constructor) {
return; return;
} }
// 以上是 审批摘要取值逻辑
await approvalRepo.create({ await approvalRepo.create({
values: { values: {
collectionName: workflow.config.collection, collectionName: workflow.config.collection,
@ -246,6 +268,10 @@ export default class ApprovalTrigger extends Trigger {
workflowId: workflow.id, workflowId: workflow.id,
workflowKey: workflow.key, workflowKey: workflow.key,
applicantRoleName: context.state.currentRole, applicantRoleName: context.state.currentRole,
summaryString: getSummaryString({
summaryConfig: workflow.config.summary,
data: dataCurrent,
}),
}, },
context, context,
}); });

View File

@ -1,5 +1,5 @@
import actions, { utils } from '@tachybase/actions'; import actions, { utils } from '@tachybase/actions';
import WorkflowPlugin, { EXECUTION_STATUS, JOB_STATUS } from '@tachybase/plugin-workflow'; import WorkflowPlugin, { EXECUTION_STATUS, JOB_STATUS, toJSON } from '@tachybase/plugin-workflow';
import { APPROVAL_STATUS, APPROVAL_ACTION_STATUS } from './constants'; import { APPROVAL_STATUS, APPROVAL_ACTION_STATUS } from './constants';
import { parseCollectionName } from '@tachybase/data-source-manager'; import { parseCollectionName } from '@tachybase/data-source-manager';
@ -60,6 +60,7 @@ const approvals = {
dataKey: values[collection.filterTargetKey], dataKey: values[collection.filterTargetKey],
workflowKey: workflow.key, workflowKey: workflow.key,
applicantRoleName: context.state.currentRole, applicantRoleName: context.state.currentRole,
summary: toJSON(workflow.config?.summary ?? []),
}, },
}); });
return actions.create(context, next); return actions.create(context, next);
@ -237,6 +238,7 @@ const approvalRecords = {
status: values.status, status: values.status,
comment: values.comment, comment: values.comment,
snapshot: approvalRecord.approval.data, snapshot: approvalRecord.approval.data,
summaryString: approvalRecord.approval.summaryString,
}); });
context.body = approvalRecord.get(); context.body = approvalRecord.get();
context.status = 202; context.status = 202;

View File

@ -1,5 +1,6 @@
import { defineCollection } from '@tachybase/database'; import { defineCollection } from '@tachybase/database';
// 审批
export default defineCollection({ export default defineCollection({
namespace: 'workflow.approvalExecutions', namespace: 'workflow.approvalExecutions',
dumpRules: 'required', dumpRules: 'required',

View File

@ -1,4 +1,6 @@
import { defineCollection } from '@tachybase/database'; import { defineCollection } from '@tachybase/database';
// 审批待办
export default defineCollection({ export default defineCollection({
namespace: 'workflow.approvalRecords', namespace: 'workflow.approvalRecords',
dumpRules: 'required', dumpRules: 'required',
@ -48,6 +50,11 @@ export default defineCollection({
name: 'snapshot', name: 'snapshot',
defaultValue: {}, defaultValue: {},
}, },
{
type: 'string',
name: 'summaryString',
defaultValue: '',
},
{ {
type: 'text', type: 'text',
name: 'comment', name: 'comment',

View File

@ -1,4 +1,6 @@
import { defineCollection } from '@tachybase/database'; import { defineCollection } from '@tachybase/database';
// 审批发起
export default defineCollection({ export default defineCollection({
namespace: 'workflow.approvals', namespace: 'workflow.approvals',
dumpRules: 'required', dumpRules: 'required',
@ -59,6 +61,11 @@ export default defineCollection({
name: 'data', name: 'data',
defaultValue: {}, defaultValue: {},
}, },
{
type: 'string',
name: 'summaryString',
defaultValue: '',
},
{ {
type: 'belongsTo', type: 'belongsTo',
name: 'applicantRole', name: 'applicantRole',

View File

@ -0,0 +1,21 @@
import _ from 'lodash';
interface ParamsType {
summaryConfig: Array<string>;
data: object;
}
export function getSummaryString(params: ParamsType): string {
const { summaryConfig = [], data } = params;
const result = summaryConfig
.map((key) => {
const value = _.get(data, key);
// XXX: 丑陋的实现, 不应该依赖具体字段, 应该从 summaryConfig, 拿到的就是最终的值, 走通优先
if (Object.prototype.toString.call(value) === '[object Object]') {
return _.get(value, 'name');
}
return _.get(data, key);
})
.join(',');
return result;
}