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,
ApplicationContext,
AssociationOrCollectionProvider,
Checkbox,
CollectionFieldOptions_deprecated,
CollectionRecordProvider,
createDesignable,
DataSourceApplicationProvider,
DatePicker,
Designable,
findFormBlock,
FormDialog,
FormProvider,
Radio,
RemoteSchemaComponent,
SchemaComponent,
SchemaComponentContext,
SchemaComponentOptions,
Select,
Cascader as TCascader,
Input as TInput,
useAPIClient,
useApp,
useBlockRequestContext,
@ -1009,6 +1015,17 @@ export const SchemaSettingsModalItem: FC<SchemaSettingsModalItemProps> = (props)
...others
} = props;
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 collection = useCollection_deprecated();
const apiClient = useAPIClient();

View File

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

View File

@ -16,8 +16,6 @@ import { UserInitiationsItem } from './component/UserInitiationsItem';
import '../style/style.css';
import { createGlobalStyle } from '@tachybase/client';
export const InitiationsBlockContext = createContext({});
export const InitiationsBlock = observer((props) => {
@ -67,28 +65,6 @@ export const InitiationsBlock = observer((props) => {
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">
<SchemaComponent
components={{
@ -111,6 +87,28 @@ export const InitiationsBlock = observer((props) => {
}}
/>
</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>
</InitiationsBlockContext.Provider>
</BlockItem>

View File

@ -1,6 +1,7 @@
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 { dayjs } from '@tachybase/utils/client';
import { useDeepCompareEffect } from 'ahooks';
import { Badge, Empty, List, Space, Tag } from 'antd-mobile';
@ -20,8 +21,10 @@ export const ApprovalItem = observer((props) => {
const user = useCurrentUserContext();
const contextFilter = useContext(InitiationsBlockContext);
const inputFilter = contextFilter['key'] === 'userInitiations' ? contextFilter['inputFilter'] : '';
const cm = useCollectionManager();
const compile = useCompile();
useDeepCompareEffect(() => {
changService(api, setData, user, { ...params?.[tabKey], ...filter }, t, setDefaultData);
changService(api, setData, user, { ...params?.[tabKey], ...filter }, t, setDefaultData, cm, compile);
}, [filter, params]);
useEffect(() => {
if (inputFilter && defaultData.length) {
@ -40,7 +43,7 @@ export const ApprovalItem = observer((props) => {
<List.Item
key={index}
onClick={() => {
navigate(`/mobile/approval/${item.id}/page`);
navigate(`/mobile/approval/${item.latestExecutionId}/page`);
}}
>
{/* <Badge color="#6ac3ff" content={Badge.dot} style={{ '--right': '100%' }}> */}
@ -54,7 +57,9 @@ export const ApprovalItem = observer((props) => {
</Tag>
</Space>
{/* </Badge> */}
<Space block> :{item.reason}</Space>
{item.reason?.map((reasonItem, index) => {
return <Space block key={index}>{`${reasonItem.label}:${reasonItem.value}`}</Space>;
})}
</List.Item>
);
})}
@ -71,7 +76,7 @@ const approvalTodoListStatus = (item, t) => {
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
.request({
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 statusType = approvalTodoListStatus(item, t);
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 {
...item,
title: `${user.data.data.nickname}${categoryTitle}`,
categoryTitle: categoryTitle,
statusTitle: t(statusType.label),
statusColor: statusType.color,
reason: item.data.reason || item.data.reason_pay,
reason: summary || [],
priorityTitle: priorityType.label,
priorityColor: priorityType.color,
};

View File

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

View File

@ -28,6 +28,9 @@ import '../../style/style.css';
import { MobileProvider } from '@tachybase/plugin-mobile-client/client';
import { useActionResubmit } from '../hook/useActionResubmit';
import { ProviderActionResubmit } from '../provider/ActionResubmit.provider';
export const ViewActionUserInitiationsContent = () => {
const params = useParams();
const navigate = useNavigate();
@ -41,7 +44,7 @@ export const ViewActionUserInitiationsContent = () => {
.request({
url: 'approvalExecutions:get',
params: {
filter: { approvalId: id },
filterByTk: id,
appends: [
'execution',
'execution.jobs',
@ -70,7 +73,7 @@ export const ViewActionUserInitiationsContent = () => {
.catch(() => {
console.error;
});
}, []);
}, [id]);
// @ts-ignore
const { approval, execution, ...approvalValue } = recordData || {};
const { workflow } = approval || {};
@ -147,6 +150,7 @@ const UserInitiationsComponent = (applyDetail) => {
WithdrawActionProvider,
DetailsBlockProvider,
MobileProvider,
ProviderActionResubmit,
}}
scope={{
useForm,
@ -155,6 +159,7 @@ const UserInitiationsComponent = (applyDetail) => {
useDetailsBlockProps: useFormBlockContext,
useWithdrawAction,
useDestroyAction,
useActionResubmit,
}}
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: '提交成功',
});
from.reset();
navigate(`/mobile/approval/${res.data.data.id}/page`);
setTimeout(() => {
navigate(-1);
}, 1000);
}
field.data.loading = false;

View File

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

View File

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

View File

@ -14,6 +14,7 @@ export function ActionBarProvider(props) {
APPROVAL_ACTION_STATUS.DRAFT,
APPROVAL_ACTION_STATUS.RETURNED,
APPROVAL_ACTION_STATUS.SUBMITTED,
APPROVAL_ACTION_STATUS.RESUBMIT,
].includes(status);
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 isSameId = data.data.id === createdById;
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) {
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}`);
let resonValue = 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 {
@ -220,7 +220,7 @@ export const changeWorkflowNoticeService = (api, t, cm, compile, input, setData,
const field = cm.getCollectionField(`${collectionName}.${key}`);
let resonValue = 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 {
label: compile(field?.uiSchema?.title || key),

View File

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

View File

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

View File

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