fix: approval, show process & feat: approval, lastNode (#1343)

Reviewed-on: daoyoucloud/tachybase#1343
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-18 17:18:13 +08:00 committed by sealday
parent 73fdc790f4
commit 220bf172fc
13 changed files with 172 additions and 117 deletions

View File

@ -60,7 +60,10 @@ export const CollectionApprovalTodos = {
type: 'number', type: 'number',
title: `{{t("Task node", { ns: "${NAMESPACE}" })}}`, title: `{{t("Task node", { ns: "${NAMESPACE}" })}}`,
'x-component': 'RemoteSelect', 'x-component': 'RemoteSelect',
'x-component-props': { fieldNames: { label: 'title', value: 'id' }, service: { resource: 'flow_nodes' } }, 'x-component-props': {
fieldNames: { label: 'title', value: 'id' },
service: { resource: 'flow_nodes' },
},
}, },
}, },
{ {

View File

@ -45,7 +45,14 @@ const schemaItems = [
'x-component': 'ApprovalBlock.Launch', 'x-component': 'ApprovalBlock.Launch',
collection: 'approvals', collection: 'approvals',
params: { params: {
appends: ['createdBy.nickname', 'workflow.title', 'workflow.enabled'], appends: [
'createdBy.nickname',
'workflow.title',
'workflow.enabled',
'records.id',
'records.status',
'records.node.title',
],
except: ['data'], except: ['data'],
}, },
}, },

View File

@ -156,6 +156,21 @@ export const SchemaApprovalBlockLaunch = {
}, },
}, },
}, },
lastNode: {
type: 'void',
'x-decorator': 'TableV2.Column.Decorator',
'x-component': 'TableV2.Column',
'x-component-props': {
width: 200,
},
title: `{{t("Task node", { ns: "${NAMESPACE}" })}}`,
properties: {
lastNode: {
'x-component': 'ApprovalLastNodeColumn',
'x-read-pretty': true,
},
},
},
}, },
}, },
}, },

View File

@ -63,7 +63,7 @@ export const ViewActionLaunchContent = () => {
// @ts-ignore // @ts-ignore
const approvalData = data?.data; const approvalData = data?.data;
const needHideProcess = actionEnabled || approvalData?.status !== APPROVAL_STATUS.RESUBMIT; const needHideProcess = actionEnabled;
if (loading) { if (loading) {
return <Spin />; return <Spin />;

View File

@ -4,13 +4,27 @@ import { createStyles, useCurrentUserContext } from '@tachybase/client';
import { Space, Table } from 'antd'; import { Space, Table } from 'antd';
import _ from 'lodash'; import _ from 'lodash';
import { EXECUTION_STATUS } from '../../../../constants'; import { usePluginTranslation } from '../../locale';
import { APPROVAL_ACTION_STATUS, APPROVAL_STATUS } from '../../constants';
import { lang, usePluginTranslation } from '../../locale';
import { useApproval } from './ApprovalData.provider'; import { useApproval } from './ApprovalData.provider';
import { getAntdTableColumns } from './process-columns/columns'; import { getAntdTableColumns } from './process-columns';
import { getResults } from './tools';
import { ContextWithActionEnabled } from './WithActionEnabled.provider'; import { ContextWithActionEnabled } from './WithActionEnabled.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};
}
`,
}));
// 审批(发起/待办)区块-查看-审批处理 // 审批(发起/待办)区块-查看-审批处理
export const ApprovalProcess = (props) => { export const ApprovalProcess = (props) => {
const { t } = usePluginTranslation(); const { t } = usePluginTranslation();
@ -32,88 +46,3 @@ export const ApprovalProcess = (props) => {
</ContextWithActionEnabled.Provider> </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 { 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: lang('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,24 @@
import React, { useMemo } from 'react';
import { useCollectionRecordData } from '@tachybase/client';
import { APPROVAL_ACTION_STATUS } from '../../../constants';
export const ApprovalLastNodeColumn = () => {
const approvalContext = useCollectionRecordData();
const lastNodeTitle = useMemo(() => getLastNodeTitle(approvalContext), [approvalContext]);
return <div>{lastNodeTitle}</div>;
};
function getLastNodeTitle(approvalContext) {
const { records } = approvalContext;
let targetRecord = records.find(({ status }) => status !== APPROVAL_ACTION_STATUS.APPROVED);
if (!targetRecord) {
targetRecord = records.pop() || {};
}
const lastNodeTitle = targetRecord.node?.title;
return lastNodeTitle;
}

View File

@ -2,7 +2,7 @@ import React from 'react';
import { useCollectionManager, useCollectionRecordData, useCompile } from '@tachybase/client'; import { useCollectionManager, useCollectionRecordData, useCompile } from '@tachybase/client';
import { dayjs } from '@tachybase/utils/client'; import { dayjs } from '@tachybase/utils/client';
import useStyles from './style'; import useStyles from '../style';
export const ApprovalsSummary = (props) => { export const ApprovalsSummary = (props) => {
const { value = '' } = props; const { value = '' } = props;

View File

@ -1,17 +1,17 @@
import { Plugin } from '@tachybase/client'; import { Plugin } from '@tachybase/client';
import { ApprovalLastNodeColumn } from './approval-columns/lastNode.column';
import { ApprovalsSummary } from './approval-columns/summary.column';
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 KitApprovalCommon extends Plugin { export class KitApprovalCommon extends Plugin {
async load() { async load() {
this.app.addComponents({ this.app.addComponents({
// ApprovalCommon,
'ApprovalCommon.Provider.ApprovalDataProvider': ApprovalDataProvider, 'ApprovalCommon.Provider.ApprovalDataProvider': ApprovalDataProvider,
'ApprovalCommon.ViewComponent.ApprovalProcess': ApprovalProcess, 'ApprovalCommon.ViewComponent.ApprovalProcess': ApprovalProcess,
ApprovalsSummary: ApprovalsSummary, ApprovalsSummary: ApprovalsSummary,
ApprovalLastNodeColumn: ApprovalLastNodeColumn,
}); });
} }
} }

View File

@ -14,6 +14,7 @@ export const getAntdTableColumns = ({ t, styles }) => {
}; };
}, },
render: renderColumnTaskNode, render: renderColumnTaskNode,
width: 120,
}, },
{ {
title: t('User'), title: t('User'),

View File

@ -28,7 +28,6 @@ export function renderColumnTaskNode(text, { node, job, groupCount, statusCount
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.5em; gap: 0.5em;
.ant-tag { .ant-tag {
margin-right: 0; margin-right: 0;
} }

View File

@ -0,0 +1,73 @@
import _ from 'lodash';
import { EXECUTION_STATUS } from '../../../../constants';
import { APPROVAL_ACTION_STATUS, APPROVAL_STATUS } from '../../constants';
import { lang } from '../../locale';
export function getResults({ approval, currentUser }) {
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: lang('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

@ -3,6 +3,7 @@ import { UiSchemaRepository } from '@tachybase/plugin-ui-schema-storage';
import { JOB_STATUS } from '../../constants'; import { JOB_STATUS } from '../../constants';
import Instruction from '../../instructions'; import Instruction from '../../instructions';
import { COLLECTION_NAME_APPROVAL_CARBON_COPY } from '../common/constants'; import { COLLECTION_NAME_APPROVAL_CARBON_COPY } from '../common/constants';
import { APPROVAL_STATUS } from './constants';
import { parsePerson } from './tools'; import { parsePerson } from './tools';
export default class ApprovalCarbonCopyInstruction extends Instruction { export default class ApprovalCarbonCopyInstruction extends Instruction {
@ -25,27 +26,29 @@ export default class ApprovalCarbonCopyInstruction extends Instruction {
appends: ['approvalExecutions', 'createdBy'], appends: ['approvalExecutions', 'createdBy'],
except: ['data'], except: ['data'],
}); });
const CarbonCopyModel = db.getModel(COLLECTION_NAME_APPROVAL_CARBON_COPY); // NOTE: 只有新发起审批的时候, 才生成抄送副本. 否则, 会生成不必要的重复副本
const notifiedPersonDataMap = targetPersonList.map((userId, index) => ({ if ([APPROVAL_STATUS.SUBMITTED].includes(approval.status)) {
userId, const CarbonCopyModel = db.getModel(COLLECTION_NAME_APPROVAL_CARBON_COPY);
jobId: job.id, const notifiedPersonDataMap = targetPersonList.map((userId, index) => ({
nodeId: node.id, userId,
executionId: job.executionId, jobId: job.id,
workflowId: node.workflowId, nodeId: node.id,
index, executionId: job.executionId,
// TODO: 怎么做到状态实时更新, 观察下审批发起的表的状态是怎么做的... workflowId: node.workflowId,
createdById: approval.createdBy?.id, index,
approvalId: approval.id, createdById: approval.createdBy?.id,
status: approval.status, approvalId: approval.id,
snapshot: approval.data, status: approval.status,
summary: approval.summary, snapshot: approval.data,
collectionName: approval.collectionName, summary: approval.summary,
dataKey: approval.dataKey, collectionName: approval.collectionName,
})); dataKey: approval.dataKey,
}));
await CarbonCopyModel.bulkCreate(notifiedPersonDataMap, { await CarbonCopyModel.bulkCreate(notifiedPersonDataMap, {
transaction: processor.transaction, transaction: processor.transaction,
}); });
}
} }
return job; return job;

View File

@ -370,6 +370,7 @@ const approvalCarbonCopy = {
context.action.mergeParams({ context.action.mergeParams({
filter: { filter: {
workflowId: centralizedApprovalFlow.map((item) => item.id), workflowId: centralizedApprovalFlow.map((item) => item.id),
// approval.status: APPROVAL_STATUS.DRAFT,
}, },
}); });
return actions.list(context, next); return actions.list(context, next);