feat: 添加手机端审批重新申请功能 (#1273)

Reviewed-on: https://git.daoyoucloud.com:8443/daoyoucloud/tachybase/pulls/1273
Reviewed-by: sealday <zhanglin@daoyoucloud.com>
Co-authored-by: wjh <wwwjh0710@163.com>
Co-committed-by: wjh <wwwjh0710@163.com>
This commit is contained in:
wjh 2024-07-04 19:55:12 +08:00 committed by sealday
parent b470ac1b12
commit a094b9670d
17 changed files with 201 additions and 50 deletions

View File

@ -52,18 +52,24 @@ import {
APIClientProvider, APIClientProvider,
ApplicationContext, ApplicationContext,
AssociationOrCollectionProvider, AssociationOrCollectionProvider,
Checkbox,
CollectionFieldOptions_deprecated, CollectionFieldOptions_deprecated,
CollectionRecordProvider, CollectionRecordProvider,
createDesignable, createDesignable,
DataSourceApplicationProvider, DataSourceApplicationProvider,
DatePicker,
Designable, Designable,
findFormBlock, findFormBlock,
FormDialog, FormDialog,
FormProvider, FormProvider,
Radio,
RemoteSchemaComponent, RemoteSchemaComponent,
SchemaComponent, SchemaComponent,
SchemaComponentContext, SchemaComponentContext,
SchemaComponentOptions, SchemaComponentOptions,
Select,
Cascader as TCascader,
Input as TInput,
useAPIClient, useAPIClient,
useApp, useApp,
useBlockRequestContext, useBlockRequestContext,
@ -1009,6 +1015,17 @@ export const SchemaSettingsModalItem: FC<SchemaSettingsModalItemProps> = (props)
...others ...others
} = props; } = props;
const options = useContext(SchemaOptionsContext); const options = useContext(SchemaOptionsContext);
options.components = {
...options.components,
Input: TInput,
'Input.TextArea': TInput.TextArea,
'Radio.Group': Radio.Group,
'Checkbox.Group': Checkbox.Group,
DatePicker,
Select,
AlternativeAssociationSelect: undefined,
Cascader: TCascader,
};
const { styles } = useStyles(); const { styles } = useStyles();
const collection = useCollection_deprecated(); const collection = useCollection_deprecated();
const apiClient = useAPIClient(); const apiClient = useAPIClient();

View File

@ -57,6 +57,8 @@ export const APPROVAL_ACTION_STATUS = {
PROCESSING: 3, PROCESSING: 3,
/** 4已完结 */ /** 4已完结 */
APPROVED: 4, APPROVED: 4,
/**5重新提交 */
RESUBMIT: 5,
/** -1拒收 */ /** -1拒收 */
REJECTED: -1, REJECTED: -1,
}; };
@ -73,6 +75,7 @@ export const ApprovalStatusEnums = [
{ value: APPROVAL_ACTION_STATUS.PROCESSING, label: `Processing`, color: 'gold' }, { value: APPROVAL_ACTION_STATUS.PROCESSING, label: `Processing`, color: 'gold' },
{ value: APPROVAL_ACTION_STATUS.APPROVED, label: `Approved`, color: 'green' }, { value: APPROVAL_ACTION_STATUS.APPROVED, label: `Approved`, color: 'green' },
{ value: APPROVAL_ACTION_STATUS.REJECTED, label: `Rejected`, color: 'red' }, { value: APPROVAL_ACTION_STATUS.REJECTED, label: `Rejected`, color: 'red' },
{ value: APPROVAL_ACTION_STATUS.RESUBMIT, label: 'ReSubmit', color: 'blue', editable: true },
]; ];
export const ApprovalPriorityType = [ export const ApprovalPriorityType = [

View File

@ -16,8 +16,6 @@ import { UserInitiationsItem } from './component/UserInitiationsItem';
import '../style/style.css'; import '../style/style.css';
import { createGlobalStyle } from '@tachybase/client';
export const InitiationsBlockContext = createContext({}); export const InitiationsBlockContext = createContext({});
export const InitiationsBlock = observer((props) => { export const InitiationsBlock = observer((props) => {
@ -67,28 +65,6 @@ export const InitiationsBlock = observer((props) => {
setContextFilter(filter); setContextFilter(filter);
}} }}
> >
<Tabs.Tab title="发起申请" key="initiations">
<SchemaComponent
components={{
InitiationsItem,
}}
schema={{
type: 'void',
name: 'initiations',
title: t('Initiations'),
'x-settings': 'ApprovalSettings',
'x-component': 'InitiationsItem',
'x-component-props': {
...props,
collectionName: 'workflows',
settingBlock: true,
tabKey: 'initiations',
parantUid: fieldSchema['x-uid'],
parantParams: props['params'],
},
}}
/>
</Tabs.Tab>
<Tabs.Tab title="我的发起" key="userInitiations"> <Tabs.Tab title="我的发起" key="userInitiations">
<SchemaComponent <SchemaComponent
components={{ components={{
@ -111,6 +87,28 @@ export const InitiationsBlock = observer((props) => {
}} }}
/> />
</Tabs.Tab> </Tabs.Tab>
<Tabs.Tab title="发起申请" key="initiations">
<SchemaComponent
components={{
InitiationsItem,
}}
schema={{
type: 'void',
name: 'initiations',
title: t('Initiations'),
'x-settings': 'ApprovalSettings',
'x-component': 'InitiationsItem',
'x-component-props': {
...props,
collectionName: 'workflows',
settingBlock: true,
tabKey: 'initiations',
parantUid: fieldSchema['x-uid'],
parantParams: props['params'],
},
}}
/>
</Tabs.Tab>
</Tabs> </Tabs>
</InitiationsBlockContext.Provider> </InitiationsBlockContext.Provider>
</BlockItem> </BlockItem>

View File

@ -1,6 +1,7 @@
import React, { useContext, useEffect, useState } from 'react'; import React, { useContext, useEffect, useState } from 'react';
import { useAPIClient, useCurrentUserContext } from '@tachybase/client'; import { useAPIClient, useCollectionManager, useCompile, useCurrentUserContext } from '@tachybase/client';
import { observer } from '@tachybase/schema'; import { observer } from '@tachybase/schema';
import { dayjs } from '@tachybase/utils/client';
import { useDeepCompareEffect } from 'ahooks'; import { useDeepCompareEffect } from 'ahooks';
import { Badge, Empty, List, Space, Tag } from 'antd-mobile'; import { Badge, Empty, List, Space, Tag } from 'antd-mobile';
@ -20,8 +21,10 @@ export const ApprovalItem = observer((props) => {
const user = useCurrentUserContext(); const user = useCurrentUserContext();
const contextFilter = useContext(InitiationsBlockContext); const contextFilter = useContext(InitiationsBlockContext);
const inputFilter = contextFilter['key'] === 'userInitiations' ? contextFilter['inputFilter'] : ''; const inputFilter = contextFilter['key'] === 'userInitiations' ? contextFilter['inputFilter'] : '';
const cm = useCollectionManager();
const compile = useCompile();
useDeepCompareEffect(() => { useDeepCompareEffect(() => {
changService(api, setData, user, { ...params?.[tabKey], ...filter }, t, setDefaultData); changService(api, setData, user, { ...params?.[tabKey], ...filter }, t, setDefaultData, cm, compile);
}, [filter, params]); }, [filter, params]);
useEffect(() => { useEffect(() => {
if (inputFilter && defaultData.length) { if (inputFilter && defaultData.length) {
@ -40,7 +43,7 @@ export const ApprovalItem = observer((props) => {
<List.Item <List.Item
key={index} key={index}
onClick={() => { onClick={() => {
navigate(`/mobile/approval/${item.id}/page`); navigate(`/mobile/approval/${item.latestExecutionId}/page`);
}} }}
> >
{/* <Badge color="#6ac3ff" content={Badge.dot} style={{ '--right': '100%' }}> */} {/* <Badge color="#6ac3ff" content={Badge.dot} style={{ '--right': '100%' }}> */}
@ -54,7 +57,9 @@ export const ApprovalItem = observer((props) => {
</Tag> </Tag>
</Space> </Space>
{/* </Badge> */} {/* </Badge> */}
<Space block> :{item.reason}</Space> {item.reason?.map((reasonItem, index) => {
return <Space block key={index}>{`${reasonItem.label}:${reasonItem.value}`}</Space>;
})}
</List.Item> </List.Item>
); );
})} })}
@ -71,7 +76,7 @@ const approvalTodoListStatus = (item, t) => {
return ApprovalStatusEnums.find((value) => value.value === status); return ApprovalStatusEnums.find((value) => value.value === status);
}; };
const changService = (api, setData, user, filter, t, setDefaultData) => { const changService = (api, setData, user, filter, t, setDefaultData, cm, compile) => {
api api
.request({ .request({
url: 'approvals:listCentralized', url: 'approvals:listCentralized',
@ -82,13 +87,27 @@ const changService = (api, setData, user, filter, t, setDefaultData) => {
const priorityType = ApprovalPriorityType.find((priorityItem) => priorityItem.value === item.data.priority); const priorityType = ApprovalPriorityType.find((priorityItem) => priorityItem.value === item.data.priority);
const statusType = approvalTodoListStatus(item, t); const statusType = approvalTodoListStatus(item, t);
const categoryTitle = item.workflow.title.replace('审批流:', ''); const categoryTitle = item.workflow.title.replace('审批流:', '');
const collectionName = item.workflow?.config?.collection || item.execution?.context?.collectionName;
const summary = Object.entries(item.summary)?.map(([key, value]) => {
const field = cm.getCollectionField(`${collectionName}.${key}`);
let resonValue = value;
if (field.type === 'date' && value) {
resonValue = dayjs(value as string).format('YYYY-MM-DD HH:mm:ss');
}
return {
label: compile(field?.uiSchema?.title || key),
value:
(Object.prototype.toString.call(value) === '[object Object]' ? resonValue?.['name'] : resonValue) || '',
};
});
return { return {
...item, ...item,
title: `${user.data.data.nickname}${categoryTitle}`, title: `${user.data.data.nickname}${categoryTitle}`,
categoryTitle: categoryTitle, categoryTitle: categoryTitle,
statusTitle: t(statusType.label), statusTitle: t(statusType.label),
statusColor: statusType.color, statusColor: statusType.color,
reason: item.data.reason || item.data.reason_pay, reason: summary || [],
priorityTitle: priorityType.label, priorityTitle: priorityType.label,
priorityColor: priorityType.color, priorityColor: priorityType.color,
}; };

View File

@ -103,6 +103,7 @@ export const ViewActionInitiationsContent = () => {
scope={{ scope={{
useSubmit: useCreateSubmit, useSubmit: useCreateSubmit,
useWithdrawAction, useWithdrawAction,
useActionResubmit,
}} }}
/> />
</div> </div>
@ -111,6 +112,10 @@ export const ViewActionInitiationsContent = () => {
); );
}; };
export function useActionResubmit() {
return { run() {} };
}
export function WithdrawActionProvider() { export function WithdrawActionProvider() {
return null; return null;
} }

View File

@ -28,6 +28,9 @@ import '../../style/style.css';
import { MobileProvider } from '@tachybase/plugin-mobile-client/client'; import { MobileProvider } from '@tachybase/plugin-mobile-client/client';
import { useActionResubmit } from '../hook/useActionResubmit';
import { ProviderActionResubmit } from '../provider/ActionResubmit.provider';
export const ViewActionUserInitiationsContent = () => { export const ViewActionUserInitiationsContent = () => {
const params = useParams(); const params = useParams();
const navigate = useNavigate(); const navigate = useNavigate();
@ -41,7 +44,7 @@ export const ViewActionUserInitiationsContent = () => {
.request({ .request({
url: 'approvalExecutions:get', url: 'approvalExecutions:get',
params: { params: {
filter: { approvalId: id }, filterByTk: id,
appends: [ appends: [
'execution', 'execution',
'execution.jobs', 'execution.jobs',
@ -70,7 +73,7 @@ export const ViewActionUserInitiationsContent = () => {
.catch(() => { .catch(() => {
console.error; console.error;
}); });
}, []); }, [id]);
// @ts-ignore // @ts-ignore
const { approval, execution, ...approvalValue } = recordData || {}; const { approval, execution, ...approvalValue } = recordData || {};
const { workflow } = approval || {}; const { workflow } = approval || {};
@ -147,6 +150,7 @@ const UserInitiationsComponent = (applyDetail) => {
WithdrawActionProvider, WithdrawActionProvider,
DetailsBlockProvider, DetailsBlockProvider,
MobileProvider, MobileProvider,
ProviderActionResubmit,
}} }}
scope={{ scope={{
useForm, useForm,
@ -155,6 +159,7 @@ const UserInitiationsComponent = (applyDetail) => {
useDetailsBlockProps: useFormBlockContext, useDetailsBlockProps: useFormBlockContext,
useWithdrawAction, useWithdrawAction,
useDestroyAction, useDestroyAction,
useActionResubmit,
}} }}
schema={formContextSchema} schema={formContextSchema}
/> />

View File

@ -0,0 +1,71 @@
import { useAPIClient } from '@tachybase/client';
import { useFlowContext } from '@tachybase/plugin-workflow/client';
import { useField } from '@tachybase/schema';
import { Toast } from 'antd-mobile';
import _ from 'lodash';
import { useNavigate } from 'react-router-dom';
import { APPROVAL_ACTION_STATUS, APPROVAL_STATUS } from '../../constants';
import { useContextApprovalExecution } from '../../context/ApprovalExecution';
// 重新发起
export function useActionResubmit() {
const field = useField();
const api = useAPIClient();
const { approval } = useContextApprovalExecution();
const { workflow } = approval;
const navigate = useNavigate();
return {
async run() {
try {
field.data = field.data ?? {};
field.data.loading = true;
const appendsConfigs = [...workflow.config.appends];
const summaryConfigs = [...workflow.config.summary];
const appends = _.intersection(appendsConfigs, summaryConfigs);
const { data: approvalData } = await api.resource('approvals').get({
filterByTk: approval.id,
});
const { collectionName, data, workflowId } = approvalData.data;
const newData = _.omit(data, ['id', 'createdAt', 'updatedAt', 'createdBy', 'updatedBy']);
api
.resource('approvals')
.reSubmit({
values: {
collectionName: collectionName,
data: newData,
status: APPROVAL_ACTION_STATUS.RESUBMIT,
workflowId: workflowId,
collectionAppends: appends,
},
})
.then((res) => {
if (res?.data?.data) {
Toast.show({
icon: 'success',
content: '申请成功',
});
setTimeout(() => {
navigate(-1);
}, 1000);
}
})
.catch(() => {
return;
});
field.data.loading = false;
} catch (v) {
if (field.data) {
field.data.loading = false;
}
}
},
};
}

View File

@ -36,7 +36,9 @@ export function useCreateSubmit() {
content: '提交成功', content: '提交成功',
}); });
from.reset(); from.reset();
navigate(`/mobile/approval/${res.data.data.id}/page`); setTimeout(() => {
navigate(-1);
}, 1000);
} }
field.data.loading = false; field.data.loading = false;

View File

@ -3,31 +3,34 @@ import { useField, useForm } from '@tachybase/schema';
import { Toast } from 'antd-mobile'; import { Toast } from 'antd-mobile';
import _ from 'lodash'; import _ from 'lodash';
import { useNavigate } from 'react-router-dom';
import { useContextApprovalExecution } from '../../context/ApprovalExecution'; import { useContextApprovalExecution } from '../../context/ApprovalExecution';
import { useContextApprovalStatus } from '../provider/ApplyActionStatus'; import { useContextApprovalStatus } from '../provider/ApplyActionStatus';
export function useUpdateSubmit() { export function useUpdateSubmit() {
const from = useForm(); const form = useForm();
const field = useField(); const field = useField();
const { approval } = useContextApprovalExecution(); const { approval } = useContextApprovalExecution();
const { setSubmitted } = useActionContext() as any; const { setSubmitted } = useActionContext() as any;
const { workflow, id } = approval; const { workflow, id } = approval;
const contextApprovalStatus = useContextApprovalStatus(); const contextApprovalStatus = useContextApprovalStatus();
const apiClient = useAPIClient(); const apiClient = useAPIClient();
const navigate = useNavigate();
return { return {
async run() { async run() {
try { try {
from.submit(); form.submit();
_.set(field, ['data', 'loading'], true); _.set(field, ['data', 'loading'], true);
const res = await apiClient.resource('approvals').update({ const res = await apiClient.resource('approvals').update({
filterByTk: id, filterByTk: id,
values: { values: {
collectionName: workflow.config.collection, collectionName: workflow.config.collection,
data: from.values, data: form.values,
status: contextApprovalStatus, status: contextApprovalStatus,
schemaFormId: workflow.config.applyForm,
summaryConfig: workflow.config.summary,
}, },
}); });
if (res.status === 200) { if (res.status === 200) {
@ -36,7 +39,7 @@ export function useUpdateSubmit() {
content: '处理成功', content: '处理成功',
}); });
setTimeout(() => { setTimeout(() => {
location.reload(); navigate(-1);
}, 1000); }, 1000);
setSubmitted(true); setSubmitted(true);
} else { } else {

View File

@ -2,34 +2,34 @@ import { useActionContext, useAPIClient } from '@tachybase/client';
import { useField } from '@tachybase/schema'; import { useField } from '@tachybase/schema';
import { Toast } from 'antd-mobile'; import { Toast } from 'antd-mobile';
import { useNavigate } from 'react-router-dom';
import { useContextApprovalExecution } from '../../context/ApprovalExecution'; import { useContextApprovalExecution } from '../../context/ApprovalExecution';
// 撤回 // 撤回
export function useWithdrawAction() { export function useWithdrawAction() {
const field = useField(); const field = useField();
const { setVisible, setSubmitted } = useActionContext() as any;
const { approval } = useContextApprovalExecution(); const { approval } = useContextApprovalExecution();
const api = useAPIClient(); const api = useAPIClient();
const navigate = useNavigate();
return { return {
async run() { async run() {
try { try {
field.data = field.data ?? {}; field.data = field.data ?? {};
field.data.loading = true; field.data.loading = true;
const res = await api.resource('approvals').withdraw({ const res = await await api.resource('approvals').withdraw({
filterByTk: approval.id, filterByTk: approval.id,
}); });
if (res.status === 202) { if (res.status === 202) {
Toast.show({ Toast.show({
icon: 'success', icon: 'success',
content: '撤回成功', content: '撤回成功',
}); });
setTimeout(() => { setTimeout(() => {
location.reload(); navigate(-1);
}, 1000); }, 1000);
setSubmitted(true);
} else { } else {
Toast.show({ Toast.show({
icon: 'fail', icon: 'fail',

View File

@ -14,6 +14,7 @@ export function ActionBarProvider(props) {
APPROVAL_ACTION_STATUS.DRAFT, APPROVAL_ACTION_STATUS.DRAFT,
APPROVAL_ACTION_STATUS.RETURNED, APPROVAL_ACTION_STATUS.RETURNED,
APPROVAL_ACTION_STATUS.SUBMITTED, APPROVAL_ACTION_STATUS.SUBMITTED,
APPROVAL_ACTION_STATUS.RESUBMIT,
].includes(status); ].includes(status);
if (!isSameId || !isSameExcutionId || !isExcutionDid) { if (!isSameId || !isSameExcutionId || !isExcutionDid) {

View File

@ -0,0 +1,19 @@
import { useCurrentUserContext } from '@tachybase/client';
import { useContextApprovalExecution } from '../../context/ApprovalExecution';
export function ProviderActionResubmit(props) {
const { data } = useCurrentUserContext();
const { approval } = useContextApprovalExecution();
const { status, createdById } = approval;
const isSameId = data.data.id === createdById;
// const isEnabledWithdraw = workflow.enabled && workflow.config.withdrawable;
// const isNotDraft = status !== APPROVAL_STATUS.DRAFT;
if (isSameId) {
return props.children;
}
return null;
}

View File

@ -18,7 +18,11 @@ export function ApplyActionStatusProvider(props) {
const { data } = useCurrentUserContext(); const { data } = useCurrentUserContext();
const isSameId = data.data.id === createdById; const isSameId = data.data.id === createdById;
const isEnbled = workflow.enabled; const isEnbled = workflow.enabled;
const isStatusDid = [APPROVAL_ACTION_STATUS.DRAFT, APPROVAL_ACTION_STATUS.RETURNED].includes(status); const isStatusDid = [
APPROVAL_ACTION_STATUS.DRAFT,
APPROVAL_ACTION_STATUS.RETURNED,
APPROVAL_ACTION_STATUS.RESUBMIT,
].includes(status);
if (isSameId && isEnbled && isStatusDid) { if (isSameId && isEnbled && isStatusDid) {
return <ContextApprovalStatus.Provider value={value}>{children}</ContextApprovalStatus.Provider>; return <ContextApprovalStatus.Provider value={value}>{children}</ContextApprovalStatus.Provider>;

View File

@ -105,7 +105,7 @@ const changeApprovalRecordsService = (api, params, filter, cm, compile, t, setDa
const field = cm.getCollectionField(`${collectionName}.${key}`); const field = cm.getCollectionField(`${collectionName}.${key}`);
let resonValue = value; let resonValue = value;
if (field.type === 'date' && value) { if (field.type === 'date' && value) {
resonValue = dayjs(value as string).format('YYYY-MM-DD'); resonValue = dayjs(value as string).format('YYYY-MM-DD HH:mm:ss');
} }
return { return {
@ -220,7 +220,7 @@ export const changeWorkflowNoticeService = (api, t, cm, compile, input, setData,
const field = cm.getCollectionField(`${collectionName}.${key}`); const field = cm.getCollectionField(`${collectionName}.${key}`);
let resonValue = value; let resonValue = value;
if (field.type === 'date' && value) { if (field.type === 'date' && value) {
resonValue = dayjs(value as string).format('YYYY-MM-DD'); resonValue = dayjs(value as string).format('YYYY-MM-DD HH:mm:ss');
} }
return { return {
label: compile(field?.uiSchema?.title || key), label: compile(field?.uiSchema?.title || key),

View File

@ -67,5 +67,6 @@
"UserInitiations":"UserInitiations", "UserInitiations":"UserInitiations",
"Processed":"Processed", "Processed":"Processed",
"Duplicate":"Duplicate", "Duplicate":"Duplicate",
"Executed":"Executed" "Executed":"Executed",
"ReSubmit":"ReSubmit"
} }

View File

@ -98,5 +98,6 @@
"Manually canceled whole execution when waiting.": "等待时被手动取消整个执行。", "Manually canceled whole execution when waiting.": "等待时被手动取消整个执行。",
"Rejected from a manual node.": "被人工节点拒绝继续。", "Rejected from a manual node.": "被人工节点拒绝继续。",
"General failed but should do another try.": "执行失败,需重试。", "General failed but should do another try.": "执行失败,需重试。",
"Queueing": "队列中" "Queueing": "队列中",
"ReSubmit":"重新提交"
} }

View File

@ -80,10 +80,12 @@ export const AntdSelect = observer((props) => {
const checkedPopup = () => { const checkedPopup = () => {
if (collectionName) { if (collectionName) {
run(); run();
} else { } else if (fieldSchema['x-collection-field']) {
const field = cm.getCollectionField(fieldSchema['x-collection-field']); const field = cm.getCollectionField(fieldSchema['x-collection-field']);
const data = field.uiSchema?.enum; const data = field.uiSchema?.enum;
setOptions(data); setOptions(data);
} else {
setOptions(fieldSchema.enum);
} }
}; };
@ -122,7 +124,7 @@ export const AntdSelect = observer((props) => {
addData(data); addData(data);
}; };
return ( return (
<CollectionProvider name={collectionName || collection.name}> <CollectionProvider name={collectionName || collection?.name}>
<BlockItem> <BlockItem>
<div <div
onClick={() => { onClick={() => {