feat/workflow-notice (#1037)

![image](/attachments/9c701f22-f743-40a4-895c-d40fb170a3a3)
![image](/attachments/04e3e2c6-0545-4230-b7e5-fd0d00add727)

1. 可能有一些遗留问题, 比如工作流状态同步显示的问题, 以及后续需要配置.
2. 目前没有时间仔细核对, 需要先做其他事情

Reviewed-on: daoyoucloud/tachybase#1037
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-28 09:52:08 +08:00 committed by sealday
parent 502ba8f21c
commit a298f4ddf0
51 changed files with 2308 additions and 81 deletions

View File

@ -172,11 +172,6 @@ export class ApprovalTrigger extends Trigger {
compile,
getCollectionFields,
});
console.log(
'%c Line:176 🍷 collectionFieldOptions',
'font-size:18px;color:#33a5ff;background:#3f7cff',
collectionFieldOptions,
);
return collectionFieldOptions;
}

View File

@ -1,5 +1,6 @@
import { ExtendCollectionsProvider, TableBlockProvider, useRecord } from '@tachybase/client';
import React from 'react';
import { ExtendCollectionsProvider, TableBlockProvider, useRecord } from '@tachybase/client';
import { CollectionApprovalTodos } from '../../common/Cn.ApprovalTodos';
import { CollectionApprovals } from '../approval-common/Approvals.collection';
import { CollectionFlowNodes } from '../approval-common/FlowNodes.collection';

View File

@ -0,0 +1,2 @@
/node_modules
/src

View File

@ -0,0 +1 @@
# @tachybase/plugin-workflow-notice

View File

@ -0,0 +1,2 @@
export * from './dist/client';
export { default } from './dist/client';

View File

@ -0,0 +1 @@
module.exports = require('./dist/client/index.js');

View File

@ -0,0 +1,34 @@
{
"name": "@tachybase/plugin-workflow-notice",
"displayName": "Workflow: Notice",
"version": "0.21.32",
"description": "A powerful BPM tool that provides foundational support for business automation, with the capability to extend unlimited triggers and nodes.",
"keywords": [
"Workflow",
"Notice"
],
"main": "dist/server/index.js",
"devDependencies": {
"@ant-design/icons": "5.x",
"@tachybase/actions": "workspace:*",
"@tachybase/components": "workspace:*",
"@tachybase/data-source-manager": "workspace:*",
"@tachybase/database": "workspace:*",
"@tachybase/plugin-ui-schema-storage": "workspace:*",
"@tachybase/schema": "workspace:*",
"@tachybase/utils": "workspace:*",
"antd": "5.16.1",
"lodash": "^4.17.21",
"react": "18.x",
"react-i18next": "^11.15.1",
"sequelize": "^6.26.0"
},
"peerDependencies": {
"@tachybase/client": "workspace:*",
"@tachybase/plugin-workflow": "workspace:*",
"@tachybase/server": "workspace:*",
"@tachybase/test": "workspace:*"
},
"description.zh-CN": "工作流节点, 通知。",
"displayName.zh-CN": "工作流: 通知"
}

View File

@ -0,0 +1,2 @@
export * from './dist/server';
export { default } from './dist/server';

View File

@ -0,0 +1 @@
module.exports = require('./dist/server/index.js');

View File

@ -0,0 +1,42 @@
import { tval } from '../locale';
export const CollectionFlowNodes = {
title: tval('Node'),
name: 'flow_nodes',
fields: [
{
type: 'bigInt',
name: 'id',
interface: 'm2o',
uiSchema: {
type: 'number',
title: 'ID',
'x-component': 'RemoteSelect',
'x-component-props': {
fieldNames: {
label: 'title',
value: 'id',
},
service: {
resource: 'flow_nodes',
params: {
filter: {
type: 'manual',
},
},
},
},
},
},
{
type: 'string',
name: 'title',
interface: 'input',
uiSchema: {
type: 'string',
title: '{{t("Title")}}',
'x-component': 'Input',
},
},
],
};

View File

@ -0,0 +1,144 @@
import { COLLECTION_NOTICE_NAME } from '../../common/constants';
import { NAMESPACE, tval } from '../locale';
import { approvalStatusOptions } from '../usage/column/column.status';
export const CollectionWorkflowNotice = {
title: tval('Notice Center'),
name: COLLECTION_NOTICE_NAME,
fields: [
{
type: 'bigInt',
name: 'id',
interface: 'number',
uiSchema: {
type: 'number',
title: 'ID',
'x-component': 'InputNumber',
},
},
{
type: 'belongsTo',
name: 'user',
target: 'users',
foreignKey: 'userId',
interface: 'm2o',
uiSchema: {
type: 'number',
title: tval('The Notified Person'),
'x-component': 'RemoteSelect',
'x-component-props': {
fieldNames: {
label: 'nickname',
value: 'id',
},
service: { resource: 'users' },
},
},
},
{
type: 'belongsTo',
name: 'node',
target: 'flow_nodes',
foreignKey: 'nodeId',
interface: 'm2o',
isAssociation: true,
uiSchema: {
type: 'number',
title: `{{t("Task node", { ns: "${NAMESPACE}" })}}`,
'x-component': 'RemoteSelect',
'x-component-props': {
fieldNames: {
label: 'title',
value: 'id',
},
service: {
resource: 'flow_nodes',
},
},
},
},
{
type: 'belongsTo',
name: 'workflow',
target: 'workflows',
foreignKey: 'workflowId',
interface: 'm2o',
uiSchema: {
type: 'number',
title: '{{t("Workflow", { ns: "workflow" })}}',
'x-component': 'RemoteSelect',
'x-component-props': {
fieldNames: {
label: 'title',
value: 'id',
},
service: { resource: 'workflows' },
},
},
},
{
type: 'integer',
name: 'status',
interface: 'select',
uiSchema: {
type: 'number',
title: '{{t("Status", { ns: "workflow" })}}',
'x-component': 'Select',
enum: approvalStatusOptions,
},
},
{
type: 'text',
name: 'comment',
interface: 'markdown',
uiSchema: {
type: 'string',
'x-component': 'Markdown',
title: `{{t("Comment", { ns: "${NAMESPACE}" })}}`,
},
},
{
name: 'createdAt',
type: 'date',
interface: 'createdAt',
uiSchema: {
type: 'datetime',
title: '{{t("Created at")}}',
'x-component': 'DatePicker',
'x-component-props': {
showTime: true,
},
},
},
{
name: 'updatedAt',
type: 'date',
interface: 'updatedAt',
uiSchema: {
type: 'datetime',
title: '{{t("Updated at")}}',
'x-component': 'DatePicker',
'x-component-props': {
showTime: true,
},
},
},
{
type: 'string',
name: 'summary',
interface: 'input',
uiSchema: {
type: 'string',
title: tval('Summary'),
'x-component': 'ApprovalsSummary',
'x-component-props': {
style: {
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
},
},
},
},
],
};

View File

@ -0,0 +1,39 @@
import { tval } from '../locale';
export const CollectionWorkflows = {
title: tval('Workflow'),
name: 'workflows',
fields: [
{
type: 'string',
name: 'title',
interface: 'input',
uiSchema: {
title: '{{t("Name")}}',
type: 'string',
'x-component': 'Input',
required: true,
},
},
{
type: 'boolean',
name: 'enabled',
interface: 'select',
uiSchema: {
type: 'boolean',
title: '{{t("Status", { ns: "workflow" })}}',
'x-component': 'Select',
enum: [
{
label: '{{t("On", { ns: "workflow" })}}',
value: true,
},
{
label: '{{t("Off", { ns: "workflow" })}}',
value: false,
},
],
},
},
],
};

View File

@ -0,0 +1,95 @@
import { ArrayItems } from '@tachybase/components';
import { Instruction } from '@tachybase/plugin-workflow/client';
import { NOTICE_INSTRUCTION_NAMESPACE } from '../../common/constants';
import { tval } from '../locale';
import { AdditionNotifiedPerson } from './config-items/AddNotifiedPerson.view';
import { ConfigButtonNotice } from './config-items/ConfigButtonNotice.view';
import { SelectNotifiedPerson } from './config-items/SelectNotifiedPerson.view';
import { NoticeDetailContainer } from './show-interface/NoticeDetailContainer.schema';
import { configSytles } from './style';
// group 的取值, 经查询猜测,可能为
// 'control' 'collection' 'extended' 'manual' 'advanced'
// TODO: 需要整理梳理 workflow, 将这个类型转移到 workflow 中去
type GroupType = 'control' | 'collection' | 'extended' | 'manual' | 'advanced';
export class NoticeInstruction extends Instruction {
title = tval('Notice');
type = NOTICE_INSTRUCTION_NAMESPACE;
group: GroupType = 'extended';
description = tval(
'In the workflow, notification messages can be viewed by the notified person in the notification center.',
);
components = {
ArrayItems: ArrayItems,
SelectNotifiedPerson: SelectNotifiedPerson,
AdditionNotifiedPerson: AdditionNotifiedPerson,
ConfigButtonNotice: ConfigButtonNotice,
ShowNoticeDetail: NoticeDetailContainer,
};
fieldset = {
notifiedPerson: {
type: 'array',
title: tval('The Notified Person'),
required: true,
'x-decorator': 'FormItem',
'x-component': 'ArrayItems',
'x-component-props': {
className: configSytles.ArrayItemsStyle,
},
items: {
type: 'void',
'x-component': 'Space',
'x-component-props': {
className: configSytles.SpaceStyle,
},
properties: {
sort: {
type: 'void',
'x-decorator': 'FormItem',
'x-component': 'ArrayItems.SortHandle',
},
input: {
type: 'string',
'x-decorator': 'FormItem',
'x-component': 'SelectNotifiedPerson',
},
remove: {
type: 'void',
'x-decorator': 'FormItem',
'x-component': 'ArrayItems.Remove',
},
},
},
properties: {
add: {
type: 'void',
title: tval('Add Notified Person'),
// HACK: 这里原本的框架逻辑, 解析竟然和 Addition 这个单词绑定, 奇怪
// 类似的还有, Index, Remove, MoveUp, MoveDown, Copy
// isOperationComponent() 针对这类操作组件, 做了特殊逻辑
'x-component': 'AdditionNotifiedPerson',
},
},
},
showNoticeDetail: {
type: 'void',
title: tval('Show Notice Detail'),
required: true,
'x-decorator': 'FormItem',
'x-component': 'ConfigButtonNotice',
properties: {
showNoticeDetail: {
type: 'void',
'x-component': 'ShowNoticeDetail',
},
},
},
};
isAvailable(params) {
return true;
}
}

View File

@ -0,0 +1,58 @@
// import { PlusOutlined } from '@ant-design/icons';
import React, { useCallback, useState } from 'react';
import { ArrayItems } from '@tachybase/components';
import { useWorkflowExecuted } from '@tachybase/plugin-workflow/client';
import { Button, Popover, Space } from 'antd';
import { useTranslation } from '../../locale';
export const AdditionNotifiedPerson = () => {
const isWorkflowExecuted = useWorkflowExecuted();
const array = ArrayItems.useArray() || { field: [] };
const { t } = useTranslation();
const [isOpen, setIsOpen] = useState(false);
const onSlecet = useCallback(() => {
array.field.push('');
setIsOpen(false);
}, [array.field]);
const onQuery = useCallback(() => {
array.field.push({ filter: {} });
setIsOpen(false);
}, [array.field]);
return (
<Popover
open={!isWorkflowExecuted && isOpen}
onOpenChange={setIsOpen}
content={<PopoverContent onSlecet={onSlecet} onQuery={onQuery} />}
>
<Button
className="ant-formily-array-base-addition"
// icon={<PlusOutlined />}
type="dashed"
block={true}
disabled={isWorkflowExecuted}
>
{t('Add Notified Person')}
</Button>
</Popover>
);
};
// Child Component
const PopoverContent = ({ onSlecet, onQuery }) => {
const { t } = useTranslation();
return (
<Space direction="vertical" size="small">
<Button type="text" onClick={onSlecet}>
{t('Select Notified Person')}
</Button>
<Button type="text" onClick={onQuery}>
{t('Query Notified Person')}
</Button>
</Space>
);
};

View File

@ -0,0 +1,26 @@
import React, { useCallback, useState } from 'react';
import { ActionContextProvider } from '@tachybase/client';
import { Button } from 'antd';
import { useTranslation } from '../../locale';
export const ConfigButtonNotice = (props) => {
const { t } = useTranslation();
const [visible, setVisible] = useState(false);
const handleButtonClick = useCallback(() => {
setVisible(true);
}, [setVisible]);
return (
<>
<Button type="primary" onClick={handleButtonClick} disabled={false}>
{t('Go to configure')}
</Button>
<ActionContextProvider value={{ visible, setVisible, formValueChanged: false }}>
{props.children}
</ActionContextProvider>
</>
);
};

View File

@ -0,0 +1,62 @@
import { RemoteSelect, SchemaComponent, Variable, useCollectionFilterOptions, useToken } from '@tachybase/client';
import { FilterDynamicComponent, useWorkflowVariableOptions } from '@tachybase/plugin-workflow/client';
import { useField } from '@tachybase/schema';
import React from 'react';
// TODO: 可以将这个选择组件封装为一个通用组件, 放在 workflow 下提供使用
export const SelectNotifiedPerson = (props) => {
if (props.value && typeof props.value === 'object') {
return <SelectNotifiedPersonCustom {...props} />;
} else {
return <SelectNotifiedPersonNormal {...props} />;
}
};
const SelectNotifiedPersonNormal = ({ value, onChange }) => {
const scope = useWorkflowVariableOptions({ types: [isUserKeyField] });
return (
<Variable.Input scope={scope} value={value} onChange={onChange}>
<RemoteSelect
fieldNames={{ label: 'nickname', value: 'id' }}
service={{ resource: 'users' }}
manual={false}
value={value}
onChange={onChange}
/>
</Variable.Input>
);
};
const SelectNotifiedPersonCustom = () => {
const field = useField();
const currentFormFields = useCollectionFilterOptions('users');
const { token } = useToken();
return (
<div style={{ border: `1px dashed ${token.colorBorder}`, padding: token.paddingSM }}>
<SchemaComponent
basePath={field.address}
schema={{
type: 'void',
properties: {
filter: {
type: 'object',
'x-component': 'Filter',
'x-component-props': {
options: currentFormFields,
dynamicComponent: FilterDynamicComponent,
},
},
},
}}
/>
</div>
);
};
function isUserKeyField(field) {
if (field.isForeignKey) {
return field.target === 'users';
} else {
return field.collectionName === 'users' && field.name === 'id';
}
}

View File

@ -0,0 +1,24 @@
import { Plugin } from '@tachybase/client';
import PluginWorkflow from '@tachybase/plugin-workflow/client';
import { NoticeInstruction } from './Notice.instruction';
import { NOTICE_INSTRUCTION_NAMESPACE } from '../../common/constants';
import { SCNoticeDetail } from './show-interface/NoticeDetail.schema';
import { ArrayItems } from '@tachybase/components';
// NOTE: 试着用一种新的约束规则去声明文件属于哪种功能. 比如 kit.ts 导出的是单个 Plugin 下的功能集成
// 注册一个节点
export class KitConfiguration extends Plugin {
async afterAdd() {
await this.app.pm.add(SCNoticeDetail);
}
async load() {
const pluginWorkflow = this.app.pm.get(PluginWorkflow);
pluginWorkflow.registerInstruction(NOTICE_INSTRUCTION_NAMESPACE, NoticeInstruction);
this.app.addComponents({
ArrayItems,
});
// this.app.schemaInitializerManager.add(ApproverActionConfigInitializer);
// this.app.schemaInitializerManager.add(ApproverAddBlockInitializer);
}
}

View File

@ -0,0 +1,45 @@
import React from 'react';
import {
createReadPrettyFormBlockSchema,
parseCollectionName,
SchemaInitializerItem,
useRecordCollectionDataSourceItems,
useSchemaInitializer,
useSchemaInitializerItem,
useSchemaTemplateManager,
} from '@tachybase/client';
import { useFlowContext } from '@tachybase/plugin-workflow/client';
import _ from 'lodash';
export const NoticeDetailAddBlock = () => {
const { workflow } = useFlowContext();
const [dataSourceName, collection] = parseCollectionName(workflow.config?.collection);
const itemConfig = useSchemaInitializerItem();
const { insert } = useSchemaInitializer();
const { getTemplateSchemaByMode } = useSchemaTemplateManager();
const items = useRecordCollectionDataSourceItems('FormItem');
const handleClick = async ({ item }) => {
const template = item.template ? await getTemplateSchemaByMode(item) : null;
const schema = createReadPrettyFormBlockSchema({
actionInitializers: null,
resource: collection,
collection,
dataSource: dataSourceName,
template,
settings: 'blockSettings:details',
});
const shcemaWithPatch = {};
// REVIEW: 删除这个属性的必要性, 需要再思考下
_.unset(schema, ['x-acl-action', 'x-acl-action-props']);
const [childSchemaKey] = Object.keys(schema.properties);
_.set(schema.properties[childSchemaKey], 'x-component-props.useProps', 'usePropsNoticeDetail');
insert(schema);
};
return <SchemaInitializerItem {...itemConfig} onClick={handleClick} items={items} />;
};

View File

@ -0,0 +1,78 @@
import { SchemaInitializer, gridRowColWrap, usePlugin } from '@tachybase/client';
import PluginWorkflow, {
useAvailableUpstreams,
useFlowContext,
useNodeContext,
useTrigger,
} from '@tachybase/plugin-workflow/client';
import { tval } from '../../locale';
import { NoticeDetailAddBlock } from './NoticeDetail.block';
// import { ApproverAddBlockComponent } from './VC.ApproverAddBlock';
// import { ApproverAddBlockKit } from './VC.ApproverAddBlockKit';
export const NoticeDetailInitializer = new SchemaInitializer({
name: 'NoticeDetailInitializer',
wrap: gridRowColWrap,
title: "{{t('Add block')}}",
items: [
{
name: 'notice',
type: 'itemGroup',
title: tval('Notice blocks'),
children: [
{
name: 'detail',
type: 'item',
title: '{{t("Details")}}',
Component: NoticeDetailAddBlock,
},
],
},
{
type: 'itemGroup',
name: 'dataBlocks',
title: '{{t("Data blocks")}}',
checkChildrenLength: true,
useChildren() {
const workflowPlugin = usePlugin(PluginWorkflow);
const { workflow } = useFlowContext();
const trigger = useTrigger();
const currentNodeContext = useNodeContext();
const nodes = useAvailableUpstreams(currentNodeContext);
const triggerInitializers = [trigger.useInitializers?.call(trigger, workflow.config)].filter(Boolean);
const nodeBlockInitializers = nodes
.map((node) => {
const instruction = workflowPlugin.instructions.get(node.type);
return instruction?.useInitializers?.call(instruction, node);
})
.filter(Boolean);
return [
...triggerInitializers,
...(nodeBlockInitializers.length
? [
{
name: 'nodes',
type: 'subMenu',
title: '{{t("Node result", { ns: "workflow" })}}',
children: nodeBlockInitializers,
},
]
: []),
].filter(Boolean);
},
},
{
name: 'others',
type: 'itemGroup',
title: '{{t("Other blocks")}}',
children: [
{
name: 'markdown',
type: 'item',
title: '{{t("Markdown")}}',
component: 'MarkdownBlockInitializer',
},
],
},
],
});

View File

@ -0,0 +1,86 @@
import React from 'react';
import { Plugin, SchemaComponent, useAPIClient, useRequest } from '@tachybase/client';
import { useForm } from '@tachybase/schema';
import { uid } from '@tachybase/utils/client';
import { Spin } from 'antd';
import { NoticeDetailInitializer } from './NoticeDetail.initializer';
export class SCNoticeDetail extends Plugin {
async load() {
this.app.schemaInitializerManager.add(NoticeDetailInitializer);
}
}
function createNewSchema(id) {
return {
type: 'void',
name: id,
'x-uid': id,
'x-component': 'Grid',
'x-initializer': 'NoticeDetailInitializer',
};
}
export const NoticeDetail = (props) => {
const { schemaData, loading, handleChange } = useAction(props);
if (loading) {
return <Spin />;
}
return (
<SchemaComponent
memoized={true}
scope={
{
// useSubmit: () => {
// return { run() {} };
// },
// useApprovalFormBlockProps,
// useDetailsBlockProps: useFormBlockContext,
}
}
components={{}}
schema={schemaData as any}
onChange={handleChange}
/>
);
};
function useAction(props) {
const { value: srcID, onChange } = props;
const apiClient = useAPIClient();
const { values, setValuesIn } = useForm();
const { data: schemaData, loading } = useRequest(async () => {
if (srcID) {
const remoteSchema = await fetchSchema(apiClient, srcID);
if (remoteSchema['x-uid'] === srcID) {
return remoteSchema;
}
}
const id = srcID ?? uid();
const newSchema = createNewSchema(id);
await instertSchema(apiClient, newSchema);
onChange(id);
return newSchema;
});
const handleChange = (data) => {};
return {
schemaData,
loading,
handleChange,
};
}
async function fetchSchema(apiClient, srcID): Promise<object> {
const { data: serviceData } = await apiClient.request({ url: `uiSchemas:getJsonSchema/${srcID}` });
const remoteSchema = serviceData.data ?? {};
return remoteSchema;
}
async function instertSchema(apiClient, schema): Promise<void> {
await apiClient.resource('uiSchemas').insert({ values: schema });
}

View File

@ -0,0 +1,43 @@
import { SchemaComponent, SchemaComponentContext } from '@tachybase/client';
import { useFlowContext } from '@tachybase/plugin-workflow/client';
import { uid } from '@tachybase/utils/client';
import React, { useContext, useMemo, useState } from 'react';
import { tval } from '../../locale';
import { schemaStyle } from './style';
import { NoticeDetail } from './NoticeDetail.schema';
export const NoticeDetailContainer = () => {
const context = useContext(SchemaComponentContext);
const [, setId] = useState(uid());
const { workflow } = useFlowContext();
const contextValue = useMemo(() => {
return {
...context,
refresh: () => setId(uid()),
designable: !workflow.executed,
};
}, [context, workflow.executed]);
return (
<SchemaComponentContext.Provider value={contextValue}>
<SchemaComponent components={{ NoticeDetail: NoticeDetail }} schema={schema} />
</SchemaComponentContext.Provider>
);
};
const schema = {
name: 'drawer',
type: 'void',
title: tval('The interface of show notice detail'),
'x-component': 'Action.Drawer',
'x-component-props': {
className: schemaStyle.ActionStyle,
},
properties: {
showNoticeDetail: {
type: 'string',
'x-component': 'NoticeDetail',
},
},
};

View File

@ -0,0 +1,9 @@
import { css } from '@tachybase/client';
export const schemaStyle = {
ActionStyle: css`
.ant-drawer-body {
background: var(--nb-box-bg);
}
`,
};

View File

@ -0,0 +1,26 @@
import { createStyles, css } from '@tachybase/client';
export const useStyles = createStyles({});
export const configSytles = {
ArrayItemsStyle: css`
&[disabled] {
> .ant-formily-array-base-addition {
display: none;
}
> .ant-formily-array-items-item .ant-space-item:not(:nth-child(2)) {
display: none;
}
}
`,
SpaceStyle: css`
width: 100%;
&.ant-space.ant-space-horizontal {
flex-wrap: nowrap;
}
> .ant-space-item:nth-child(2) {
flex-grow: 1;
}
`,
};

View File

@ -0,0 +1,13 @@
import { Plugin } from '@tachybase/client';
import { KitConfiguration } from './configuration/kit';
import { KitUsage } from './usage/kit';
class PluginWorkflowNoticeClient extends Plugin {
async afterAdd() {
await this.app.pm.add(KitConfiguration);
await this.app.pm.add(KitUsage);
}
}
export default PluginWorkflowNoticeClient;

View File

@ -0,0 +1,21 @@
import { i18n, tval as nTval } from '@tachybase/client';
export const NAMESPACE = '@tachybase/plugin-workflow-notice';
export function usePluginTranslation(): any {
return useTranslation();
}
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) => nTval(key, { ns: NAMESPACE });

View File

@ -0,0 +1,66 @@
import { Plugin, SchemaInitializerItem, useSchemaInitializer, useSchemaInitializerItem } from '@tachybase/client';
import { uid } from '@tachybase/utils/client';
import React, { useCallback } from 'react';
import { COLLECTION_NOTICE_NAME } from '../../common/constants';
import { NoticeBlockProvider } from './NoticeBlock.provider';
import { NoticeCenter } from './NoticeCenter.schema';
export class SCApprovalBlock extends Plugin {
async load() {
this.app.addComponents({
NoticeBlockProvider: NoticeBlockProvider,
NoticeCenter: NoticeCenter,
});
}
}
function createSchema() {
const id = uid();
return {
type: 'void',
name: id,
'x-uid': id,
'x-decorator-props': {
collection: COLLECTION_NOTICE_NAME,
params: {
appends: [
'user.id',
'user.nickname',
'node.id',
'node.title',
'job.id',
'job.status',
'job.result',
'workflow.id',
'workflow.title',
'workflow.enabled',
'execution.id',
'execution.status',
],
},
action: 'listCentralized',
},
'x-decorator': 'NoticeBlockProvider',
'x-component': 'CardItem',
'x-designer': 'TableBlockDesigner',
'x-toolbar': 'BlockSchemaToolbar',
'x-settings': 'blockSettings:table',
properties: {
notice: {
type: 'void',
'x-component': 'NoticeCenter',
},
},
};
}
export const NoticeBlockInitializer = () => {
const { insert } = useSchemaInitializer();
const itemConfig = useSchemaInitializerItem();
const handleClick = useCallback(() => {
const schema = createSchema();
insert(schema);
}, [insert, createSchema]);
return <SchemaInitializerItem {...itemConfig} onClick={handleClick} />;
};

View File

@ -0,0 +1,45 @@
import React from 'react';
import { ExtendCollectionsProvider, TableBlockProvider, useRecord } from '@tachybase/client';
import { CollectionFlowNodes } from '../common/flowNodes.collection';
import { CollectionWorkflowNotice } from '../common/notice.collection';
import { CollectionWorkflows } from '../common/workflow.collection';
export const NoticeBlockProvider = ({ children, ...props }) => {
const {
collection,
params = {
filter: {},
},
action = 'list',
} = props;
const record = useRecord();
const config = {
collection,
resource: collection,
action,
params: {
pageSize: 20,
sort: ['-createdAt'],
...params,
filter: record?.id
? {
dataKey: `${record.id}`,
...params.filter,
}
: { ...params.filter },
},
rowKey: 'id',
showIndex: true,
dragSort: false,
};
return (
<ExtendCollectionsProvider collections={[CollectionWorkflows, CollectionFlowNodes, CollectionWorkflowNotice]}>
<TableBlockProvider name={collection} {...config}>
{children}
</TableBlockProvider>
</ExtendCollectionsProvider>
);
};

View File

@ -0,0 +1,199 @@
import React from 'react';
import { css, SchemaComponent } from '@tachybase/client';
import { NAMESPACE, tval } from '../locale';
import { ColumnAction } from './column/column.action';
import { ColumnNode } from './column/column.node';
import { ColumnStatus } from './column/column.status';
import { ColumnUser } from './column/column.user';
import { ColumnWorkflow } from './column/column.workflow';
const schemaStyles = {
ActionbarStyle: css`
margin-bottom: 16px;
`,
TableV2Style: css`
.ant-table-cell {
text-align: center;
}
`,
};
export const NoticeCenter = () => (
<SchemaComponent
components={{
ColumnNode,
ColumnWorkflow,
ColumnUser,
ColumnStatus,
ColumnAction,
}}
schema={schema}
/>
);
const schema = {
type: 'void',
name: 'notice-center',
properties: {
actions: {
type: 'void',
'x-component': 'ActionBar',
'x-component-props': {
className: schemaStyles.ActionbarStyle,
},
properties: {
filter: {
type: 'void',
title: '{{ t("Filter") }}',
'x-action': 'filter',
'x-designer': 'Filter.Action.Designer',
'x-component': 'Filter.Action',
'x-use-component-props': 'useFilterActionProps',
'x-component-props': {
icon: 'FilterOutlined',
},
'x-align': 'left',
},
refresher: {
type: 'void',
title: '{{ t("Refresh") }}',
'x-action': 'refresh',
'x-component': 'Action',
'x-use-component-props': 'useRefreshActionProps',
'x-designer': 'Action.Designer',
'x-component-props': { icon: 'ReloadOutlined' },
'x-align': 'right',
},
},
},
table: {
type: 'array',
'x-component': 'TableV2',
'x-use-component-props': 'useTableBlockProps',
'x-component-props': {
rowKey: 'id',
tableLayout: 'fixed',
className: css`
.ant-table-cell {
text-align: center;
}
`,
},
properties: {
action: {
type: 'void',
'x-decorator': 'TableV2.Column.Decorator',
'x-component': 'TableV2.Column',
'x-component-props': {
width: 60,
},
title: '{{t("Actions")}}',
properties: {
action: {
'x-component': 'ColumnAction',
},
},
},
id: {
type: 'void',
'x-decorator': 'TableV2.Column.Decorator',
'x-component': 'TableV2.Column',
'x-component-props': {
width: 80,
},
title: tval('ID'),
properties: {
id: {
type: 'number',
'x-component': 'Input',
'x-read-pretty': true,
},
},
},
workflow: {
type: 'void',
'x-decorator': 'TableV2.Column.Decorator',
'x-component': 'TableV2.Column',
'x-component-props': {
width: null,
},
title: '{{t("Workflow", { ns: "workflow" })}}',
properties: {
workflow: {
'x-component': 'ColumnWorkflow',
'x-read-pretty': true,
},
},
},
node: {
type: 'void',
'x-decorator': 'TableV2.Column.Decorator',
'x-component': 'TableV2.Column',
'x-component-props': {
width: null,
},
title: `{{t("Task node", { ns: "${NAMESPACE}" })}}`,
properties: {
node: {
'x-component': 'ColumnNode',
'x-read-pretty': true,
},
},
},
// status: {
// type: 'void',
// 'x-decorator': 'TableV2.Column.Decorator',
// 'x-component': 'TableV2.Column',
// 'x-component-props': { width: 100 },
// title: '{{t("Status", { ns: "workflow" })}}',
// properties: {
// status: {
// 'x-component': 'ColumnStatus',
// 'x-read-pretty': true,
// },
// },
// },
user: {
type: 'void',
'x-decorator': 'TableV2.Column.Decorator',
'x-component': 'TableV2.Column',
'x-component-props': { width: 140 },
title: tval('The Notified Person'),
properties: {
user: {
'x-component': 'ColumnUser',
'x-read-pretty': true,
},
},
},
createdAt: {
type: 'void',
'x-decorator': 'TableV2.Column.Decorator',
'x-component': 'TableV2.Column',
'x-component-props': { width: 160 },
properties: {
createdAt: {
type: 'string',
'x-component': 'CollectionField',
'x-read-pretty': true,
},
},
},
summary: {
type: 'void',
'x-decorator': 'TableV2.Column.Decorator',
'x-component': 'TableV2.Column',
title: tval('Summary'),
properties: {
summary: {
type: 'string',
'x-component': 'CollectionField',
'x-read-pretty': true,
},
},
},
},
},
},
};

View File

@ -0,0 +1,42 @@
import React from 'react';
import { SchemaComponent, useCollectionRecordData } from '@tachybase/client';
import { NoticeDetailContent } from '../show-detail/NoticeDetail.schema';
const createSchema = ({ record }) => {
const { id } = record;
return {
name: `notice-view-${id}`,
type: 'void',
'x-component': 'Action.Link',
title: '{{t("View")}}',
properties: {
drawer: {
type: 'void',
'x-component': 'Action.Drawer',
'x-component-props': {
className: 'nb-action-popup',
},
properties: {
content: {
type: 'void',
'x-component': 'NoticeDetailContent',
},
},
},
},
};
};
export const ColumnAction = () => {
const record = useCollectionRecordData();
return (
<SchemaComponent
components={{
NoticeDetailContent: NoticeDetailContent,
}}
schema={createSchema({ record })}
/>
);
};

View File

@ -0,0 +1,17 @@
import { observer, useField, Field } from '@tachybase/schema';
export const ColumnNode = observer(
() => {
const field = useField<Field>();
const fieldValue = field?.value;
if (fieldValue?.title) {
return fieldValue.title;
} else if (fieldValue?.id) {
return `#${fieldValue.id}`;
}
return '';
},
{ displayName: 'ColumnNode' },
);

View File

@ -0,0 +1,49 @@
import React from 'react';
import { useCompile, useRecord } from '@tachybase/client';
import { EXECUTION_STATUS } from '@tachybase/plugin-workflow/client';
import { Field, observer, useField } from '@tachybase/schema';
import { Tag } from 'antd';
import _ from 'lodash';
import { NAMESPACE, useTranslation } from '../../locale';
export const ColumnStatus = observer(
() => {
const { value } = useField<Field>();
const record = useRecord();
return <ColumnStatusComponent value={value} record={record} />;
},
{ displayName: 'ColumnStatus' },
);
export const ColumnStatusComponent = (props) => {
const { value, record } = props;
const omitProps = _.omit(props, ['value', 'record']);
const compile = useCompile();
const { t } = useTranslation();
const { option = approvalStatusConfigObj[value] } = omitProps;
const { workflow, execution, job } = record ?? {};
return (!workflow?.enabled || execution?.stauts || job?.status) && [].includes(value) ? (
<Tag>{t('Unprocessed')}</Tag>
) : (
<Tag color={option?.color}>{compile(option?.label)}</Tag>
);
};
export const approvalStatusOptions = [
{ value: EXECUTION_STATUS.QUEUEING, label: `{{t("Assigned", { ns: "${NAMESPACE}" })}}`, color: 'blue' },
{ value: EXECUTION_STATUS.STARTED, label: `{{t("Pending", { ns: "${NAMESPACE}" })}}`, color: 'gold' },
// { value: EXECUTION_STATUS.RETURNED, label: `{{t("Returned", { ns: "${NAMESPACE}" })}}`, color: 'purple' },
{ value: 2, label: `{{t("Approved", { ns: "${NAMESPACE}" })}}`, color: 'green' },
{ value: EXECUTION_STATUS.REJECTED, label: `{{t("Rejected", { ns: "${NAMESPACE}" })}}`, color: 'red' },
// { value: EXECUTION_STATUS.WITHDRAWN, label: `{{t("Withdrawn", { ns: "${NAMESPACE}" })}}` },
];
const approvalStatusConfigObj = approvalStatusOptions.reduce(
(e, t) =>
Object.assign(e, {
[t.value]: t,
}),
{},
);

View File

@ -0,0 +1,16 @@
import { observer, useField, Field } from '@tachybase/schema';
export const ColumnUser = observer(
() => {
const field = useField<Field>();
const fieldValue = field?.value;
if (fieldValue?.nickname) {
return fieldValue.nickname;
} else if (fieldValue?.id !== undefined) {
return fieldValue.id;
}
return '';
},
{ displayName: 'ColumnUser' },
);

View File

@ -0,0 +1,28 @@
import { css } from '@tachybase/client';
import { Field, observer, useField } from '@tachybase/schema';
import React from 'react';
import { useTranslation } from '../../locale';
export const ColumnWorkflow = observer(
() => {
const { t } = useTranslation();
const { value } = useField<Field>();
const title = value?.title || `#${value?.id}`;
if (value?.enabled) {
return title;
} else {
return (
<span
className={css`
text-decoration: line-through;
`}
title={t('Disabled')}
>
{title}
</span>
);
}
},
{ displayName: 'ColumnWorkflow' },
);

View File

@ -0,0 +1,25 @@
import { Plugin } from '@tachybase/client';
import { tval } from '../locale';
import { NoticeBlockInitializer, SCApprovalBlock } from './NoticeBlock.initializer';
// NOTE: 试着用一种新的约束规则去声明文件属于哪种功能. 比如 kit.ts 导出的是单个 Plugin 下的功能集成
export class KitUsage extends Plugin {
async afterAdd() {
await this.app.pm.add(SCApprovalBlock);
}
async load() {
this.app.addComponents({
NoticeBlockInitializer: NoticeBlockInitializer,
});
this.app.schemaInitializerManager.get('page:addBlock').add('otherBlocks.notice', {
key: 'noticeBlock',
name: 'noticeBlock',
type: 'item',
title: tval('Notice Center'),
Component: 'NoticeBlockInitializer',
icon: 'NotificationOutlined',
});
}
}

View File

@ -0,0 +1,78 @@
import React from 'react';
import {
SchemaComponentContext,
useCollectionRecordData,
useRequest,
useSchemaComponentContext,
} from '@tachybase/client';
import { ExecutionContextProvider } from '@tachybase/plugin-workflow/client';
import { Result, Spin } from 'antd';
import { COLLECTION_NOTICE_NAME } from '../../../common/constants';
import { useTranslation } from '../../locale';
import { ProviderContextMyComponent } from './contexts/MyComponent.context';
import { ProviderContextWorkflowNotice } from './contexts/WorkflowNotice.context';
export const NoticeDetailProvider = ({ children, ...props }) => {
// THINK: 顺序问题, 工具优先, 数据之后, 外部优先, 本地滞后
const schemaComponentContext = useSchemaComponentContext();
const { t } = useTranslation();
const { id } = useCollectionRecordData();
// const { actionEnabled } = useContext(ContextWithActionEnabled);
const { loading, data } = useRequestData(id);
if (loading) {
return <Spin />;
}
if (!data?.data) {
return <Result status="error" title={t('Submission may be withdrawn, please try refresh the list.')} />;
}
const itemsData = data.data;
const { node, workflow, execution } = itemsData;
const schemaId = node?.config.showNoticeDetail;
const { designable } = props;
// THINK: Provider 的顺序, 数据放在外面, 配置放在内层
return (
<ExecutionContextProvider workflow={workflow} nodes={workflow.nodes} execution={execution}>
<ProviderContextWorkflowNotice value={itemsData}>
<ProviderContextMyComponent
value={{
id,
schemaId,
}}
>
<SchemaComponentContext.Provider value={{ ...schemaComponentContext, designable }}>
{children}
</SchemaComponentContext.Provider>
</ProviderContextMyComponent>
</ProviderContextWorkflowNotice>
</ExecutionContextProvider>
);
};
function useRequestData(id) {
const { loading, data }: any = useRequest(
{
resource: COLLECTION_NOTICE_NAME,
action: 'get',
params: {
filterByTk: id,
appends: ['node', 'job', 'workflow', 'workflow.nodes', 'execution', 'execution.jobs', 'user'],
except: ['workflow.config', 'workflow.options', 'nodes.config'],
sort: ['-createdAt'],
},
},
{
refreshDeps: [id],
},
);
return {
loading,
data,
};
}

View File

@ -0,0 +1,77 @@
import React from 'react';
import {
RemoteSchemaComponent,
SchemaComponent,
SchemaComponentProvider,
useFormBlockContext,
} from '@tachybase/client';
import { DetailsBlockProvider } from '@tachybase/plugin-workflow/client';
import { tval } from '../../locale';
import { useContextMyComponent } from './contexts/MyComponent.context';
import { usePropsNoticeDetail } from './hooks/usePropsNoticeDetail';
import { NoticeDetailProvider } from './NoticeDetail.provider';
// 审批-待办-查看: 内容
export const NoticeDetailContent = () => {
return (
<NoticeDetailProvider>
<NoticeDetail />
</NoticeDetailProvider>
);
};
const NoticeDetail = (props) => {
const { id, schemaId } = useContextMyComponent();
return (
<SchemaComponent
components={{
NoticeDetailProvider,
RemoteSchemaComponent,
// SchemaComponentContextProvider,
// FormBlockProvider,
// ActionBarProvider,
// ApprovalActionProvider,
// ApprovalFormBlockProvider: ApprovalFormBlockDecorator,
SchemaComponentProvider,
DetailsBlockProvider,
}}
scope={{
// XXX: 这里的这个scope 需要想一个更有利于可读性的注册地方
usePropsNoticeDetail,
// useApprovalDetailBlockProps,
// useApprovalFormBlockProps,
useDetailsBlockProps: useFormBlockContext,
// useSubmit,
}}
schema={{
name: `content-${id}`,
type: 'void',
'x-component': 'Tabs',
properties: {
detail: {
type: 'void',
title: tval('Content Detail'),
'x-component': 'Tabs.TabPane',
properties: {
detail: {
type: 'void',
'x-decorator': 'NoticeDetailProvider',
'x-decorator-props': {
designable: false,
},
'x-component': 'RemoteSchemaComponent',
'x-component-props': {
uid: schemaId,
noForm: true,
},
},
},
},
},
}}
/>
);
};

View File

@ -0,0 +1,13 @@
import React from 'react';
interface ContextMyComponentProps {
id?: string;
schemaId?: string;
}
export const ContextMyComponent = React.createContext<ContextMyComponentProps>({});
export const ProviderContextMyComponent = ContextMyComponent.Provider;
export function useContextMyComponent() {
return React.useContext(ContextMyComponent);
}

View File

@ -0,0 +1,10 @@
import React from 'react';
// NOTE: 传递的是内置表 workflowNotice 的数据
export const ContextWorkflowNotice = React.createContext<any>({});
export const ProviderContextWorkflowNotice = ContextWorkflowNotice.Provider;
export function useContexWorkflowNotice() {
return React.useContext(ContextWorkflowNotice);
}

View File

@ -0,0 +1,15 @@
import { useEffect } from 'react';
import { useFormBlockContext } from '@tachybase/client';
import { useContexWorkflowNotice } from '../contexts/WorkflowNotice.context';
export function usePropsNoticeDetail() {
const { snapshot } = useContexWorkflowNotice();
const { form } = useFormBlockContext();
useEffect(() => {
form.setValues(snapshot);
}, [form, snapshot]);
return { form };
}

View File

@ -0,0 +1,2 @@
export const NOTICE_INSTRUCTION_NAMESPACE = 'notice';
export const COLLECTION_NOTICE_NAME = 'workflowNotice';

View File

@ -0,0 +1,2 @@
export * from './server';
export { default } from './server';

View File

@ -0,0 +1,17 @@
{
"Notice": "Notice",
"In the workflow, notification messages can be viewed by the notified person in the notification center.":"In the workflow, notification messages can be viewed by the notified person in the notification center.",
"The Notified Person": "The Notified Person",
"Add Notified Person": "Add Notified Person",
"Select Notified Person": "Select Notified Person",
"Query Notified Person": "Query Notified Person",
"Show Notice Detail": "Show Notice Detail",
"Go to configure": "Go to configure",
"The interface of show notice detail": "The interface of show notice detail",
"Notice blocks": "Notice blocks",
"Notice Center": "Notice Center",
"Summary": "Summary",
"Task node": "Task node",
"ID": "ID",
"Content Detail": "Content Detail"
}

View File

@ -0,0 +1,17 @@
{
"Notice": "通知",
"In the workflow, notification messages can be viewed by the notified person in the notification center.":"在工作流程中通知消息, 被通知人可以在通知中心查看到消息.",
"The Notified Person": "被通知人",
"Add Notified Person": "添加被通知人",
"Select Notified Person": "选择被通知人",
"Query Notified Person": "查询被通知人",
"Show Notice Detail":"显示通知详情",
"Go to configure": "进行配置",
"The interface of show notice detail": "显示通知详情界面",
"Notice blocks": "通知区块",
"Notice Center": "工作流:通知中心",
"Summary": "摘要",
"Task node": "任务节点",
"ID": "编号",
"Content Detail": "内容详情"
}

View File

@ -0,0 +1,157 @@
import { UiSchemaRepository } from '@tachybase/plugin-ui-schema-storage';
import { Instruction, JOB_STATUS } from '@tachybase/plugin-workflow';
import { uid } from '@tachybase/utils';
import { COLLECTION_NOTICE_NAME } from '../common/constants';
import { NOTICE_ACTION_STATUS, NOTICE_STATUS } from './constants';
import { getNegotiationMode, NoticeJobStatusMap, parsePerson } from './tools';
class NoticeInstruction extends Instruction {
async run(node, prevJob, processor) {
const job = await processor.saveJob({
status: JOB_STATUS.RESOLVED,
nodeId: node.id,
nodeKey: node.key,
upstreamId: prevJob?.id ?? null,
});
const notifiedPerson = await parsePerson(node, processor);
const { db } = processor.options.plugin;
// TODO-A: 改成取上一个节点的数据集, 或者当前执行流数据源的数据集, 目前是对审批事件做了特化处理; 需要理解下其他类型的触发事件, 怎么拿数据
const ApprovalRepo = db.getRepository('approvals');
const approval = await ApprovalRepo.findOne({
filter: {
'executions.id': processor.execution.id,
},
fields: ['id', 'status', 'data', 'summary', 'collectionName'],
appends: ['approvalExecutions'],
except: ['data'],
});
const NoticeModel = db.getModel(COLLECTION_NOTICE_NAME);
const notifiedPersonMapData = notifiedPerson.map((userId, index) => ({
userId,
jobId: job.id,
nodeId: node.id,
executionId: job.executionId,
workflowId: node.workflowId,
index,
status: node.config.order && index ? NOTICE_ACTION_STATUS.ASSIGNED : NOTICE_ACTION_STATUS.APPROVED,
snapshot: approval.data,
summary: approval.summary,
collectionName: approval.collectionName,
dataKey: approval.dataKey,
}));
await NoticeModel.bulkCreate(notifiedPersonMapData, {
transaction: processor.transaction,
});
return job;
}
// NOTE: 目前没有用处, 不过留着作为未来如果有动作的实现的参考
async resume(node, job, processor) {
if (job.nodeId !== node.id) {
const nodeJob = processor.findBranchParentJob(job, node);
if (job.status === JOB_STATUS.RESOLVED) {
const jobNode = processor.nodesMap.get(job.nodeId);
const branchStart = processor.findBranchStartNode(jobNode);
if (branchStart.branchIndex === NOTICE_ACTION_STATUS.RETURNED) {
nodeJob.set('status', JOB_STATUS.RETRY_NEEDED);
} else if (branchStart.branchIndex === NOTICE_ACTION_STATUS.REJECTED && node.config.endOnReject) {
nodeJob.set('status', JOB_STATUS.REJECTED);
}
return nodeJob;
}
return processor.exit(job.status);
}
const { branchMode, negotiation, order } = node.config;
const notifiedPerson = await parsePerson(node, processor);
const NoticeRepo = this.workflow.app.db.getRepository(COLLECTION_NOTICE_NAME);
const records = await NoticeRepo.find({
filter: {
jobId: job.id,
},
except: ['snapshot'],
sort: ['index'],
transaction: processor.transaction,
});
const distribution = records.reduce((prev, record) => {
const item = prev.find((item2) => item2.status === record.status);
if (item) {
item.count += 1;
} else {
prev.push({
status: record.status,
count: 1,
});
}
return prev;
}, []);
const processing = Boolean(distribution.find((item) => item.status !== NOTICE_ACTION_STATUS.PENDING));
const status =
job.status ||
(getNegotiationMode(negotiation).getStatus(distribution, notifiedPerson, negotiation) ?? JOB_STATUS.PENDING);
const result = NoticeJobStatusMap[status];
processor.logger.debug(`notice resume job and next status: ${status}`);
job.set({
status: status && status !== JOB_STATUS.CANCELED ? (branchMode ? JOB_STATUS.RESOLVED : status) : status,
result,
});
if ((status && status !== JOB_STATUS.CANCELED) || (negotiation && processing)) {
await job.latestUserJob.approval.update(
{
status: NOTICE_STATUS.PROCESSING,
},
{ transaction: processor.transaction },
);
}
const nextPerson = notifiedPerson[notifiedPerson.indexOf(job.latestUserJob.userId) + 1];
if (!status && negotiation && order && nextPerson) {
await NoticeRepo.update({
values: {
status: NOTICE_ACTION_STATUS.PENDING,
},
filter: {
jobId: job.id,
userId: nextPerson,
},
transaction: processor.transaction,
});
}
if (branchMode) {
const branchNode = processor.nodes.find((item) => item.upstream === node && item.branchIndex === result);
if (branchNode) {
await processor.saveJob(job);
await processor.run(branchNode, job);
return null;
}
}
return job;
}
async duplicateConfig(node, { transaction }) {
const uiSchemaRepo = this.workflow.app.db.getRepository<UiSchemaRepository>('uiSchemas');
if (!node.config.showNoticeDetail) {
return node.config;
}
const result = await uiSchemaRepo.duplicate(node.config.showNoticeDetail, {
transaction,
});
return {
...node.config,
showNoticeDetail: result?.['x-uid'] ?? uid(),
};
}
}
export default NoticeInstruction;

View File

@ -0,0 +1,107 @@
import actions, { utils } from '@tachybase/actions';
import WorkflowPlugin from '@tachybase/plugin-workflow';
import { COLLECTION_NOTICE_NAME, NOTICE_INSTRUCTION_NAMESPACE } from '../common/constants';
import { COLLECTION_WORKFLOWS_NAME } from './collections/workflowNotice';
import { NOTICE_ACTION_STATUS } from './constants';
const workflows = {
async listWorkflowNoticeFlows(context, next) {
context.action.mergeParams({
filter: {
type: NOTICE_INSTRUCTION_NAMESPACE,
enabled: true,
},
});
return actions.list(context, next);
},
};
const workflowNotice = {
async listCentralized(context, next) {
const centralizedNoticeFlow = await context.db.getRepository(COLLECTION_WORKFLOWS_NAME).find({
filter: {
'config.centralized': true,
},
fields: ['id'],
});
context.action.mergeParams({
filter: {
workflowId: centralizedNoticeFlow.map((item) => item.id),
},
});
return actions.list(context, next);
},
async submit(context, next) {
const repository = utils.getRepositoryFromParams(context);
const { filterByTk, values } = context.action.params;
const { currentUser } = context.state;
if (!currentUser) {
return context.throw(401);
}
const workflowNotice = await repository.findOne({
filterByTk,
filter: {
userId: currentUser == null ? void 0 : currentUser.id,
},
appends: ['job', 'node', 'execution', 'workflow'],
context,
});
if (!workflowNotice) {
return context.throw(404);
}
if (
!workflowNotice.workflow.enabled ||
workflowNotice.execution.status ||
workflowNotice.job.status ||
workflowNotice.status !== NOTICE_ACTION_STATUS.PENDING ||
!(workflowNotice.node.config.actions ?? []).includes(values.status)
) {
return context.throw(400);
}
// TODO: 完善这里的取值逻辑
await workflowNotice.update({
status: values.status,
// comment: values.comment,
// snapshot: workflowNotice.approval.data,
// summary: workflowNotice.approval.summary,
// collectionName: workflowNotice.approval.collectionName,
});
context.body = workflowNotice.get();
context.status = 202;
await next();
workflowNotice.execution.workflow = workflowNotice.workflow;
workflowNotice.job.execution = workflowNotice.execution;
workflowNotice.job.latestUserJob = workflowNotice.get();
const workflow = context.app.getPlugin(WorkflowPlugin);
const processor = workflow.createProcessor(workflowNotice.execution);
processor.logger.info(
`notice node (${workflowNotice.nodeId}) action trigger execution (${workflowNotice.execution.id}) to resume`,
);
workflow.resume(workflowNotice.job);
},
};
export function init({ app }) {
app.actions({
...make(COLLECTION_WORKFLOWS_NAME, workflows),
...make(COLLECTION_NOTICE_NAME, workflowNotice),
});
}
function make(name, mod) {
return Object.keys(mod).reduce(
(result, key) => ({
...result,
[`${name}:${key}`]: mod[key],
}),
{},
);
}

View File

@ -0,0 +1,67 @@
import { defineCollection } from '@tachybase/database';
import { COLLECTION_NOTICE_NAME } from '../../common/constants';
export const COLLECTION_WORKFLOWS_NAME = 'workflows';
const WorkflowNotice = defineCollection({
namespace: 'workflow.notice',
dumpRules: 'required',
name: COLLECTION_NOTICE_NAME,
createdBy: true,
updatedBy: true,
fields: [
{
type: 'string',
name: 'collectionName',
},
{
type: 'string',
name: 'dataKey',
},
{
type: 'string',
name: 'index',
},
{
type: 'integer',
name: 'status',
},
{
type: 'jsonb',
name: 'snapshot',
defaultValue: {},
},
{
type: 'jsonb',
name: 'summary',
defaultValue: {},
},
{
type: 'belongsTo',
name: 'user',
target: 'users',
},
{
type: 'belongsTo',
name: 'workflow',
onDelete: 'CASCADE',
},
{
type: 'belongsTo',
name: 'execution',
},
{
type: 'belongsTo',
name: 'node',
target: 'flow_nodes',
},
{
type: 'belongsTo',
name: 'job',
target: 'jobs',
},
],
});
export default WorkflowNotice;

View File

@ -0,0 +1,16 @@
export const NOTICE_STATUS = {
DRAFT: 0,
RETURNED: 1,
SUBMITTED: 2,
PROCESSING: 3,
APPROVED: 4,
REJECTED: -1,
};
export const NOTICE_ACTION_STATUS = {
ASSIGNED: null,
PENDING: 0,
RETURNED: 1,
APPROVED: 2,
REJECTED: -1,
CANCELED: -2,
};

View File

@ -0,0 +1 @@
export { default } from './plugin';

View File

@ -0,0 +1,49 @@
import WorkflowPlugin from '@tachybase/plugin-workflow';
import { Plugin } from '@tachybase/server';
import path from 'path';
import { NOTICE_INSTRUCTION_NAMESPACE } from '../common/constants';
import NoticeInstruction from './NoticeInstruction';
import { init } from './actions';
import { COLLECTION_WORKFLOWS_NAME } from './collections/workflowNotice';
import { COLLECTION_NOTICE_NAME } from '../common/constants';
export class PluginWorkflowNoticeServer extends Plugin {
workflow;
async afterAdd() {}
async beforeLoad() {
this.app.on('afterLoadPlugin', (plugin) => {
if (!(plugin instanceof WorkflowPlugin)) {
return;
}
this.workflow = plugin;
plugin.instructions.register(NOTICE_INSTRUCTION_NAMESPACE, new NoticeInstruction(plugin));
});
}
async load() {
const { db } = this;
db.addMigrations({
namespace: NOTICE_INSTRUCTION_NAMESPACE,
directory: path.resolve(__dirname, 'migrations'),
context: {
plugin: this,
},
});
await db.import({
directory: path.resolve(__dirname, 'collections'),
});
init(this);
this.app.acl.allow(COLLECTION_WORKFLOWS_NAME, ['listWorkflowNoticeFlows'], 'loggedIn');
this.app.acl.allow(COLLECTION_NOTICE_NAME, ['get', 'list', 'listCentralized', 'submit'], 'loggedIn');
}
async install() {}
async afterEnable() {}
async afterDisable() {}
async remove() {}
}
export default PluginWorkflowNoticeServer;

View File

@ -0,0 +1,138 @@
import { JOB_STATUS } from '@tachybase/plugin-workflow';
import _ from 'lodash';
import { NOTICE_ACTION_STATUS } from './constants';
interface ParamsType {
summaryConfig: Array<string>;
data: object;
}
export function getSummary(params: ParamsType): object {
const { summaryConfig = [], data } = params;
const result = summaryConfig.reduce((summary, key) => {
const value = _.get(data, key);
return {
...summary,
[key]: value,
};
}, {});
return result;
}
export async function parsePerson(node, processor) {
const configPerson = processor
.getParsedValue(node.config.notifiedPerson ?? [], node.id)
.flat()
.filter(Boolean);
const notifiedPerson = new Set();
const UserRepo = processor.options.plugin.app.db.getRepository('users');
for (const item of configPerson) {
if (typeof item === 'object') {
const result = await UserRepo.find({
...item,
fields: ['id'],
transaction: processor.transaction,
});
result.forEach((item2) => notifiedPerson.add(item2.id));
} else {
notifiedPerson.add(item);
}
}
return [...notifiedPerson];
}
const NEGOTIATION_MODE = {
SINGLE: Symbol('single'),
ALL: Symbol('all'),
ANY: Symbol('any'),
PERCENTAGE: Symbol('percentage'),
};
const JobStatusMap = {
[NOTICE_ACTION_STATUS.PENDING]: JOB_STATUS.PENDING,
[NOTICE_ACTION_STATUS.APPROVED]: JOB_STATUS.RESOLVED,
[NOTICE_ACTION_STATUS.REJECTED]: JOB_STATUS.REJECTED,
[NOTICE_ACTION_STATUS.RETURNED]: JOB_STATUS.RETRY_NEEDED,
};
const Modes = {
[NEGOTIATION_MODE.SINGLE]: {
getStatus(distribution, assignees, mode) {
const done = distribution.find((item) => item.status !== NOTICE_ACTION_STATUS.PENDING && item.count > 0);
return done ? JobStatusMap[done.status] : null;
},
},
[NEGOTIATION_MODE.ALL]: {
getStatus(distribution, assignees, mode) {
const rejected = distribution.find((item) =>
[NOTICE_ACTION_STATUS.REJECTED, NOTICE_ACTION_STATUS.RETURNED].includes(item.status),
);
if (rejected && rejected.count) {
return JobStatusMap[rejected.status];
}
const approved = distribution.find((item) => item.status === NOTICE_ACTION_STATUS.APPROVED);
if (approved && approved.count === assignees.length) {
return JOB_STATUS.RESOLVED;
}
return null;
},
},
[NEGOTIATION_MODE.ANY]: {
getStatus(distribution, assignees, mode) {
const returned = distribution.find((item) => item.status === NOTICE_ACTION_STATUS.RETURNED);
if (returned && returned.count) {
return JOB_STATUS.RETRY_NEEDED;
}
const approved = distribution.find((item) => item.status === NOTICE_ACTION_STATUS.APPROVED);
if (approved && approved.count) {
return NOTICE_ACTION_STATUS.APPROVED;
}
const rejectedCount = distribution.reduce(
(count, item) => (item.status === NOTICE_ACTION_STATUS.REJECTED ? count + item.count : count),
0,
);
if (rejectedCount === assignees.length) {
return JOB_STATUS.REJECTED;
}
return null;
},
},
[NEGOTIATION_MODE.PERCENTAGE]: {
getStatus(distribution, assignees, mode) {
const returned = distribution.find((item) => item.status === NOTICE_ACTION_STATUS.RETURNED);
if (returned && returned.count) {
return JOB_STATUS.RETRY_NEEDED;
}
const approved = distribution.find((item) => item.status === NOTICE_ACTION_STATUS.APPROVED);
if (approved && approved.count / assignees.length > mode) {
return JOB_STATUS.RESOLVED;
}
const rejected = distribution.find((item) => item.status === NOTICE_ACTION_STATUS.REJECTED);
if (rejected && rejected.count / assignees.length >= 1 - mode) {
return JOB_STATUS.REJECTED;
}
return null;
},
},
};
export const NoticeJobStatusMap = {
[JOB_STATUS.PENDING]: NOTICE_ACTION_STATUS.PENDING,
[JOB_STATUS.RESOLVED]: NOTICE_ACTION_STATUS.APPROVED,
[JOB_STATUS.REJECTED]: NOTICE_ACTION_STATUS.REJECTED,
[JOB_STATUS.RETRY_NEEDED]: NOTICE_ACTION_STATUS.RETURNED,
[JOB_STATUS.CANCELED]: NOTICE_ACTION_STATUS.CANCELED,
};
export function getNegotiationMode(mode) {
switch (true) {
case mode === 1:
return Modes[NEGOTIATION_MODE.ALL];
case 0 < mode && mode < 1:
return Modes[NEGOTIATION_MODE.PERCENTAGE];
default:
return Modes[NEGOTIATION_MODE.SINGLE];
}
}

View File

@ -3969,6 +3969,61 @@ importers:
specifier: ^6.26.0
version: 6.35.2
packages/plugins/@tachybase/plugin-workflow-notice:
dependencies:
'@tachybase/client':
specifier: workspace:*
version: link:../../../core/client
'@tachybase/plugin-workflow':
specifier: workspace:*
version: link:../plugin-workflow
'@tachybase/server':
specifier: workspace:*
version: link:../../../core/server
'@tachybase/test':
specifier: workspace:*
version: link:../../../core/test
devDependencies:
'@ant-design/icons':
specifier: 5.x
version: 5.3.7(react-dom@18.2.0)(react@18.2.0)
'@tachybase/actions':
specifier: workspace:*
version: link:../../../core/actions
'@tachybase/components':
specifier: workspace:*
version: link:../../../core/components
'@tachybase/data-source-manager':
specifier: workspace:*
version: link:../../../core/data-source-manager
'@tachybase/database':
specifier: workspace:*
version: link:../../../core/database
'@tachybase/plugin-ui-schema-storage':
specifier: workspace:*
version: link:../plugin-ui-schema-storage
'@tachybase/schema':
specifier: workspace:*
version: link:../../../core/schema
'@tachybase/utils':
specifier: workspace:*
version: link:../../../core/utils
antd:
specifier: 5.16.1
version: 5.16.1(react-dom@18.2.0)(react@18.2.0)
lodash:
specifier: ^4.17.21
version: 4.17.21
react:
specifier: 18.x
version: 18.2.0
react-i18next:
specifier: ^11.15.1
version: 11.18.6(i18next@22.5.1)(react-dom@18.2.0)(react@18.2.0)
sequelize:
specifier: ^6.26.0
version: 6.35.2
packages/plugins/@tachybase/plugin-workflow-test:
dependencies:
'@tachybase/client':
@ -4919,6 +4974,21 @@ packages:
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
/@ant-design/icons@5.3.7(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-bCPXTAg66f5bdccM4TT21SQBDO1Ek2gho9h3nO9DAKXJP4sq+5VBjrQMSxMVXSB3HyEz+cUbHQ5+6ogxCOpaew==}
engines: {node: '>=8'}
peerDependencies:
react: '>=16.0.0'
react-dom: '>=16.0.0'
dependencies:
'@ant-design/colors': 7.0.2
'@ant-design/icons-svg': 4.4.2
'@babel/runtime': 7.24.5
classnames: 2.5.1
rc-util: 5.41.0(react-dom@18.2.0)(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
/@ant-design/plots@2.1.14(lodash-es@4.17.21)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-ZFhvlNUsPcvD8d+nX4bXA9djbEUDVfKm8/xvFplffWT68rWM37d32qI7EghSvLUreszqh60DZAy80wCA08lgoQ==}
peerDependencies:
@ -4972,7 +5042,7 @@ packages:
'@babel/runtime': 7.24.5
'@ctrl/tinycolor': 3.6.1
antd: 5.16.1(react-dom@18.2.0)(react@18.2.0)
rc-util: 5.39.1(react-dom@18.2.0)(react@18.2.0)
rc-util: 5.41.0(react-dom@18.2.0)(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
swr: 2.2.4(react@18.2.0)
@ -4984,14 +5054,14 @@ packages:
react: '>=17.0.0'
react-dom: '>=17.0.0'
dependencies:
'@ant-design/icons': 5.3.6(react-dom@18.2.0)(react@18.2.0)
'@ant-design/icons': 5.3.7(react-dom@18.2.0)(react@18.2.0)
'@ant-design/pro-provider': 2.13.5(antd@5.16.1)(react-dom@18.2.0)(react@18.2.0)
'@babel/runtime': 7.24.5
antd: 5.16.1(react-dom@18.2.0)(react@18.2.0)
classnames: 2.5.1
dayjs: 1.11.10
lodash.merge: 4.6.2
rc-util: 5.39.1(react-dom@18.2.0)(react@18.2.0)
rc-util: 5.41.0(react-dom@18.2.0)(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
safe-stable-stringify: 2.4.3
@ -10506,7 +10576,7 @@ packages:
'@babel/runtime': 7.24.5
'@ctrl/tinycolor': 3.6.1
classnames: 2.5.1
rc-util: 5.39.1(react-dom@18.2.0)(react@18.2.0)
rc-util: 5.41.0(react-dom@18.2.0)(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
@ -10517,7 +10587,7 @@ packages:
react-dom: '>=16.9.0'
dependencies:
'@babel/runtime': 7.24.5
rc-util: 5.39.1(react-dom@18.2.0)(react@18.2.0)
rc-util: 5.41.0(react-dom@18.2.0)(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
@ -10536,7 +10606,7 @@ packages:
dependencies:
'@babel/runtime': 7.24.5
classnames: 2.5.1
rc-util: 5.39.1(react-dom@18.2.0)(react@18.2.0)
rc-util: 5.41.0(react-dom@18.2.0)(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
@ -10549,7 +10619,7 @@ packages:
dependencies:
'@babel/runtime': 7.24.5
classnames: 2.5.1
rc-util: 5.39.1(react-dom@18.2.0)(react@18.2.0)
rc-util: 5.41.0(react-dom@18.2.0)(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
@ -10562,9 +10632,9 @@ packages:
dependencies:
'@babel/runtime': 7.24.5
'@rc-component/portal': 1.1.2(react-dom@18.2.0)(react@18.2.0)
'@rc-component/trigger': 2.1.0(react-dom@18.2.0)(react@18.2.0)
'@rc-component/trigger': 2.1.1(react-dom@18.2.0)(react@18.2.0)
classnames: 2.5.1
rc-util: 5.39.1(react-dom@18.2.0)(react@18.2.0)
rc-util: 5.41.0(react-dom@18.2.0)(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
@ -10578,9 +10648,9 @@ packages:
'@babel/runtime': 7.24.5
'@rc-component/portal': 1.1.2(react-dom@18.2.0)(react@18.2.0)
classnames: 2.5.1
rc-motion: 2.9.0(react-dom@18.2.0)(react@18.2.0)
rc-motion: 2.9.1(react-dom@18.2.0)(react@18.2.0)
rc-resize-observer: 1.4.0(react-dom@18.2.0)(react@18.2.0)
rc-util: 5.39.1(react-dom@18.2.0)(react@18.2.0)
rc-util: 5.41.0(react-dom@18.2.0)(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
dev: false
@ -10595,9 +10665,25 @@ packages:
'@babel/runtime': 7.24.5
'@rc-component/portal': 1.1.2(react-dom@18.2.0)(react@18.2.0)
classnames: 2.5.1
rc-motion: 2.9.0(react-dom@18.2.0)(react@18.2.0)
rc-motion: 2.9.1(react-dom@18.2.0)(react@18.2.0)
rc-resize-observer: 1.4.0(react-dom@18.2.0)(react@18.2.0)
rc-util: 5.39.1(react-dom@18.2.0)(react@18.2.0)
rc-util: 5.41.0(react-dom@18.2.0)(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
/@rc-component/trigger@2.1.1(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-UjHkedkgtEcgQu87w1VuWug1idoDJV7VUt0swxHXRcmei2uu1AuUzGBPEUlmOmXGJ+YtTgZfVLi7kuAUKoZTMA==}
engines: {node: '>=8.x'}
peerDependencies:
react: '>=16.9.0'
react-dom: '>=16.9.0'
dependencies:
'@babel/runtime': 7.24.5
'@rc-component/portal': 1.1.2(react-dom@18.2.0)(react@18.2.0)
classnames: 2.5.1
rc-motion: 2.9.1(react-dom@18.2.0)(react@18.2.0)
rc-resize-observer: 1.4.0(react-dom@18.2.0)(react@18.2.0)
rc-util: 5.41.0(react-dom@18.2.0)(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
@ -14276,7 +14362,7 @@ packages:
dependencies:
'@ant-design/colors': 7.0.2
'@ant-design/cssinjs': 1.20.0(react-dom@18.2.0)(react@18.2.0)
'@ant-design/icons': 5.3.6(react-dom@18.2.0)(react@18.2.0)
'@ant-design/icons': 5.3.7(react-dom@18.2.0)(react@18.2.0)
'@ant-design/react-slick': 1.1.2(react@18.2.0)
'@babel/runtime': 7.24.5
'@ctrl/tinycolor': 3.6.1
@ -25654,7 +25740,7 @@ packages:
'@babel/runtime': 7.24.5
classnames: 2.5.1
dom-align: 1.12.4
rc-util: 5.39.1(react-dom@18.2.0)(react@18.2.0)
rc-util: 5.41.0(react-dom@18.2.0)(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
resize-observer-polyfill: 1.5.1
@ -25670,8 +25756,8 @@ packages:
array-tree-filter: 2.1.0
classnames: 2.5.1
rc-select: 14.13.0(react-dom@18.2.0)(react@18.2.0)
rc-tree: 5.8.5(react-dom@18.2.0)(react@18.2.0)
rc-util: 5.39.1(react-dom@18.2.0)(react@18.2.0)
rc-tree: 5.8.7(react-dom@18.2.0)(react@18.2.0)
rc-util: 5.41.0(react-dom@18.2.0)(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
@ -25683,7 +25769,7 @@ packages:
dependencies:
'@babel/runtime': 7.24.5
classnames: 2.5.1
rc-util: 5.39.1(react-dom@18.2.0)(react@18.2.0)
rc-util: 5.41.0(react-dom@18.2.0)(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
@ -25695,8 +25781,8 @@ packages:
dependencies:
'@babel/runtime': 7.24.5
classnames: 2.5.1
rc-motion: 2.9.0(react-dom@18.2.0)(react@18.2.0)
rc-util: 5.39.1(react-dom@18.2.0)(react@18.2.0)
rc-motion: 2.9.1(react-dom@18.2.0)(react@18.2.0)
rc-util: 5.41.0(react-dom@18.2.0)(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
@ -25709,8 +25795,8 @@ packages:
'@babel/runtime': 7.24.5
'@rc-component/portal': 1.1.2(react-dom@18.2.0)(react@18.2.0)
classnames: 2.5.1
rc-motion: 2.9.0(react-dom@18.2.0)(react@18.2.0)
rc-util: 5.39.1(react-dom@18.2.0)(react@18.2.0)
rc-motion: 2.9.1(react-dom@18.2.0)(react@18.2.0)
rc-util: 5.41.0(react-dom@18.2.0)(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
@ -25723,8 +25809,8 @@ packages:
'@babel/runtime': 7.24.5
'@rc-component/portal': 1.1.2(react-dom@18.2.0)(react@18.2.0)
classnames: 2.5.1
rc-motion: 2.9.0(react-dom@18.2.0)(react@18.2.0)
rc-util: 5.39.1(react-dom@18.2.0)(react@18.2.0)
rc-motion: 2.9.1(react-dom@18.2.0)(react@18.2.0)
rc-util: 5.41.0(react-dom@18.2.0)(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
@ -25735,9 +25821,9 @@ packages:
react-dom: '>=16.11.0'
dependencies:
'@babel/runtime': 7.24.5
'@rc-component/trigger': 2.1.0(react-dom@18.2.0)(react@18.2.0)
'@rc-component/trigger': 2.1.1(react-dom@18.2.0)(react@18.2.0)
classnames: 2.5.1
rc-util: 5.39.1(react-dom@18.2.0)(react@18.2.0)
rc-util: 5.41.0(react-dom@18.2.0)(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
@ -25750,7 +25836,7 @@ packages:
dependencies:
'@babel/runtime': 7.24.5
async-validator: 4.2.5
rc-util: 5.39.1(react-dom@18.2.0)(react@18.2.0)
rc-util: 5.41.0(react-dom@18.2.0)(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
@ -25763,7 +25849,7 @@ packages:
dependencies:
'@babel/runtime': 7.24.5
async-validator: 4.2.5
rc-util: 5.39.1(react-dom@18.2.0)(react@18.2.0)
rc-util: 5.41.0(react-dom@18.2.0)(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
@ -25777,8 +25863,8 @@ packages:
'@rc-component/portal': 1.1.2(react-dom@18.2.0)(react@18.2.0)
classnames: 2.5.1
rc-dialog: 9.4.0(react-dom@18.2.0)(react@18.2.0)
rc-motion: 2.9.0(react-dom@18.2.0)(react@18.2.0)
rc-util: 5.39.1(react-dom@18.2.0)(react@18.2.0)
rc-motion: 2.9.1(react-dom@18.2.0)(react@18.2.0)
rc-util: 5.41.0(react-dom@18.2.0)(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
@ -25792,7 +25878,7 @@ packages:
'@rc-component/mini-decimal': 1.1.0
classnames: 2.5.1
rc-input: 1.4.5(react-dom@18.2.0)(react@18.2.0)
rc-util: 5.39.1(react-dom@18.2.0)(react@18.2.0)
rc-util: 5.41.0(react-dom@18.2.0)(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
@ -25804,7 +25890,7 @@ packages:
dependencies:
'@babel/runtime': 7.24.5
classnames: 2.5.1
rc-util: 5.39.1(react-dom@18.2.0)(react@18.2.0)
rc-util: 5.41.0(react-dom@18.2.0)(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
@ -25815,12 +25901,12 @@ packages:
react-dom: '>=16.9.0'
dependencies:
'@babel/runtime': 7.24.5
'@rc-component/trigger': 2.1.0(react-dom@18.2.0)(react@18.2.0)
'@rc-component/trigger': 2.1.1(react-dom@18.2.0)(react@18.2.0)
classnames: 2.5.1
rc-input: 1.4.5(react-dom@18.2.0)(react@18.2.0)
rc-menu: 9.13.0(react-dom@18.2.0)(react@18.2.0)
rc-textarea: 1.6.3(react-dom@18.2.0)(react@18.2.0)
rc-util: 5.39.1(react-dom@18.2.0)(react@18.2.0)
rc-util: 5.41.0(react-dom@18.2.0)(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
@ -25831,11 +25917,11 @@ packages:
react-dom: '>=16.9.0'
dependencies:
'@babel/runtime': 7.24.5
'@rc-component/trigger': 2.1.0(react-dom@18.2.0)(react@18.2.0)
'@rc-component/trigger': 2.1.1(react-dom@18.2.0)(react@18.2.0)
classnames: 2.5.1
rc-motion: 2.9.0(react-dom@18.2.0)(react@18.2.0)
rc-motion: 2.9.1(react-dom@18.2.0)(react@18.2.0)
rc-overflow: 1.3.2(react-dom@18.2.0)(react@18.2.0)
rc-util: 5.39.1(react-dom@18.2.0)(react@18.2.0)
rc-util: 5.41.0(react-dom@18.2.0)(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
@ -25847,7 +25933,19 @@ packages:
dependencies:
'@babel/runtime': 7.24.5
classnames: 2.5.1
rc-util: 5.39.1(react-dom@18.2.0)(react@18.2.0)
rc-util: 5.41.0(react-dom@18.2.0)(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
/rc-motion@2.9.1(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-QD4bUqByjVQs7PhUT1d4bNxvtTcK9ETwtg7psbDfo6TmYalH/1hhjj4r2hbhW7g5OOEqYHhfwfj4noIvuOVRtQ==}
peerDependencies:
react: '>=16.9.0'
react-dom: '>=16.9.0'
dependencies:
'@babel/runtime': 7.24.5
classnames: 2.5.1
rc-util: 5.41.0(react-dom@18.2.0)(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
@ -25860,8 +25958,8 @@ packages:
dependencies:
'@babel/runtime': 7.24.5
classnames: 2.5.1
rc-motion: 2.9.0(react-dom@18.2.0)(react@18.2.0)
rc-util: 5.39.1(react-dom@18.2.0)(react@18.2.0)
rc-motion: 2.9.1(react-dom@18.2.0)(react@18.2.0)
rc-util: 5.41.0(react-dom@18.2.0)(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
@ -25874,7 +25972,7 @@ packages:
'@babel/runtime': 7.24.5
classnames: 2.5.1
rc-resize-observer: 1.4.0(react-dom@18.2.0)(react@18.2.0)
rc-util: 5.39.1(react-dom@18.2.0)(react@18.2.0)
rc-util: 5.41.0(react-dom@18.2.0)(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
@ -25886,7 +25984,7 @@ packages:
dependencies:
'@babel/runtime': 7.24.5
classnames: 2.5.1
rc-util: 5.39.1(react-dom@18.2.0)(react@18.2.0)
rc-util: 5.41.0(react-dom@18.2.0)(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
@ -25911,12 +26009,12 @@ packages:
optional: true
dependencies:
'@babel/runtime': 7.24.5
'@rc-component/trigger': 2.1.0(react-dom@18.2.0)(react@18.2.0)
'@rc-component/trigger': 2.1.1(react-dom@18.2.0)(react@18.2.0)
classnames: 2.5.1
dayjs: 1.11.10
rc-overflow: 1.3.2(react-dom@18.2.0)(react@18.2.0)
rc-resize-observer: 1.4.0(react-dom@18.2.0)(react@18.2.0)
rc-util: 5.39.1(react-dom@18.2.0)(react@18.2.0)
rc-util: 5.41.0(react-dom@18.2.0)(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
@ -25928,7 +26026,7 @@ packages:
dependencies:
'@babel/runtime': 7.24.5
classnames: 2.5.1
rc-util: 5.39.1(react-dom@18.2.0)(react@18.2.0)
rc-util: 5.41.0(react-dom@18.2.0)(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
@ -25941,7 +26039,7 @@ packages:
dependencies:
'@babel/runtime': 7.24.5
classnames: 2.5.1
rc-util: 5.39.1(react-dom@18.2.0)(react@18.2.0)
rc-util: 5.41.0(react-dom@18.2.0)(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
@ -25953,7 +26051,7 @@ packages:
dependencies:
'@babel/runtime': 7.24.5
classnames: 2.5.1
rc-util: 5.39.1(react-dom@18.2.0)(react@18.2.0)
rc-util: 5.41.0(react-dom@18.2.0)(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
resize-observer-polyfill: 1.5.1
@ -25966,8 +26064,8 @@ packages:
dependencies:
'@babel/runtime': 7.24.5
classnames: 2.5.1
rc-motion: 2.9.0(react-dom@18.2.0)(react@18.2.0)
rc-util: 5.39.1(react-dom@18.2.0)(react@18.2.0)
rc-motion: 2.9.1(react-dom@18.2.0)(react@18.2.0)
rc-util: 5.41.0(react-dom@18.2.0)(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
@ -25980,10 +26078,10 @@ packages:
dependencies:
'@babel/runtime': 7.24.5
classnames: 2.5.1
rc-motion: 2.9.0(react-dom@18.2.0)(react@18.2.0)
rc-motion: 2.9.1(react-dom@18.2.0)(react@18.2.0)
rc-overflow: 1.3.2(react-dom@18.2.0)(react@18.2.0)
rc-trigger: 5.3.4(react-dom@18.2.0)(react@18.2.0)
rc-util: 5.39.1(react-dom@18.2.0)(react@18.2.0)
rc-util: 5.41.0(react-dom@18.2.0)(react@18.2.0)
rc-virtual-list: 3.11.4(react-dom@18.2.0)(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
@ -26001,7 +26099,7 @@ packages:
classnames: 2.5.1
rc-motion: 2.9.0(react-dom@18.2.0)(react@18.2.0)
rc-overflow: 1.3.2(react-dom@18.2.0)(react@18.2.0)
rc-util: 5.39.1(react-dom@18.2.0)(react@18.2.0)
rc-util: 5.41.0(react-dom@18.2.0)(react@18.2.0)
rc-virtual-list: 3.11.3(react-dom@18.2.0)(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
@ -26015,11 +26113,11 @@ packages:
react-dom: '*'
dependencies:
'@babel/runtime': 7.24.5
'@rc-component/trigger': 2.1.0(react-dom@18.2.0)(react@18.2.0)
'@rc-component/trigger': 2.1.1(react-dom@18.2.0)(react@18.2.0)
classnames: 2.5.1
rc-motion: 2.9.0(react-dom@18.2.0)(react@18.2.0)
rc-motion: 2.9.1(react-dom@18.2.0)(react@18.2.0)
rc-overflow: 1.3.2(react-dom@18.2.0)(react@18.2.0)
rc-util: 5.39.1(react-dom@18.2.0)(react@18.2.0)
rc-util: 5.41.0(react-dom@18.2.0)(react@18.2.0)
rc-virtual-list: 3.11.4(react-dom@18.2.0)(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
@ -26033,7 +26131,7 @@ packages:
dependencies:
'@babel/runtime': 7.24.5
classnames: 2.5.1
rc-util: 5.39.1(react-dom@18.2.0)(react@18.2.0)
rc-util: 5.41.0(react-dom@18.2.0)(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
@ -26046,7 +26144,7 @@ packages:
dependencies:
'@babel/runtime': 7.24.5
classnames: 2.5.1
rc-util: 5.39.1(react-dom@18.2.0)(react@18.2.0)
rc-util: 5.41.0(react-dom@18.2.0)(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
@ -26058,7 +26156,7 @@ packages:
dependencies:
'@babel/runtime': 7.24.5
classnames: 2.5.1
rc-util: 5.39.1(react-dom@18.2.0)(react@18.2.0)
rc-util: 5.41.0(react-dom@18.2.0)(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
@ -26073,7 +26171,7 @@ packages:
'@rc-component/context': 1.4.0(react-dom@18.2.0)(react@18.2.0)
classnames: 2.5.1
rc-resize-observer: 1.4.0(react-dom@18.2.0)(react@18.2.0)
rc-util: 5.39.1(react-dom@18.2.0)(react@18.2.0)
rc-util: 5.41.0(react-dom@18.2.0)(react@18.2.0)
rc-virtual-list: 3.11.4(react-dom@18.2.0)(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
@ -26089,9 +26187,9 @@ packages:
classnames: 2.5.1
rc-dropdown: 4.2.0(react-dom@18.2.0)(react@18.2.0)
rc-menu: 9.13.0(react-dom@18.2.0)(react@18.2.0)
rc-motion: 2.9.0(react-dom@18.2.0)(react@18.2.0)
rc-motion: 2.9.1(react-dom@18.2.0)(react@18.2.0)
rc-resize-observer: 1.4.0(react-dom@18.2.0)(react@18.2.0)
rc-util: 5.39.1(react-dom@18.2.0)(react@18.2.0)
rc-util: 5.41.0(react-dom@18.2.0)(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
@ -26105,7 +26203,7 @@ packages:
classnames: 2.5.1
rc-input: 1.4.5(react-dom@18.2.0)(react@18.2.0)
rc-resize-observer: 1.4.0(react-dom@18.2.0)(react@18.2.0)
rc-util: 5.39.1(react-dom@18.2.0)(react@18.2.0)
rc-util: 5.41.0(react-dom@18.2.0)(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
@ -26116,7 +26214,7 @@ packages:
react-dom: '>=16.9.0'
dependencies:
'@babel/runtime': 7.24.5
'@rc-component/trigger': 2.1.0(react-dom@18.2.0)(react@18.2.0)
'@rc-component/trigger': 2.1.1(react-dom@18.2.0)(react@18.2.0)
classnames: 2.5.1
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
@ -26145,8 +26243,8 @@ packages:
'@babel/runtime': 7.24.5
classnames: 2.5.1
rc-select: 14.13.0(react-dom@18.2.0)(react@18.2.0)
rc-tree: 5.8.5(react-dom@18.2.0)(react@18.2.0)
rc-util: 5.39.1(react-dom@18.2.0)(react@18.2.0)
rc-tree: 5.8.7(react-dom@18.2.0)(react@18.2.0)
rc-util: 5.41.0(react-dom@18.2.0)(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
@ -26174,8 +26272,8 @@ packages:
dependencies:
'@babel/runtime': 7.24.5
classnames: 2.5.1
rc-motion: 2.9.0(react-dom@18.2.0)(react@18.2.0)
rc-util: 5.39.1(react-dom@18.2.0)(react@18.2.0)
rc-motion: 2.9.1(react-dom@18.2.0)(react@18.2.0)
rc-util: 5.41.0(react-dom@18.2.0)(react@18.2.0)
rc-virtual-list: 3.11.4(react-dom@18.2.0)(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
@ -26191,7 +26289,7 @@ packages:
'@babel/runtime': 7.24.5
classnames: 2.5.1
rc-motion: 2.9.0(react-dom@18.2.0)(react@18.2.0)
rc-util: 5.39.1(react-dom@18.2.0)(react@18.2.0)
rc-util: 5.41.0(react-dom@18.2.0)(react@18.2.0)
rc-virtual-list: 3.11.3(react-dom@18.2.0)(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
@ -26206,8 +26304,23 @@ packages:
dependencies:
'@babel/runtime': 7.24.5
classnames: 2.5.1
rc-motion: 2.9.0(react-dom@18.2.0)(react@18.2.0)
rc-util: 5.39.1(react-dom@18.2.0)(react@18.2.0)
rc-motion: 2.9.1(react-dom@18.2.0)(react@18.2.0)
rc-util: 5.41.0(react-dom@18.2.0)(react@18.2.0)
rc-virtual-list: 3.11.4(react-dom@18.2.0)(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
/rc-tree@5.8.7(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-cpsIQZ4nNYwpj6cqPRt52e/69URuNdgQF9wZ10InmEf8W3+i0A41OVmZWwHuX9gegQSqj+DPmaDkZFKQZ+ZV1w==}
engines: {node: '>=10.x'}
peerDependencies:
react: '*'
react-dom: '*'
dependencies:
'@babel/runtime': 7.24.5
classnames: 2.5.1
rc-motion: 2.9.1(react-dom@18.2.0)(react@18.2.0)
rc-util: 5.41.0(react-dom@18.2.0)(react@18.2.0)
rc-virtual-list: 3.11.4(react-dom@18.2.0)(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
@ -26222,8 +26335,8 @@ packages:
'@babel/runtime': 7.24.5
classnames: 2.5.1
rc-align: 4.0.15(react-dom@18.2.0)(react@18.2.0)
rc-motion: 2.9.0(react-dom@18.2.0)(react@18.2.0)
rc-util: 5.39.1(react-dom@18.2.0)(react@18.2.0)
rc-motion: 2.9.1(react-dom@18.2.0)(react@18.2.0)
rc-util: 5.41.0(react-dom@18.2.0)(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
dev: true
@ -26236,7 +26349,7 @@ packages:
dependencies:
'@babel/runtime': 7.24.5
classnames: 2.5.1
rc-util: 5.39.1(react-dom@18.2.0)(react@18.2.0)
rc-util: 5.41.0(react-dom@18.2.0)(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
@ -26262,6 +26375,17 @@ packages:
react-dom: 18.2.0(react@18.2.0)
react-is: 18.3.1
/rc-util@5.41.0(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-xtlCim9RpmVv0Ar2Nnc3WfJCxjQkTf3xHPWoFdjp1fSs2NirQwqiQrfqdU9HUe0kdfb168M/T8Dq0IaX50xeKg==}
peerDependencies:
react: '>=16.9.0'
react-dom: '>=16.9.0'
dependencies:
'@babel/runtime': 7.24.5
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
react-is: 18.3.1
/rc-virtual-list@3.11.3(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-tu5UtrMk/AXonHwHxUogdXAWynaXsrx1i6dsgg+lOo/KJSF8oBAcprh1z5J3xgnPJD5hXxTL58F8s8onokdt0Q==}
engines: {node: '>=8.x'}
@ -26272,7 +26396,7 @@ packages:
'@babel/runtime': 7.24.5
classnames: 2.5.1
rc-resize-observer: 1.4.0(react-dom@18.2.0)(react@18.2.0)
rc-util: 5.39.1(react-dom@18.2.0)(react@18.2.0)
rc-util: 5.41.0(react-dom@18.2.0)(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
dev: false
@ -26287,7 +26411,7 @@ packages:
'@babel/runtime': 7.24.5
classnames: 2.5.1
rc-resize-observer: 1.4.0(react-dom@18.2.0)(react@18.2.0)
rc-util: 5.39.1(react-dom@18.2.0)(react@18.2.0)
rc-util: 5.41.0(react-dom@18.2.0)(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)