feat: init support mobile approvals (#1002)

Co-authored-by: wjh <wwwjh0710@163.com>
Co-authored-by: sealday <zhanglin@daoyoucloud.com>
Reviewed-on: daoyoucloud/tachybase#1002
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-17 20:50:25 +08:00 committed by sealday
parent 29d25466bd
commit b5c518fd4b
58 changed files with 2040 additions and 159 deletions

View File

@ -6,15 +6,18 @@
"@ant-design/icons": "5.x", "@ant-design/icons": "5.x",
"@tachybase/schema": "workspace:*", "@tachybase/schema": "workspace:*",
"@types/lodash": "^4.17.0", "@types/lodash": "^4.17.0",
"ahooks": "^3.7.2",
"antd": "5.16.1", "antd": "5.16.1",
"antd-mobile": "^5.35.0", "antd-mobile": "^5.35.0",
"antd-mobile-icons": "^0.3.0", "antd-mobile-icons": "^0.3.0",
"classnames": "^2.3.1", "classnames": "^2.3.1",
"lodash": "4.17.21", "lodash": "4.17.21",
"react-i18next": "^11.15.1" "react-i18next": "^11.15.1",
"react-router-dom": "6.x"
}, },
"peerDependencies": { "peerDependencies": {
"@tachybase/client": "workspace:*", "@tachybase/client": "workspace:*",
"@tachybase/plugin-workflow": "workspace:*",
"@tachybase/server": "workspace:*", "@tachybase/server": "workspace:*",
"@tachybase/test": "workspace:*", "@tachybase/test": "workspace:*",
"@tachybase/utils": "workspace:*" "@tachybase/utils": "workspace:*"

View File

@ -37,11 +37,24 @@ export const ApprovalBlockInitializer = () => {
export const ApprovalInitializerItem = [ export const ApprovalInitializerItem = [
{ {
type: 'item', type: 'itemGroup',
name: 'initiations', name: 'initiations',
title: '发起', title: '发起',
children: [
{
type: 'item',
name: 'initiationsApproval',
title: '发起申请',
itemComponent: 'InitiationsBlock', itemComponent: 'InitiationsBlock',
}, },
{
type: 'item',
name: 'currApproval',
title: '我发起的',
itemComponent: 'UserInitiationsBlock',
},
],
},
{ {
type: 'item', type: 'item',
title: '审批', title: '审批',

View File

@ -1,65 +0,0 @@
import { BlockItem, css } from '@tachybase/client';
import React from 'react';
import { AutoCenter, Card, SearchBar, Selector, Space } from 'antd-mobile';
import { TeamFill } from 'antd-mobile-icons';
export const InitiationsBlock = () => {
return (
<BlockItem>
<div style={{ padding: '10px' }}>
<SearchBar placeholder="请输入内容" clearable style={{ '--background': '#ffffff' }} />
<Card
className={css`
overflow: hidden;
margin-top: 20px;
.adm-card-body {
padding: 0;
}
`}
style={{ padding: '0', backgroundColor: '#f3f3f3' }}
>
<Selector
style={{ '--color': '#ffffff', '--gap': '1px' }}
columns={3}
showCheckMark={false}
options={[
{
value: '1',
label: (
<div style={{ display: 'flex', flexDirection: 'column', alignContent: 'center' }}>
<div>
<TeamFill />
</div>
11111
</div>
),
},
{
value: '2',
label: (
<div style={{ display: 'flex', flexDirection: 'column', alignContent: 'center' }}>
<div>
<TeamFill />
</div>
11111
</div>
),
},
{
value: '3',
label: (
<div style={{ display: 'flex', flexDirection: 'column', alignContent: 'center' }}>
<div>
<TeamFill />
</div>
11111
</div>
),
},
]}
/>
</Card>
</div>
</BlockItem>
);
};

View File

@ -0,0 +1,117 @@
import { createStyles, useCurrentUserContext } from '@tachybase/client';
import { EXECUTION_STATUS } from '@tachybase/plugin-workflow/client';
import _ from 'lodash';
import React, { useMemo } from 'react';
import { APPROVAL_ACTION_STATUS, APPROVAL_STATUS } from '../constants';
import { lang, usePluginTranslation } from '../locale';
import { useContextApprovalExecution } from '../context/ApprovalExecution';
import { ContextWithActionEnabled } from '../context/WithActionEnabled';
// 审批(发起/待办)区块-查看-审批处理
export const ApprovalProcess = (props) => {
const { t } = usePluginTranslation();
const { approval: approvalContext } = useContextApprovalExecution();
const { styles } = getStyles();
const { data } = useCurrentUserContext();
const results = useMemo(() => getResults({ approval: approvalContext, currentUser: data }), [approvalContext, data]);
// const columns = useMemo(() => getAntdTableColumns({ t, styles }), [t, styles]);
return (
<ContextWithActionEnabled.Provider value={{ actionEnabled: props.actionEnabled }}>
{/* <Space direction="vertical" size="middle" className={styles.layout}>
{results.map((item) => (
<Table key={item.id} dataSource={item.records} rowKey={'id'} pagination={false} columns={columns} />
))}
</Space> */}
</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_ACTION_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_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,
);
}
const getStepsResult = (result) => {
const stepData = result.map((value) => {});
};

View File

@ -1,70 +0,0 @@
import { connect, useFieldSchema } from '@tachybase/schema';
import { Badge, Empty, List, Space, Tag } from 'antd-mobile';
import React from 'react';
export const TabApprovalItem = () => {
const fieldSchema = useFieldSchema();
const props = fieldSchema['x-component-props'];
const blockData = props.approvalKey === 'duplicate' ? [] : data;
return (
<div style={{ marginTop: '10px' }}>
{blockData.length ? (
<List style={{ '--font-size': '12px' }}>
{blockData.map((item, index) => {
return (
<List.Item key={index}>
<Badge color="#6ac3ff" content={Badge.dot} style={{ '--right': '100%' }}>
<Space block>
{item.title}
<Tag color="primary" fill="outline">
</Tag>
</Space>
</Badge>
<Space block> xxxx:{item.context}</Space>
<Space block> xxxx:{item.price}</Space>
<Space block> xxxx:{item.context}</Space>
</List.Item>
);
})}
</List>
) : (
<Empty description="暂无数据" />
)}
</div>
);
};
const data = [
{
title: 'xx-xxxxx的申请',
context: 'xxxxxxxxx',
price: 'xxxxxxx',
type: '1',
status: '1',
date: '2024-01-08',
read: false,
applicantId: 9,
},
{
title: 'xx-xxxxx的申请',
context: 'xxxxxxxxx',
price: 'xxxxxxx',
type: '2',
status: '2',
date: '2024-01-09',
read: true,
applicantId: 10,
},
{
title: 'xx-xxxxx的申请',
context: 'xxxxxxxxx',
price: 'xxxxxxx',
type: '3',
status: '3',
date: '2024-01-010',
read: false,
applicantId: 11,
},
];

View File

@ -0,0 +1,108 @@
import { JOB_STATUS } from '@tachybase/plugin-workflow/client';
import { NAMESPACE, lang, tval } from './locale';
/**显示状态 */
export const APPROVAL_STATUS = {
/**已分配 */
ASSIGNED: null,
/** 待处理*/
PENDING: 0,
/**已退回 */
RETURNED: 1,
/**已通过 */
APPROVED: 2,
/**已拒绝 */
REJECTED: -1,
/**取消 */
CANCELED: -2,
/**撤回 */
WITHDRAWN: -3,
};
export const approvalStatusOptions = [
{ value: APPROVAL_STATUS.ASSIGNED, label: `Assigned`, color: 'blue' },
{ value: APPROVAL_STATUS.PENDING, label: `Pending`, color: 'gold' },
{ value: APPROVAL_STATUS.RETURNED, label: `Returned`, color: 'purple' },
{ value: APPROVAL_STATUS.APPROVED, label: `Approved`, color: 'green' },
{ value: APPROVAL_STATUS.REJECTED, label: `Rejected`, color: 'red' },
{ value: APPROVAL_STATUS.WITHDRAWN, label: `Withdrawn` },
];
/**行为状态 */
export const APPROVAL_ACTION_STATUS = {
/** 0草稿 */
DRAFT: 0,
/** 1已退回 */
RETURNED: 1,
/** 2提交 */
SUBMITTED: 2,
/** 3处理中 */
PROCESSING: 3,
/** 4已完结 */
APPROVED: 4,
/** -1拒收 */
REJECTED: -1,
};
export const ApprovalStatusEnums = [
{ value: APPROVAL_ACTION_STATUS.DRAFT, label: `Draft`, editable: true },
{
value: APPROVAL_ACTION_STATUS.RETURNED,
label: `Returned`,
color: 'purple',
editable: true,
},
{ value: APPROVAL_ACTION_STATUS.SUBMITTED, label: `Submitted`, color: 'cyan' },
{ 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' },
];
export const ApprovalPriorityType = [
{ value: '1', label: '一般', color: 'cyan' },
{ value: '2', label: '紧急', color: 'gold' },
{ value: '3', label: '非常紧急', color: 'red' },
];
export const ApprovalStatusEnumDict = ApprovalStatusEnums.reduce((e, t) => Object.assign(e, { [t.value]: t }), {});
export const JobStatusEnums = {
[JOB_STATUS.PENDING]: { color: 'gold', label: `Pending` },
[JOB_STATUS.RESOLVED]: { color: 'green', label: `Approved` },
[JOB_STATUS.REJECTED]: { color: 'red', label: `Rejected` },
[JOB_STATUS.RETRY_NEEDED]: { color: 'red', label: `Returned` },
};
export const VoteCategory = { SINGLE: Symbol('single'), ALL: Symbol('all'), VOTE: Symbol('vote') };
export const VoteCategoryEnums = [
{ value: VoteCategory.SINGLE, label: `Or"` },
{ value: VoteCategory.ALL, label: `And"` },
{
value: VoteCategory.VOTE,
label: (v: number) => `${lang('Voting')} ( > ${(v * 100).toFixed(0)}%)`,
},
].reduce((obj, vote) => Object.assign(obj, { [vote.value]: vote }), {});
export function voteOption(value: number) {
switch (true) {
case value === 1:
return VoteCategory.ALL;
case 0 < value && value < 1:
return VoteCategory.VOTE;
default:
return VoteCategory.SINGLE;
}
}
export function flatSchemaArray(sourceData, filter, needRecursion = false) {
const flatArray = [];
if (!sourceData) {
return flatArray;
}
if (filter(sourceData) && (!needRecursion || !sourceData.properties)) {
flatArray.push(sourceData);
} else {
sourceData.properties &&
Object.keys(sourceData.properties).forEach((key) => {
flatArray.push(...flatSchemaArray(sourceData.properties[key], filter));
});
}
return flatArray;
}

View File

@ -0,0 +1,8 @@
import { createContext, useContext } from 'react';
import { ApprovalExecution } from '../todos/interface/interface';
export const ContextApprovalExecution = createContext<Partial<ApprovalExecution>>({});
export function useContextApprovalExecution() {
return useContext(ContextApprovalExecution);
}

View File

@ -0,0 +1,76 @@
import React from 'react';
import {
BlockRequestContext_deprecated,
CollectionProvider_deprecated,
FormActiveFieldsProvider,
FormBlockContext,
FormProvider,
FormV2,
RecordProvider,
useAPIClient,
useAssociationNames,
useDesignable,
} from '@tachybase/client';
import { RecursionField, createForm, useField, useFieldSchema } from '@tachybase/schema';
import { Fragment, useContext, useMemo, useRef } from 'react';
import { useContextApprovalExecution } from './ApprovalExecution';
export const FormBlockProvider = (props) => {
const { approvalExecution } = useContextApprovalExecution();
const { snapshot } = approvalExecution;
const fieldSchema = useFieldSchema();
const field = useField();
const formBlockRef = useRef(null);
const { getAssociationAppends } = useAssociationNames();
const { appends, updateAssociationValues } = getAssociationAppends();
// @ts-ignore
const { findComponent } = useDesignable();
const ContainerFormComp = findComponent(field.component?.[0]) || Fragment;
const form = useMemo(() => createForm({ initialValues: snapshot }), [snapshot]);
const params = useMemo(() => ({ ...appends, ...props.params }), [appends, props.params]);
const service = useMemo(() => ({ loading: false, data: { data: snapshot } }), [snapshot]);
const collectionResource = useAPIClient().resource(props.collection);
const blockContext = useContext(BlockRequestContext_deprecated);
const formValue = useMemo(
() => ({
params,
form,
field,
service,
updateAssociationValues,
formBlockRef,
}),
[field, form, params, service, updateAssociationValues],
);
return (
<CollectionProvider_deprecated dataSource={props.dataSource} collection={props.collection}>
{/* @ts-ignore */}
<RecordProvider record={snapshot}>
<FormActiveFieldsProvider name="form">
<BlockRequestContext_deprecated.Provider
value={{
block: 'form',
props: props,
field: field,
service: service,
resource: collectionResource,
__parent: blockContext,
}}
>
<FormBlockContext.Provider value={formValue}>
<ContainerFormComp {...field.componentProps}>
<FormV2.Templates style={{ marginBottom: 18 }} form={form} />
<FormProvider form={form}>
<div ref={formBlockRef}>
<RecursionField schema={fieldSchema} onlyRenderProperties />
</div>
</FormProvider>
</ContainerFormComp>
</FormBlockContext.Provider>
</BlockRequestContext_deprecated.Provider>
</FormActiveFieldsProvider>
</RecordProvider>
</CollectionProvider_deprecated>
);
};

View File

@ -0,0 +1,11 @@
import { SchemaComponentContext, useSchemaComponentContext } from '@tachybase/client';
import React from 'react';
export function SchemaComponentContextProvider({ designable, children }) {
const schemaComponentContext = useSchemaComponentContext();
return (
<SchemaComponentContext.Provider value={{ ...schemaComponentContext, designable }}>
{children}
</SchemaComponentContext.Provider>
);
}

View File

@ -0,0 +1,3 @@
import { createContext } from 'react';
export const ContextWithActionEnabled = createContext({ actionEnabled: false });

View File

@ -1,8 +1,13 @@
import { Plugin } from '@tachybase/client'; import { Plugin } from '@tachybase/client';
import { ApprovalBlockInitializer } from './ApprovalBlockInitializer'; import { ApprovalBlockInitializer } from './ApprovalBlockInitializer';
import { ApprovalSettings } from './ApprovalSettings'; import { ApprovalSettings } from './ApprovalSettings';
import { TodosBlock } from './TodosBlock'; import { TodosBlock } from './todos/TodosBlock';
import { InitiationsBlock } from './InitiationsBlock'; import { InitiationsBlock } from './initiations/InitiationsBlock';
import { LauncherActionConfigInitializer } from './initiations/config/LauncherActionConfig';
import { ViewActionTodosContent } from './todos/component/ViewActionTodosContent';
import { UserInitiationsBlock } from './initiations/UserInitiationsBlock';
import { ViewActionUserInitiationsContent } from './initiations/component/ViewActionUserInitiationsContent';
import { ApprovalProcess } from './component/ApprovalProcess.view';
class PluginApproval extends Plugin { class PluginApproval extends Plugin {
async load() { async load() {
@ -10,6 +15,10 @@ class PluginApproval extends Plugin {
ApprovalBlockInitializer, ApprovalBlockInitializer,
InitiationsBlock, InitiationsBlock,
TodosBlock, TodosBlock,
UserInitiationsBlock,
ViewActionTodosContent,
ViewActionUserInitiationsContent,
'ApprovalCommon.ViewComponent.MApprovalProcess': ApprovalProcess,
}); });
this.app.schemaSettingsManager.add(ApprovalSettings); this.app.schemaSettingsManager.add(ApprovalSettings);
this.app.schemaInitializerManager.addItem('mobilePage:addBlock', 'otherBlocks.approval', { this.app.schemaInitializerManager.addItem('mobilePage:addBlock', 'otherBlocks.approval', {
@ -18,6 +27,7 @@ class PluginApproval extends Plugin {
type: 'item', type: 'item',
Component: 'ApprovalBlockInitializer', Component: 'ApprovalBlockInitializer',
}); });
this.app.schemaInitializerManager.add(LauncherActionConfigInitializer);
} }
} }

View File

@ -0,0 +1,65 @@
import { BlockItem, css, useAPIClient, useRequest } from '@tachybase/client';
import React, { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { AutoCenter, Card, SearchBar, Selector, Space } from 'antd-mobile';
import { TeamFill } from 'antd-mobile-icons';
export const InitiationsBlock = () => {
const [options, setOptions] = useState([]);
const api = useAPIClient();
const navigate = useNavigate();
useEffect(() => {
api
.request({
url: 'workflows:list',
params: { pageSize: 9999, filter: { type: { $eq: 'approval' }, enabled: { $eq: true } } },
})
.then((res) => {
const option = res?.data?.data.map((value) => {
return {
...value,
value: value.id,
label: (
<div
style={{ display: 'flex', flexDirection: 'column', alignContent: 'center' }}
onClick={(c) => {
navigate(`/mobile/${value.config.collection}/approval/${value.id}/page`);
}}
>
<div>
<TeamFill />
</div>
{value.title?.replace('审批流:', '') || ''}
</div>
),
};
});
setOptions(option);
})
.catch(() => {});
}, []);
return (
<BlockItem>
<div style={{ padding: '10px', height: '90vh' }}>
<SearchBar placeholder="请输入内容" clearable style={{ '--background': '#ffffff' }} />
<Card
className={css`
overflow: hidden;
margin-top: 20px;
.adm-card-body {
padding: 0;
}
`}
style={{ padding: '0', backgroundColor: '#f3f3f3' }}
>
<Selector
style={{ '--color': '#ffffff', '--gap': '1px' }}
columns={3}
showCheckMark={false}
options={options}
/>
</Card>
</div>
</BlockItem>
);
};

View File

@ -0,0 +1,35 @@
import { BlockItem, SchemaComponent, css } from '@tachybase/client';
import { Divider, SearchBar, Space, Tabs } from 'antd-mobile';
import React, { useState } from 'react';
import { useFieldSchema } from '@tachybase/schema';
import { ApprovalTemplateType } from './component/ApprovalTemplateType';
import { ApprovalStatus } from './component/ApprovalStatus';
import { ApprovalItem } from './component/ApprovalItem';
import { ApprovalReachDataType } from './component/ApprovalReachDataType';
export const UserInitiationsBlock = () => {
const fieldSchema = useFieldSchema();
fieldSchema['x-component-props'] = fieldSchema['x-component-props']?.['approvalKey']
? fieldSchema['x-component-props']
: (() => {
fieldSchema['x-component-props']['approvalKey'] = 'pending';
return fieldSchema['x-component-props'];
})();
const props = fieldSchema['x-component-props'];
return (
<BlockItem>
<SearchBar placeholder="搜索人名,标题、内容" clearable style={{ '--background': '#ffffff', padding: '10px' }} />
<Space justify="evenly" style={spaceStyle}>
{/* 模版类型 */}
<ApprovalTemplateType />
{/* 到达日期 */}
<ApprovalReachDataType />
{/* 审批状态 */}
<ApprovalStatus />
</Space>
<ApprovalItem />
</BlockItem>
);
};
const spaceStyle = { width: '100%', fontSize: '10px', color: '#8e8e8e' };

View File

@ -0,0 +1,90 @@
import { useAPIClient, useCollectionManager, useCurrentUserContext } from '@tachybase/client';
import { useFieldSchema } from '@tachybase/schema';
import { Badge, Empty, List, Space, Tag } from 'antd-mobile';
import React, { useState } from 'react';
import { useAsyncEffect } from 'ahooks';
import { ApprovalPriorityType, ApprovalStatusEnums } from '../../constants';
import { useNavigate } from 'react-router-dom';
import { useTranslation } from '../../locale';
export const ApprovalItem = () => {
const fieldSchema = useFieldSchema();
const props = fieldSchema['x-component-props'];
const cm = useCollectionManager();
const api = useAPIClient();
const [data, setData] = useState([]);
const { t } = useTranslation();
const navigate = useNavigate();
const user = useCurrentUserContext();
useAsyncEffect(async () => {
api
.request({
url: 'approvals:listCentralized',
params: { pageSize: 99999, appends: ['workflow'] },
})
.then((res) => {
const result = res.data?.data.map((item) => {
const priorityType = ApprovalPriorityType.find((priorityItem) => priorityItem.value === item.data.priority);
const statusType = approvalTodoListStatus(item, t);
const categoryTitle = item.workflow.title.replace('审批流:', '');
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,
priorityTitle: priorityType.label,
priorityColor: priorityType.color,
};
});
result.sort((a, b) => {
return Date.parse(b.createdAt) - Date.parse(a.createdAt);
});
setData(result);
})
.catch(() => {
console.error;
});
}, props);
return (
<div style={{ marginTop: '10px', minHeight: '70vh' }}>
{data.length ? (
<List style={{ '--font-size': '12px' }}>
{data.map((item, index) => {
return (
<List.Item
key={index}
onClick={() => {
navigate(`/mobile/approval/${item.id}/page`);
}}
>
<Badge color="#6ac3ff" content={Badge.dot} style={{ '--right': '100%' }}>
<Space block>
{item.title}
<Tag color={item.statusColor} fill="outline">
{item.statusTitle}
</Tag>
<Tag color={item.priorityColor} fill="outline">
{item.priorityTitle}
</Tag>
</Space>
</Badge>
<Space block> :{item.reason}</Space>
</List.Item>
);
})}
</List>
) : (
<Empty description="暂无数据" />
)}
</div>
);
};
const approvalTodoListStatus = (item, t) => {
const { status } = item;
return ApprovalStatusEnums.find((value) => value.value === status);
};

View File

@ -0,0 +1,31 @@
import { Picker, Space } from 'antd-mobile';
import { DownOutline } from 'antd-mobile-icons';
import React, { useState } from 'react';
export const ApprovalReachDataType = () => {
const [visible, setVisible] = useState(false);
return (
<>
<Space>
<DownOutline />
</Space>
<Picker
columns={columns}
visible={visible}
onClose={() => {
setVisible(false);
}}
/>
</>
);
};
const columns = [
[
{ label: '全部', value: '1' },
{ label: '近7日', value: '2' },
{ label: '近30日', value: '3' },
{ label: '自定义区间', value: '4' },
],
];

View File

@ -0,0 +1,43 @@
import { Picker, Space } from 'antd-mobile';
import { DownOutline } from 'antd-mobile-icons';
import React, { useState } from 'react';
import { useTranslation } from '../../locale';
export const ApprovalStatus = () => {
const [visible, setVisible] = useState(false);
const { t } = useTranslation();
return (
<>
<Space
onClick={() => {
setVisible(true);
}}
>
<DownOutline />
</Space>
<Picker
columns={[
columns[0].map((value) => {
return { ...value, label: t(value.label) };
}),
]}
visible={visible}
onClose={() => {
setVisible(false);
}}
/>
</>
);
};
const columns = [
[
{ label: 'Draft', value: '0' },
{ label: 'Returned', value: '1' },
{ label: 'Submitted', value: '2' },
{ label: 'Processing', value: '3' },
{ label: 'Approved', value: '4' },
{ label: 'Rejected', value: '-1' },
],
];

View File

@ -0,0 +1,37 @@
import { Picker, Space } from 'antd-mobile';
import { DownOutline } from 'antd-mobile-icons';
import React, { useState } from 'react';
export const ApprovalTemplateType = () => {
const [visible, setVisible] = useState(false);
return (
<>
<Space
onClick={() => {
setVisible(true);
}}
>
<DownOutline />
</Space>
<Picker
columns={columns}
visible={visible}
onClose={() => {
setVisible(false);
}}
/>
</>
);
};
const columns = [
[
{ label: '全部', value: '0' },
{ label: '入职申请', value: '1' },
{ label: '转正申请', value: '2' },
{ label: '调用申请', value: '3' },
{ label: '离职申请', value: '4' },
{ label: 'xxx申请', value: '5' },
],
];

View File

@ -0,0 +1,175 @@
import React, { useEffect, useState } from 'react';
import {
RemoteSchemaComponent,
SchemaComponent,
SchemaComponentProvider,
useAPIClient,
useDestroyAction,
useFormBlockContext,
useRecord,
useRequest,
} from '@tachybase/client';
import { DetailsBlockProvider, FlowContext } from '@tachybase/plugin-workflow/client';
import { useForm } from '@tachybase/schema';
import { Result, Spin } from 'antd';
import { useContext } from 'react';
import { ContextWithActionEnabled } from '../../context/WithActionEnabled';
import { SchemaComponentContextProvider } from '../../context/SchemaComponent';
import { NavBar, Skeleton, TabBar } from 'antd-mobile';
import { useNavigate, useParams } from 'react-router-dom';
import { FormBlockProvider } from '../../context/FormBlock';
import { ActionBarProvider } from '../provider/ActionBar';
import { ApplyActionStatusProvider } from '../provider/ApplyActionStatus';
import { WithdrawActionProvider } from '../provider/WithdrawAction';
import { ContextApprovalExecution } from '../../context/ApprovalExecution';
import { useSubmit } from '../hook/useSubmit';
import { useFormBlockProps } from '../hook/useFormBlockProps';
import { useWithdrawAction } from '../hook/useWithdrawAction';
import { FileOutline, UserContactOutline } from 'antd-mobile-icons';
import { useTranslation } from '../../locale';
import '../../style/style.css';
export const ViewActionUserInitiationsContent = () => {
const params = useParams();
const navigate = useNavigate();
const { id } = params;
const { actionEnabled } = useContext(ContextWithActionEnabled);
const [noDate, setNoDate] = useState(false);
const [recordData, setRecordDate] = useState({});
const [currContext, setCurrContext] = useState('formContext');
const { t } = useTranslation();
const api = useAPIClient();
useEffect(() => {
api
.request({
url: 'approvalExecutions:get',
params: {
filter: { approvalId: id },
appends: [
'execution',
'execution.jobs',
'approval',
'approval.workflow',
'approval.workflow.nodes',
'approval.approvalExecutions',
'approval.createdBy.id',
'approval.createdBy.nickname',
'approval.records',
'approval.records.node.title',
'approval.records.node.config',
'approval.records.job',
'approval.records.user.nickname',
],
except: ['approval.approvalExecutions.snapshot', 'approval.records.snapshot'],
},
})
.then((res) => {
if (res.data?.data) {
setRecordDate(res.data.data);
} else {
setNoDate(true);
}
})
.catch(() => {
console.error;
});
}, []);
// @ts-ignore
const { approval, execution, ...approvalValue } = recordData || {};
const { workflow } = approval || {};
return (
<div style={{ minHeight: '100vh', backgroundColor: '#f3f3f3', overflow: 'auto' }}>
<NavBar
onBack={() => {
navigate(-1);
}}
className="navBarStyle"
>
{'审批'}
</NavBar>
<ContextApprovalExecution.Provider value={recordData}>
<div className="approvalContext">
{Object.keys(recordData).length && !noDate ? (
<ContextApprovalExecution.Provider value={recordData}>
{UserInitiationsComponent(workflow?.config.applyForm, t, currContext)}
<TabBar
onChange={(item) => {
setCurrContext(item);
}}
className="tabsBarStyle"
>
<TabBar.Item key="formContext" icon={<FileOutline />} title="申请内容" />
{actionEnabled ? null : <TabBar.Item key="approval" icon={<UserContactOutline />} title="审批处理" />}
</TabBar>
</ContextApprovalExecution.Provider>
) : (
<div>
<Skeleton.Title animated />
<Skeleton.Paragraph lineCount={5} animated />
</div>
)}
</div>
</ContextApprovalExecution.Provider>
</div>
);
};
const UserInitiationsComponent = (applyDetail, t, currContext) => {
const formContextSchema = {
type: 'void',
'x-component': 'MPage',
'x-designer': 'MPage.Designer',
'x-component-props': {},
properties: {
Approval: {
type: 'void',
'x-decorator': 'SchemaComponentContextProvider',
'x-decorator-props': { designable: false },
'x-component': 'RemoteSchemaComponent',
'x-component-props': {
uid: applyDetail,
noForm: true,
},
},
},
};
const approvalSchema = {
// type: 'void',
// 'x-component': 'MPage',
// 'x-designer': 'MPage.Designer',
// 'x-component-props': {},
// properties: {
// process: {
// type: 'void',
// 'x-decorator': 'CardItem',
// 'x-component': 'ApprovalCommon.ViewComponent.ApprovalProcess',
// },
// },
};
return (
<SchemaComponent
components={{
SchemaComponentProvider: SchemaComponentProvider,
RemoteSchemaComponent: RemoteSchemaComponent,
SchemaComponentContextProvider,
FormBlockProvider,
ActionBarProvider,
ApplyActionStatusProvider,
WithdrawActionProvider,
DetailsBlockProvider,
}}
scope={{
useForm,
useSubmit: useSubmit,
useFormBlockProps,
useDetailsBlockProps: useFormBlockContext,
useWithdrawAction,
useDestroyAction,
}}
schema={currContext === 'formContext' ? formContextSchema : approvalSchema}
/>
);
};

View File

@ -0,0 +1,28 @@
import { SchemaInitializer } from '@tachybase/client';
import { LauncherActionConfigComponent } from './LauncherActionConfigComponent';
import { NAMESPACE } from '../../locale';
import { APPROVAL_ACTION_STATUS, APPROVAL_STATUS } from '../../constants';
// 区块-配置操作
export const LauncherActionConfigInitializer = new SchemaInitializer({
name: 'ApprovalApplyAddActionButton',
title: '{{t("Configure actions")}}',
items: [
{
name: 'submit',
type: 'item',
title: '{{t("Submit")}}',
Component: LauncherActionConfigComponent,
action: APPROVAL_ACTION_STATUS.SUBMITTED,
actionProps: { type: 'primary' },
disabled: true,
},
{
name: 'save',
type: 'item',
title: `{{t("Save draft", { ns: "${NAMESPACE}" })}}`,
Component: LauncherActionConfigComponent,
action: APPROVAL_ACTION_STATUS.DRAFT,
},
],
});

View File

@ -0,0 +1,31 @@
import { ActionInitializer, useSchemaInitializerItem } from '@tachybase/client';
import React from 'react';
// 区块-配置操作
export const LauncherActionConfigComponent = () => {
const itemConfig = useSchemaInitializerItem();
const { action, actionProps = {}, ...restItemConfig } = itemConfig;
return (
<ActionInitializer
{...restItemConfig}
schema={{
type: 'void',
title: restItemConfig.title,
'x-decorator': 'ApplyActionStatusProvider',
'x-decorator-props': {
value: action,
},
'x-component': 'Action',
'x-component-props': {
...actionProps,
useAction: '{{ useSubmit }}',
},
'x-designer': 'Action.Designer',
'x-action': `${action}`,
'x-action-settings': {
assignedValues: {},
},
}}
/>
);
};

View File

@ -0,0 +1,27 @@
import { useAPIClient, useActionContext } from '@tachybase/client';
import { useField } from '@tachybase/schema';
import _ from 'lodash';
import { useContextApprovalExecution } from '../../context/ApprovalExecution';
export function useDestroyAction() {
const field = useField();
const { setVisible, setSubmitted } = useActionContext() as any;
const { approval } = useContextApprovalExecution();
const apiClient = useAPIClient();
return {
async run() {
try {
_.set(field, ['data', 'loading'], true);
await apiClient.resource('approvals').destroy({
filterByTk: approval.id,
});
setSubmitted(true);
} catch (err) {
_.set(field, ['data', 'loading'], false);
}
},
};
}

View File

@ -0,0 +1,32 @@
import { useCurrentUserContext } from '@tachybase/client';
import { useFlowContext } from '@tachybase/plugin-workflow/client';
import { useForm } from '@tachybase/schema';
import { useEffect } from 'react';
import { useContextApprovalExecution } from '../../context/ApprovalExecution';
import { ApprovalStatusEnums } from '../../constants';
export function useFormBlockProps() {
const { approval, id } = useContextApprovalExecution();
const { workflow } = approval;
const form = useForm();
const { data } = useCurrentUserContext();
const { editable } = ApprovalStatusEnums.find((value) => value.value === approval.status);
const needEditable =
editable && approval?.latestExecutionId === id && approval?.createdById === data?.data.id && workflow.enabled;
useEffect(() => {
if (!approval) {
return;
}
if (needEditable) {
form.setPattern('editable');
} else {
form.setPattern('readPretty');
}
}, [form, approval, needEditable]);
return { form };
}

View File

@ -0,0 +1,42 @@
import { useAPIClient, useActionContext } from '@tachybase/client';
import { useFlowContext } from '@tachybase/plugin-workflow/client';
import { useField, useForm } from '@tachybase/schema';
import _ from 'lodash';
import { useContextApprovalExecution } from '../../context/ApprovalExecution';
import { useContextApprovalStatus } from '../provider/ApplyActionStatus';
export function useSubmit() {
const from = useForm();
const field = useField();
const { approval } = useContextApprovalExecution();
const { setVisible, setSubmitted } = useActionContext() as any;
const { id } = approval;
const { workflow } = useFlowContext();
const contextApprovalStatus = useContextApprovalStatus();
const apiClient = useAPIClient();
return {
async run() {
try {
from.submit();
_.set(field, ['data', 'loading'], true);
apiClient.resource('approvals').update({
filterByTk: id,
values: {
collectionName: workflow.config.collection,
data: from.values,
status: contextApprovalStatus,
},
});
setSubmitted(true);
setVisible(false);
from.reset();
_.set(field, ['data', 'loading'], false);
} catch (m) {
_.set(field, ['data', 'loading'], false);
}
},
};
}

View File

@ -0,0 +1,31 @@
import { useAPIClient, useActionContext } from '@tachybase/client';
import { useField } from '@tachybase/schema';
import { useContextApprovalExecution } from '../../context/ApprovalExecution';
// 撤回
export function useWithdrawAction() {
const field = useField();
const { setVisible, setSubmitted } = useActionContext() as any;
const { approval } = useContextApprovalExecution();
const api = useAPIClient();
return {
async run() {
try {
field.data = field.data ?? {};
field.data.loading = true;
await api.resource('approvals').withdraw({
filterByTk: approval.id,
});
setSubmitted(true);
field.data.loading = false;
} catch (v) {
if (field.data) {
field.data.loading = false;
}
}
},
};
}

View File

@ -0,0 +1,23 @@
import { useCurrentUserContext } from '@tachybase/client';
import { useContextApprovalExecution } from '../../context/ApprovalExecution';
import { APPROVAL_ACTION_STATUS } from '../../constants';
export function ActionBarProvider(props) {
const { data } = useCurrentUserContext();
const { approval, id } = useContextApprovalExecution();
const { status, createdById, latestExecutionId } = approval;
const isSameId = data.data.id === createdById;
const isSameExcutionId = latestExecutionId === id;
const isExcutionDid = [
APPROVAL_ACTION_STATUS.DRAFT,
APPROVAL_ACTION_STATUS.RETURNED,
APPROVAL_ACTION_STATUS.SUBMITTED,
].includes(status);
if (!isSameId || !isSameExcutionId || !isExcutionDid) {
return null;
}
return props.children;
}

View File

@ -0,0 +1,28 @@
import React, { useContext } from 'react';
import { useCurrentUserContext } from '@tachybase/client';
import { useFlowContext } from '@tachybase/plugin-workflow/client';
import { createContext } from 'react';
import { APPROVAL_ACTION_STATUS } from '../../constants';
import { useContextApprovalExecution } from '../../context/ApprovalExecution';
const ContextApprovalStatus = createContext(APPROVAL_ACTION_STATUS.SUBMITTED);
export function useContextApprovalStatus() {
return useContext(ContextApprovalStatus);
}
export function ApplyActionStatusProvider(props) {
const { value, children } = props;
const { approval } = useContextApprovalExecution();
const { status, createdById, workflow } = approval;
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);
if (isSameId && isEnbled && isStatusDid) {
return <ContextApprovalStatus.Provider value={value}>{children}</ContextApprovalStatus.Provider>;
}
return null;
}

View File

@ -0,0 +1,18 @@
import { useCurrentUserContext } from '@tachybase/client';
import { APPROVAL_ACTION_STATUS } from '../../constants';
import { useContextApprovalExecution } from '../../context/ApprovalExecution';
export function WithdrawActionProvider({ children }) {
const { data } = useCurrentUserContext();
const { approval } = useContextApprovalExecution();
const { status, createdById, workflow } = approval;
const isSameId = data.data.id === createdById;
const isEnabledWithdraw = workflow.enabled && workflow.config.withdrawable;
const isStatusSubmitted = APPROVAL_ACTION_STATUS.SUBMITTED === status;
if (isSameId && isEnabledWithdraw && isStatusSubmitted) {
return children;
}
return null;
}

View File

@ -1,15 +1,26 @@
import { useApp, tval as nTval, i18n } from '@tachybase/client'; import { tval as nTval, i18n } from '@tachybase/client';
const NAMESPACE = '@hera/plugin-approval-mobile'; export const NAMESPACE = '@hera/plugin-approval-mobile';
export const useTranslation = (): any => { export function usePluginTranslation(): any {
const { i18n } = useApp(); return useTranslation();
const t = (key: string, props = {}) => i18n.t(key, { ns: NAMESPACE, ...props });
return { t };
};
export const tval = (key: string) => nTval(key, { ns: NAMESPACE });
export function lang(key: string) {
return i18n.t(key, { ns: NAMESPACE });
} }
export function useTranslation() {
const t = (key: string, options = {}) => i18n.t(key, { ns: NAMESPACE, ...options });
return { t };
}
export function lang(key: string, options = {}) {
return i18n.t(key, {
...options,
ns: NAMESPACE,
});
}
export const tval = (key: string, haveNamespace: boolean = true) => {
if (haveNamespace) {
return nTval(key, { ns: NAMESPACE });
} else {
return nTval(key);
}
};

View File

@ -0,0 +1,21 @@
.tabsBarStyle{
position: fixed;
bottom: 0;
z-index: 2;
height: 5vh;
width: 100vw;
background-color: #ffffff;
}
.navBarStyle{
background-color: #ffffff;
margin-bottom: 10px;
position: fixed;
z-index: 2;
height: 5vh;
width: 100vw;
}
.approvalContext{
margin: 6vh 0 6vh 0;
}

View File

@ -0,0 +1,100 @@
import { useAPIClient, useCollectionManager, useRequest } from '@tachybase/client';
import { connect, useFieldSchema } from '@tachybase/schema';
import { Badge, Empty, List, Space, Tag } from 'antd-mobile';
import React, { useEffect, useState } from 'react';
import { useAsyncEffect } from 'ahooks';
import { APPROVAL_STATUS, ApprovalPriorityType, approvalStatusOptions } from '../../constants';
import { useNavigate } from 'react-router-dom';
import { tval, useTranslation } from '../../locale';
export const TabApprovalItem = () => {
const fieldSchema = useFieldSchema();
const props = fieldSchema['x-component-props'];
const cm = useCollectionManager();
const api = useAPIClient();
const [data, setData] = useState([]);
const { t } = useTranslation();
const navigate = useNavigate();
useAsyncEffect(async () => {
const { data: user } = await api.request({ url: 'users:list', params: { pageSize: 999, appends: ['roles'] } });
api
.request({
url: 'approvalRecords:listCentralized',
params: { pageSize: 99999, appends: ['execution', 'job', 'node', 'workflow'] },
})
.then((res) => {
const result = res.data?.data.map((item) => {
const itemUser = user.data.find((value) => value.id === item.userId);
const priorityType = ApprovalPriorityType.find(
(priorityItem) => priorityItem.value === item.snapshot.priority,
);
const statusType = approvalTodoListStatus(item, t);
const categoryTitle = item.workflow.title.replace('审批流:', '');
return {
...item,
title: `${itemUser.nickname}${categoryTitle}`,
categoryTitle: categoryTitle,
statusTitle: t(statusType.label),
statusColor: statusType.color,
reason: item.snapshot.reason || item.snapshot.reason_pay,
priorityTitle: priorityType.label,
priorityColor: priorityType.color,
};
});
result.sort((a, b) => {
return Date.parse(b.createdAt) - Date.parse(a.createdAt);
});
setData(result);
})
.catch(() => {
console.error;
});
}, props);
return (
<div style={{ marginTop: '10px', minHeight: '70vh' }}>
{data.length ? (
<List style={{ '--font-size': '12px' }}>
{data.map((item, index) => {
return (
<List.Item
key={index}
onClick={() => {
navigate(`/mobile/approval/${item.id}/${item.categoryTitle}/detailspage`);
}}
>
<Badge color="#6ac3ff" content={Badge.dot} style={{ '--right': '100%' }}>
<Space block>
{item.title}
<Tag color={item.statusColor} fill="outline">
{item.statusTitle}
</Tag>
<Tag color={item.priorityColor} fill="outline">
{item.priorityTitle}
</Tag>
</Space>
</Badge>
<Space block> :{item.reason}</Space>
</List.Item>
);
})}
</List>
) : (
<Empty description="暂无数据" />
)}
</div>
);
};
const approvalTodoListStatus = (item, t) => {
const { workflow, execution, job, status } = item;
if (
(!(workflow != null && workflow.enabled) || (execution != null && execution.stauts) || job?.status) &&
[APPROVAL_STATUS.ASSIGNED, APPROVAL_STATUS.PENDING].includes(status)
) {
return { label: t('Unprocessed'), color: 'default' };
} else {
return approvalStatusOptions.find((value) => value.value === status);
}
};

View File

@ -0,0 +1,186 @@
import {
RemoteSchemaComponent,
SchemaComponent,
SchemaComponentProvider,
useAPIClient,
useFormBlockContext,
useRecord,
useRequest,
} from '@tachybase/client';
import { Result } from 'antd';
import _ from 'lodash';
import React, { useContext, useEffect, useState } from 'react';
import { useTranslation } from '../../locale';
import { DetailsBlockProvider } from '@tachybase/plugin-workflow/client';
import { ContextWithActionEnabled } from '../../context/WithActionEnabled';
import { ContextApprovalExecution } from '../../context/ApprovalExecution';
import { NavBar, Skeleton, TabBar } from 'antd-mobile';
import { useNavigate, useParams } from 'react-router-dom';
import { SchemaComponentContextProvider } from '../../context/SchemaComponent';
import { ApprovalActionProvider } from '../provider/ApprovalAction';
import { ApprovalFormBlockDecorator } from '../provider/ApprovalFormBlock';
import { useApprovalDetailBlockProps } from '../hook/useApprovalDetailBlockProps';
import { useApprovalFormBlockProps } from '../hook/useApprovalFormBlockProps';
import { useSubmit } from '../hook/useSubmit';
import { CouponOutline, FileOutline, ScanCodeOutline, UserContactOutline } from 'antd-mobile-icons';
import { ActionBarProvider } from '../provider/ActionBarProvider';
import { FormBlockProvider } from '../../context/FormBlock';
import '../../style/style.css';
// 审批-待办-查看: 内容
export const ViewActionTodosContent = () => {
const { t } = useTranslation();
const { actionEnabled } = useContext(ContextWithActionEnabled);
const navigate = useNavigate();
const params = useParams();
const { id } = params;
const api = useAPIClient();
const [noDate, setNoDate] = useState(false);
const [recordData, setRecordDate] = useState({});
const [currContext, setCurrContext] = useState('formContext');
useEffect(() => {
api
.request({
url: 'approvalRecords:get',
params: {
filter: { id },
appends: [
'approvalExecution',
'node',
'job',
'workflow',
'workflow.nodes',
'execution',
'execution.jobs',
'user',
'approval',
'approval.createdBy',
'approval.approvalExecutions',
'approval.createdBy.nickname',
'approval.records',
'approval.records.node.title',
'approval.records.node.config',
'approval.records.job',
'approval.records.user.nickname',
],
except: [
'approval.data',
'approval.approvalExecutions.snapshot',
'approval.records.snapshot',
'workflow.config',
'workflow.options',
'nodes.config',
],
sort: ['-createdAt'],
},
})
.then((res) => {
if (res.data?.data) {
setRecordDate(res.data.data);
} else {
setNoDate(true);
}
})
.catch(() => {
console.error;
});
}, []);
if (noDate) {
return <Result status="error" title={t('Submission may be withdrawn, please try refresh the list.')} />;
}
const { node } = recordData as any;
return (
<div style={{ minHeight: '100vh', backgroundColor: '#f3f3f3', overflow: 'auto' }}>
<NavBar
onBack={() => {
navigate(-1);
}}
className="navBarStyle"
>
{'审批'}
</NavBar>
<div className="approvalContext">
{Object.keys(recordData).length && !noDate ? (
<ContextApprovalExecution.Provider value={recordData}>
{todosComponent(node?.config.applyDetail, t, currContext)}
<TabBar
onChange={(item) => {
setCurrContext(item);
}}
className="tabsBarStyle"
>
<TabBar.Item key="formContext" icon={<FileOutline />} title="申请内容" />
{actionEnabled ? null : <TabBar.Item key="approval" icon={<UserContactOutline />} title="审批处理" />}
</TabBar>
</ContextApprovalExecution.Provider>
) : (
<div>
<Skeleton.Title animated />
<Skeleton.Paragraph lineCount={5} animated />
</div>
)}
</div>
</div>
);
};
const todosComponent = (applyDetail, t, currContext) => {
const formContextSchema = {
type: 'void',
'x-component': 'MPage',
'x-designer': 'MPage.Designer',
'x-component-props': {},
properties: {
Approval: {
type: 'void',
'x-decorator': 'SchemaComponentContextProvider',
'x-decorator-props': { designable: false },
'x-component': 'RemoteSchemaComponent',
'x-component-props': {
uid: applyDetail,
noForm: true,
},
},
},
};
const approvalSchema = {
type: 'void',
'x-component': 'MPage',
'x-designer': 'MPage.Designer',
'x-component-props': {},
properties: {
process: {
type: 'void',
'x-decorator': 'CardItem',
'x-component': 'ApprovalCommon.ViewComponent.MApprovalProcess',
},
},
};
return (
<SchemaComponent
components={{
SchemaComponentProvider,
RemoteSchemaComponent,
SchemaComponentContextProvider,
FormBlockProvider,
ActionBarProvider,
ApprovalActionProvider,
ApprovalFormBlockProvider: ApprovalFormBlockDecorator,
DetailsBlockProvider,
TabBar,
}}
scope={{
useApprovalDetailBlockProps,
useApprovalFormBlockProps,
useDetailsBlockProps: useFormBlockContext,
useSubmit,
}}
schema={currContext === 'formContext' ? formContextSchema : approvalSchema}
/>
);
};

View File

@ -0,0 +1,14 @@
import { useFormBlockContext } from '@tachybase/client';
import { useEffect } from 'react';
import { useContextApprovalExecution } from '../../context/ApprovalExecution';
export function useApprovalDetailBlockProps() {
const { snapshot } = useContextApprovalExecution();
const { form } = useFormBlockContext();
useEffect(() => {
form.setValues(snapshot);
}, [form, snapshot]);
return { form };
}

View File

@ -0,0 +1,6 @@
import { useFormBlockContext } from '@tachybase/client';
export function useApprovalFormBlockProps() {
const { form } = useFormBlockContext();
return { form };
}

View File

@ -0,0 +1,27 @@
import { useAPIClient, useActionContext } from '@tachybase/client';
import { useField } from '@tachybase/schema';
import _ from 'lodash';
import { useContextApprovalExecution } from '../../context/ApprovalExecution';
export function useDestroyAction() {
const field = useField();
const { setSubmitted } = useActionContext() as any;
const { approval } = useContextApprovalExecution();
const apiClient = useAPIClient();
return {
async run() {
try {
_.set(field, ['data', 'loading'], true);
await apiClient.resource('approvals').destroy({
filterByTk: approval.id,
});
setSubmitted(true);
} catch (err) {
_.set(field, ['data', 'loading'], false);
}
},
};
}

View File

@ -0,0 +1,30 @@
import { useCurrentUserContext } from '@tachybase/client';
import { useForm } from '@tachybase/schema';
import { useEffect } from 'react';
import { useContextApprovalExecution } from '../../context/ApprovalExecution';
import { ApprovalStatusEnumDict } from '../../constants';
export function useFormBlockProps() {
const { approval, id, workflow } = useContextApprovalExecution();
const form = useForm();
const { data } = useCurrentUserContext();
const { editable } = ApprovalStatusEnumDict[approval.status];
const needEditable =
editable && approval?.latestExecutionId === id && approval.createdById === data?.data.id && workflow.enabled;
useEffect(() => {
if (!approval) {
return;
}
if (needEditable) {
form.setPattern('editable');
} else {
form.setPattern('readPretty');
}
}, [form, approval, needEditable]);
return { form };
}

View File

@ -0,0 +1,37 @@
import { useAPIClient, useActionContext } from '@tachybase/client';
import { useField, useForm } from '@tachybase/schema';
import { useContextApprovalExecution } from '../../context/ApprovalExecution';
import { useContextApprovalAction } from '../provider/ApprovalAction';
export function useSubmit() {
const field = useField();
const api = useAPIClient();
const form = useForm();
const { id } = useContextApprovalExecution();
const { status } = useContextApprovalAction();
const { setVisible, setSubmitted } = useActionContext() as any;
return {
run: async () => {
try {
if (form.values.status) {
return;
}
await form.submit();
field.data = field.data ?? {};
field.data.loading = true;
setVisible(false);
await api.resource('approvalRecords').submit({
filterByTk: id,
values: { ...form.values, status },
});
field.data.loading = false;
await form.reset();
// refreshAction?.();
setSubmitted?.(true);
} catch (error) {
console.error(error);
field.data && (field.data.loading = false);
}
},
};
}

View File

@ -0,0 +1,146 @@
export interface ApprovalExecution {
id: number;
approval: Approval;
approvalExecution?: approvalExecution;
approvalId: number;
comment?: any;
execution: execution;
executionId: number;
index?: string;
job?: job;
jobId?: number;
node?: node;
nodeId?: number;
snapshot: snapshot;
status: number;
user?: user;
userId?: number;
workflow?: any;
workflowId?: number;
updatedAt: any;
}
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;
}
export interface approvalExecution {
approvalId: number;
executionId: number;
id: number;
snapshot: {};
status: any;
}
export interface execution {
context: {};
id: number;
jobs: any[];
key: string;
status: number;
workflowId: number;
}
export interface job {
executionId: number;
id: number;
nodeId: number;
nodeKey: string;
result: any;
status: number;
upstreamId: any;
}
export interface node {
branchIndex: any;
config: {};
downstreamId: any;
id: number;
key: string;
title: string;
type: string;
upstreamId: any;
workflowId: number;
}
export interface snapshot {
ReasonCollection: any;
account_collection: any;
account_collection_id: any;
account_comment: any;
account_id: any;
account_pay: any;
account_pay_id: any;
amount_pay: any;
approve_status: string;
approver_list: any[];
approver_pre_list: any[];
attachments: any[];
category: string;
cc_list: any[];
comment_collection: any;
comment_pay: any;
company: any;
company_id: any;
company_pay: {};
company_pay_id: number;
company_receive: {};
company_receive_id: number;
createdById: number;
date_pay: any;
date_receive: any;
id: number;
items: any[];
items_amount: any;
items_amount_pay: any;
items_amount_receive: number;
items_amount_show: any;
priority: string;
project: any;
project_collection_id: any;
project_id: any;
project_pay_id: any;
reason: any;
reason_collection: any;
reason_pay: any;
sort: number;
style_temp: any;
}
export interface user {
appLang: string;
email: string;
id: number;
nickname: string;
pdf_top_margin: any;
phone: string;
systemSettings: {};
username: string;
}
export interface workflow {
allExecuted: number;
current: boolean;
description: any;
enabled: boolean;
executed: number;
id: number;
key: string;
nodes: any[];
sync: boolean;
title: string;
triggerTitle: any;
type: string;
}

View File

@ -0,0 +1,29 @@
import React from 'react';
import { ActionBarProvider as ClientActionBarProvider, useCompile } from '@tachybase/client';
import { str2moment } from '@tachybase/utils/client';
import { Space, Tag } from 'antd';
import { useContextApprovalExecution } from '../../context/ApprovalExecution';
import { approvalStatusOptions } from '../../constants';
export function ActionBarProvider(props) {
const { status } = useContextApprovalExecution();
if (status) {
return <ComponentUserInfo />;
} else {
return <ClientActionBarProvider {...props} />;
}
}
const ComponentUserInfo = () => {
const compile = useCompile();
const { status, updatedAt, user } = useContextApprovalExecution();
const configObj = approvalStatusOptions.find((value) => value.value === status);
return (
<Space>
<Tag color={configObj.color}>{compile(configObj.label)}</Tag>
<time>{str2moment(updatedAt).format('YYYY-MM-DD HH:mm:ss')}</time>
<Tag>{user.nickname}</Tag>
</Space>
);
};

View File

@ -0,0 +1,27 @@
import React, { useContext } from 'react';
import { useCurrentUserContext } from '@tachybase/client';
import { createContext } from 'react';
import { APPROVAL_ACTION_STATUS } from '../../constants';
import { useContextApprovalExecution } from '../../context/ApprovalExecution';
const ContextApprovalStatus = createContext(APPROVAL_ACTION_STATUS.SUBMITTED);
export function useContextApprovalStatus() {
return useContext(ContextApprovalStatus);
}
export function ApplyActionStatusProvider(props) {
const { value, children } = props;
const { approval } = useContextApprovalExecution();
const { status, createdById, workflow } = approval;
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);
if (isSameId && isEnbled && isStatusDid) {
return <ContextApprovalStatus.Provider value={value}>{children}</ContextApprovalStatus.Provider>;
}
return null;
}

View File

@ -0,0 +1,23 @@
import React, { useContext } from 'react';
import { createContext } from 'react';
import { useContextApprovalExecution } from '../../context/ApprovalExecution';
export interface ApprovalAction {
status: number | null;
}
const ContextApprovalAction = createContext<Partial<ApprovalAction>>({});
export function useContextApprovalAction() {
return useContext(ContextApprovalAction);
}
export function ApprovalActionProvider({ children, ...props }) {
const { status } = useContextApprovalExecution();
if (!status || status === props.status) {
return <ContextApprovalAction.Provider value={props}>{children}</ContextApprovalAction.Provider>;
}
return null;
}

View File

@ -0,0 +1,85 @@
import {
BlockRequestContext_deprecated,
FormActiveFieldsProvider,
FormBlockContext,
FormV2,
useAPIClient,
useAssociationNames,
useCurrentUserContext,
useDesignable,
} from '@tachybase/client';
import { RecursionField, createForm, useField, useFieldSchema } from '@tachybase/schema';
import _ from 'lodash';
import React, { Fragment, useContext, useMemo, useRef } from 'react';
import { useContextApprovalExecution } from '../../context/ApprovalExecution';
export function ApprovalFormBlockDecorator(props) {
const approvalExecutions = useContextApprovalExecution();
const { job, execution, workflow } = approvalExecutions;
const omitApproval = _.omit(approvalExecutions, ['approval', 'job', 'node', 'snapshot']);
const fieldSchema = useFieldSchema();
const field = useField();
const formBlockRef = useRef(null);
const { getAssociationAppends } = useAssociationNames();
const { appends, updateAssociationValues } = getAssociationAppends();
const { data } = useCurrentUserContext();
const { findComponent } = useDesignable();
const ContainerFormComp = findComponent(field.component?.[0]) || Fragment;
const form = useMemo(() => {
return createForm({
initialValues: omitApproval,
pattern:
!workflow?.enabled || execution?.status || job?.status || omitApproval?.status == null
? 'disabled'
: omitApproval.status || data.data?.id !== omitApproval.userId
? 'readPretty'
: 'editable',
});
}, [data.data?.id, omitApproval?.status, workflow?.enabled]);
const params = useMemo(() => ({ appends: appends, ...props.params }), [appends, props.params]);
const result = useMemo(() => ({ loading: false, data: { data: omitApproval } }), [omitApproval]);
const collectionApi = useAPIClient().resource(props.collection);
const blockRequestContext = useContext(BlockRequestContext_deprecated);
const formValue = useMemo(
() => ({
params: params,
form: form,
field: field,
service: result,
updateAssociationValues: updateAssociationValues,
formBlockRef: formBlockRef,
}),
[field, form, params, result, updateAssociationValues],
);
if (!omitApproval.status && omitApproval.userId !== data.data?.id) {
return null;
}
return (
<FormActiveFieldsProvider name="form">
<BlockRequestContext_deprecated.Provider
value={{
block: 'form',
props: props,
field: field,
service: result,
resource: collectionApi,
__parent: blockRequestContext,
}}
>
<FormBlockContext.Provider value={formValue}>
<ContainerFormComp {...field.componentProps}>
<FormV2.Templates key={1} form={form} />
<div key={2} ref={formBlockRef}>
<RecursionField schema={fieldSchema} onlyRenderProperties={true} />
</div>
</ContainerFormComp>
</FormBlockContext.Provider>
</BlockRequestContext_deprecated.Provider>
</FormActiveFieldsProvider>
);
}

View File

@ -0,0 +1,19 @@
import { useCurrentUserContext } from '@tachybase/client';
import { APPROVAL_ACTION_STATUS } from '../../constants';
import { useContextApprovalExecution } from '../../context/ApprovalExecution';
export function WithdrawActionProvider({ children }) {
const { data } = useCurrentUserContext();
const { approval } = useContextApprovalExecution();
const { workflow, status, createdById } = approval;
const isSameId = data.data.id === createdById;
const isEnabledWithdraw = workflow.enabled && workflow.config.withdrawable;
const isStatusSubmitted = APPROVAL_ACTION_STATUS.SUBMITTED === status;
if (isSameId && isEnabledWithdraw && isStatusSubmitted) {
return children;
}
return null;
}

View File

@ -8,7 +8,16 @@ export class PluginApprovalMobileClient extends Plugin {
async beforeLoad() {} async beforeLoad() {}
async load() {} async load() {
this.app.router.add('mobile.approval.page', {
path: '/mobile/:name/:id/page',
Component: 'ViewActionUserInitiationsContent',
});
this.app.router.add('mobile.approval.detailspage', {
path: '/mobile/:name/:id/:category/detailspage',
Component: 'ViewActionTodosContent',
});
}
} }
export default PluginApprovalMobileClient; export default PluginApprovalMobileClient;

View File

@ -0,0 +1,65 @@
{
"Approval": "Approval",
"Approvals": "Approvals",
"Approval blocks": "Approval blocks",
"Launch": "Launch",
"Todos": "Todos",
"Approval applications": "Approval applications",
"Approval todos": "Approval todos",
"Related approvals": "Related approvals",
"Apply new": "Apply new",
"Apply": "Apply",
"Initiator": "Initiator",
"Application content": "Application content",
"Approval process": "Approval process",
"History": "History",
"Approval ID": "Approval ID",
"Node": "Node",
"Assignee": "Assignees",
"Assignees": "Assignees",
"Add assignee": "Add assignee",
"Negotiation mode": "Negotiation mode",
"Or": "Or",
"And": "And",
"Voting": "Voting",
"Anyone approve or reject as result.": "Anyone approve or reject as result.",
"Everyone approve as approved, or any one reject as rejected.": "Everyone approve as approved, or any one reject as rejected.",
"Approve when approvals rate greater than the set percentage, reject when rejections rate greater than or equal to (1 - percentage).": "Approve when approvals rate greater than the set percentage, reject when rejections rate greater than or equal to (1 - percentage).",
"Order": "Order",
"Sequentially": "Sequentially",
"Parallelly": "Parallelly",
"Draft": "Draft",
"Withdraw": "Withdraw",
"Are you sure you want to withdraw it?": "Are you sure you want to withdraw it?",
"Returned": "Returned",
"Submitted": "Submitted",
"Processing": "Processing",
"Approved": "Approved",
"Rejected": "Rejected",
"Approve": "Approve",
"Reject": "Reject",
"Return": "Return",
"Canceled": "Canceled",
"Assigned": "Assigned",
"Pending": "Pending",
"Withdrawn": "Withdrawn",
"Unprocessed": "Unprocessed",
"User interface": "User interface",
"View user interface": "View user interface",
"Configure user interface": "Configure user interface",
"Apply form": "Apply form",
"Withdrawable": "Withdrawable",
"Returnable": "Returnable",
"Comment": "Comment",
"Event will be triggered when submitted current workflow bound form action, or new application from approval block.": "Event will be triggered when submitted current workflow bound form action, or new application from approval block.",
"Used to perform manual approval operations within approval workflow, approvers can perform approval action through the to-do list in the approval block, such as approval, rejection or return.": "Used to perform manual approval operations within approval workflow, approvers can perform approval action through the to-do list in the approval block, such as approval, rejection or return.",
"Pass mode": "Pass mode",
"Passthrough mode": "Passthrough mode",
"When rejected or returned, the workflow will be terminated immediately.": "When rejected or returned, the workflow will be terminated immediately.",
"Branch mode": "Branch mode",
"Could run different branch based on result.": "Could run different branch based on result.",
"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.",
"Disabled": "Disabled",
"Trigger data": "Trigger data"
}

View File

@ -0,0 +1,80 @@
{
"Approval event": "审批事件",
"Approval": "审批",
"Approvals": "审批",
"Approval blocks": "审批区块",
"Launch": "发起",
"Todos": "待办",
"Approval applications": "审批申请",
"Approval todos": "审批待办",
"Related approvals": "相关审批",
"Apply new": "发起新申请",
"Apply": "发起",
"Initiator": "发起人",
"Application content": "申请内容",
"Approval process": "审批处理",
"No data yet": "暂无数据",
"Approval ID": "单据编号",
"Current status": "当前状态",
"Task node": "任务节点",
"Assignee": "审批人",
"Assignees": "审批人",
"Add assignee": "添加审批人",
"Select assignees": "选择审批人",
"Query assignees": "查询审批人",
"Negotiation mode": "协商模式",
"Or": "或签",
"And": "会签",
"Voting": "投票",
"The approval or rejection by anyone of them is the result.": "任意一人通过或否决即为结果。",
"If it's approved by all, it's approved. If it's rejected by anyone, it's rejected.": "所有人通过才通过,任意一人否决则否决。",
"Approved if the approval rate is greater than the set percentage, otherwise rejected.": "通过率大于设置的百分比时通过,否则否决。",
"Order": "多人处理顺序",
"Parallelly": "并行",
"Multiple approvers can approve in any order.": "多个审批人可以任意顺序审批。",
"Sequentially": "顺序",
"Multiple approvers in sequential order.": "多个审批人按照排序审批。",
"Save draft": "保存草稿",
"Draft": "草稿",
"Withdraw": "撤回",
"Are you sure you want to withdraw it?": "确定要撤回吗?",
"Returned": "退回",
"Submitted": "提交",
"Processing": "处理中",
"Approved": "通过",
"Rejected": "否决",
"Approve": "通过",
"Reject": "否决",
"Return": "退回",
"Canceled": "取消",
"Assigned": "已分配",
"Pending": "待处理",
"Withdrawn": "撤回",
"Unprocessed": "未处理",
"Where to initiate and approve": "发起和审批的位置",
"Initiate and approve in data blocks only": "仅在数据区块中发起和审批",
"Actions from any form block can be bound to this workflow for initiating approvals, and the approval process can be handled and tracked in the approval block of a single record which is typically applicable to business data.": "可以将任意表单区块的操作绑定到该工作流,用于发起审批,并在单条数据的审批区块里处理和跟踪审批过程,通常适用于业务数据。",
"Initiate and approve in both data blocks and global approval blocks": "在数据区块和审批中心都可以发起和审批",
"In addition to data blocks, a global approval block can also be used to initiates and processes approvals, which typically applies to administrative data.": "除了数据区块,还可以在全局的审批中心发起和处理审批,这通常适用于行政数据。",
"Initiator's interface": "发起人的操作界面",
"Approver's interface": "审批人的操作界面",
"Go to configure": "进入配置",
"For initiating approvals, or viewing and manipulating initiated approvals.": "用于发起审批,或者查看和操作已发起的审批。",
"Apply form": "申请表单",
"Allowed to be withdrawn": "允许撤回",
"Allow the initiator to withdraw the approval before the approval starts.": "在审批开始之前,允许发起人撤回审批。",
"Returnable": "可退回",
"Comment": "意见",
"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.": "通过操作按钮或 API 发起审批申请时触发。专用于审批流程,有专属的审批节点和区块用于管理单据和追踪处理过程。",
"Manual approval operations within the approval process, the approver can approve in the global approval block or in the approval block of a single record.": "在审批流程内进行人工审批操作,审批人可以在全局的审批区块里进行审批,也可以在单条数据的审批区块里审批。",
"Pass mode": "通过模式",
"Passthrough mode": "直通模式",
"When rejected or returned, the workflow will be terminated immediately.": "当否决或退回时,工作流将立即终止。",
"Branch mode": "分支模式",
"Could run different branch based on result.": "产生结果后可按结果继续不同的分支。",
"End the workflow after rejection branch": "否决后终止流程",
"When checked, the workflow will terminate when the rejection branch ends.": "勾选后,否决分支结束后工作流将终止。",
"Disabled": "已失效",
"Submission may be withdrawn, please try refresh the list.": "提交可能已被撤回,请尝试刷新列表。",
"Trigger data": "触发器数据"
}

View File

@ -103,7 +103,6 @@ const approvals = {
return actions.destroy(context, next); return actions.destroy(context, next);
}, },
async withdraw(context, next) { async withdraw(context, next) {
let _a;
const { filterByTk } = context.action.params; const { filterByTk } = context.action.params;
const repository = utils.getRepositoryFromParams(context); const repository = utils.getRepositoryFromParams(context);
const approval = await repository.findOne({ const approval = await repository.findOne({
@ -114,7 +113,7 @@ const approvals = {
if (!approval) { if (!approval) {
return context.throw(404); return context.throw(404);
} }
if (approval.createdById !== ((_a = context.state.currentUser) == null ? void 0 : _a.id)) { if (approval.createdById !== context.state.currentUser?.id) {
return context.throw(403); return context.throw(403);
} }
if (approval.status !== APPROVAL_STATUS.SUBMITTED || !approval.workflow.config.withdrawable) { if (approval.status !== APPROVAL_STATUS.SUBMITTED || !approval.workflow.config.withdrawable) {
@ -157,7 +156,7 @@ const approvals = {
map.set(record.job.id, record.job); map.set(record.job.id, record.job);
} }
return map; return map;
}, /* @__PURE__ */ new Map()); }, new Map());
return Array.from(jobsMap.values()); return Array.from(jobsMap.values());
}); });
context.body = approval; context.body = approval;

View File

@ -8,6 +8,7 @@
}, },
"peerDependencies": { "peerDependencies": {
"@tachybase/client": "0.x", "@tachybase/client": "0.x",
"@tachybase/database": "0.x",
"@tachybase/server": "0.x", "@tachybase/server": "0.x",
"@tachybase/test": "0.x" "@tachybase/test": "0.x"
} }

View File

@ -33,7 +33,7 @@ export const HomePage: React.FC<{}> = () => {
</header> </header>
<main> <main>
<Carousel autoplay> <Carousel autoplay>
{data.data.map((item) => ( {data?.data?.map((item) => (
<div key={item.id}> <div key={item.id}>
<Image preview={false} src={item.pictures[0].url} /> <Image preview={false} src={item.pictures[0].url} />
</div> </div>

View File

@ -98,3 +98,5 @@ export const MobileCore: React.FC<PropsWithChildren> = (props) => {
</SchemaComponentOptions> </SchemaComponentOptions>
); );
}; };
export * from './schema';

View File

@ -88,3 +88,5 @@ export class MobileClientPlugin extends Plugin {
} }
export default MobileClientPlugin; export default MobileClientPlugin;
export * from './core';

View File

@ -1418,6 +1418,9 @@ importers:
'@tachybase/client': '@tachybase/client':
specifier: workspace:* specifier: workspace:*
version: link:../../../core/client version: link:../../../core/client
'@tachybase/plugin-workflow':
specifier: workspace:*
version: link:../../@tachybase/plugin-workflow
'@tachybase/server': '@tachybase/server':
specifier: workspace:* specifier: workspace:*
version: link:../../../core/server version: link:../../../core/server
@ -1437,6 +1440,9 @@ importers:
'@types/lodash': '@types/lodash':
specifier: ^4.17.0 specifier: ^4.17.0
version: 4.17.0 version: 4.17.0
ahooks:
specifier: ^3.7.2
version: 3.7.11(react@18.2.0)
antd: antd:
specifier: 5.16.1 specifier: 5.16.1
version: 5.16.1(react-dom@18.2.0)(react@18.2.0) version: 5.16.1(react-dom@18.2.0)(react@18.2.0)
@ -1455,6 +1461,9 @@ importers:
react-i18next: react-i18next:
specifier: ^11.15.1 specifier: ^11.15.1
version: 11.18.6(i18next@22.5.1)(react-dom@18.2.0)(react@18.2.0) version: 11.18.6(i18next@22.5.1)(react-dom@18.2.0)(react@18.2.0)
react-router-dom:
specifier: 6.x
version: 6.22.3(react-dom@18.2.0)(react@18.2.0)
packages/plugins/@hera/plugin-audit-logs: packages/plugins/@hera/plugin-audit-logs:
dependencies: dependencies:
@ -1668,6 +1677,9 @@ importers:
'@tachybase/client': '@tachybase/client':
specifier: 0.x specifier: 0.x
version: link:../../../core/client version: link:../../../core/client
'@tachybase/database':
specifier: 0.x
version: link:../../../core/database
'@tachybase/server': '@tachybase/server':
specifier: 0.x specifier: 0.x
version: link:../../../core/server version: link:../../../core/server
@ -13923,7 +13935,7 @@ packages:
peerDependencies: peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 react: ^16.8.0 || ^17.0.0 || ^18.0.0
dependencies: dependencies:
'@babel/runtime': 7.24.4 '@babel/runtime': 7.24.5
dayjs: 1.11.10 dayjs: 1.11.10
intersection-observer: 0.12.2 intersection-observer: 0.12.2
js-cookie: 2.2.1 js-cookie: 2.2.1
@ -26198,7 +26210,7 @@ packages:
react: '>=16.9.0' react: '>=16.9.0'
react-dom: '>=16.9.0' react-dom: '>=16.9.0'
dependencies: dependencies:
'@babel/runtime': 7.24.0 '@babel/runtime': 7.24.5
react: 18.2.0 react: 18.2.0
react-dom: 18.2.0(react@18.2.0) react-dom: 18.2.0(react@18.2.0)
react-is: 18.3.1 react-is: 18.3.1