feat: approval, carbon copy (#1330)

Co-authored-by: wjh <wwwjh0710@163.com>
Co-authored-by: sealday <zhanglin@daoyoucloud.com>
Reviewed-on: daoyoucloud/tachybase#1330
Co-authored-by: bai.zixv <bai.zixv@foxmail.com>
Co-committed-by: bai.zixv <bai.zixv@foxmail.com>
This commit is contained in:
bai.zixv 2024-07-17 10:58:29 +08:00 committed by sealday
parent 182d409c79
commit f28a45cf7e
83 changed files with 1628 additions and 36 deletions

View File

@ -0,0 +1,187 @@
import { COLLECTION_NAME_APPROVAL_CARBON_COPY } from '../../common/constants';
import { ApprovalStatusEnums } from '../constants';
import { NAMESPACE, tval } from '../locale';
import { approvalStatusOptions } from '../usage/approval-common/notice-columns/column.status';
export const CollectionApprovalCarbonCopy = {
title: tval('ApprovalCarbonCopy'),
name: COLLECTION_NAME_APPROVAL_CARBON_COPY,
fields: [
{
type: 'bigInt',
name: 'approvalId',
interface: 'number',
uiSchema: {
type: 'number',
title: 'ID',
'x-component': 'InputNumber',
},
},
{
type: 'belongsTo',
name: 'createdBy',
target: 'users',
foreignKey: 'createdById',
interface: 'm2o',
uiSchema: {
type: 'number',
title: `{{t("Initiator", { ns: "${NAMESPACE}" })}}`,
'x-component': 'RemoteSelect',
'x-component-props': {
fieldNames: {
label: 'nickname',
value: 'id',
},
service: {
resource: 'users',
},
},
},
},
{
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',
},
},
},
},
{
type: 'integer',
name: 'status',
interface: 'select',
uiSchema: {
type: 'number',
title: '{{t("Status", { ns: "workflow" })}}',
'x-component': 'Select',
enum: ApprovalStatusEnums,
},
},
],
};

View File

@ -1,5 +1,5 @@
import { ApprovalStatusEnums } from '../../constants'; import { ApprovalStatusEnums } from '../constants';
import { NAMESPACE, tval } from '../../locale'; import { NAMESPACE, tval } from '../locale';
export const CollectionApprovals = { export const CollectionApprovals = {
title: `{{t("Approval applications", { ns: "${NAMESPACE}" })}}`, title: `{{t("Approval applications", { ns: "${NAMESPACE}" })}}`,

View File

@ -1,7 +1,7 @@
import { NAMESPACE } from '../../locale'; import { tval } from '../locale';
export const CollectionFlowNodes = { export const CollectionFlowNodes = {
title: `{{t("Node", { ns: "${NAMESPACE}" })}}`, title: tval('Node'),
name: 'flow_nodes', name: 'flow_nodes',
fields: [ fields: [
{ {

View File

@ -1,4 +1,4 @@
import { NAMESPACE } from '../../locale'; import { NAMESPACE } from '../locale';
export const CollectionWorkflows = { export const CollectionWorkflows = {
title: `{{t("Workflow", { ns: "${NAMESPACE}" })}}`, title: `{{t("Workflow", { ns: "${NAMESPACE}" })}}`,

View File

@ -11,7 +11,7 @@ import { useForm } from '@tachybase/schema';
import { uid } from '@tachybase/utils/client'; import { uid } from '@tachybase/utils/client';
import { useFlowContext } from '../../../../../FlowContext'; import { useFlowContext } from '../../../../../FlowContext';
import { CollectionApprovalTodos } from '../../../common/ApprovalTodos.collection'; import { CollectionApprovalTodos } from '../../../collections/approvalRecords.collection';
import { NAMESPACE } from '../../../locale'; import { NAMESPACE } from '../../../locale';
import { SchemaAddBlock } from '../../trigger/launcher-interface/SchemaAddBlock.component'; import { SchemaAddBlock } from '../../trigger/launcher-interface/SchemaAddBlock.component';
import { ApproverBlock } from './ApproverBlock.view'; import { ApproverBlock } from './ApproverBlock.view';

View File

@ -0,0 +1,89 @@
import { ArrayItems } from '@tachybase/components';
import { GroupType, Instruction } from '../../../../nodes';
import { COLLECTION_NAME_APPROVAL_CARBON_COPY } 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 { CarbonCopyDetailContainer } from './show-interface/CarbonCopyDetailContainer.schema';
import { configSytles } from './style';
export class ApprovalCarbonCopyInstruction extends Instruction {
title = tval('ApprovalCarbonCopy');
type = COLLECTION_NAME_APPROVAL_CARBON_COPY;
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,
CarbonCopyDetail: CarbonCopyDetailContainer,
};
fieldset = {
carbonCopyPerson: {
type: 'array',
title: tval('The Notified Person'),
'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',
},
},
},
showCarbonCopyDetail: {
type: 'void',
title: tval('Show Notice Detail'),
required: true,
'x-decorator': 'FormItem',
'x-component': 'ConfigButtonNotice',
properties: {
showCarbonCopyDetail: {
type: 'void',
'x-component': 'CarbonCopyDetail',
},
},
},
};
isAvailable(params) {
return true;
}
}

View File

@ -0,0 +1,51 @@
import React, { useCallback, useState } from 'react';
import { ArrayItems } from '@tachybase/components';
import { Button, Popover, Space } from 'antd';
import { useWorkflowExecuted } from '../../../../../hooks';
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" 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,64 @@
import React from 'react';
import { RemoteSelect, SchemaComponent, useCollectionFilterOptions, useToken, Variable } from '@tachybase/client';
import { useField } from '@tachybase/schema';
import { FilterDynamicComponent } from '../../../../../components';
import { useWorkflowVariableOptions } from '../../../../../variable';
// XXX: 可以将这个选择组件封装为一个通用组件, 放在 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,27 @@
import { Plugin } from '@tachybase/client';
// import { ArrayItems } from '@tachybase/components';
import { PluginWorkflow } from '../../../../Plugin';
import { COLLECTION_NAME_APPROVAL_CARBON_COPY } from '../../../common/constants';
import { ApprovalCarbonCopyInstruction } from './ApprovalCarbonCopy.instruction';
import { SCCarbonCopyDetail } from './show-interface/CarbonCopyDetail.schema';
// NOTE: 试着用一种新的约束规则去声明文件属于哪种功能. 比如 kit.ts 导出的是单个 Plugin 下的功能集成
// 注册一个节点
export class KitCarbonCopy extends Plugin {
async afterAdd() {
await this.app.pm.add(SCCarbonCopyDetail);
}
async load() {
const pluginWorkflow = this.app.pm.get(PluginWorkflow);
pluginWorkflow.registerInstruction(COLLECTION_NAME_APPROVAL_CARBON_COPY, ApprovalCarbonCopyInstruction);
// this.app.addComponents({
// ArrayItems,
// });
// this.app.schemaInitializerManager.add(ApproverActionConfigInitializer);
// this.app.schemaInitializerManager.add(ApproverAddBlockInitializer);
}
}

View File

@ -0,0 +1,46 @@
import React from 'react';
import {
createReadPrettyFormBlockSchema,
parseCollectionName,
SchemaInitializerItem,
useRecordCollectionDataSourceItems,
useSchemaInitializer,
useSchemaInitializerItem,
useSchemaTemplateManager,
} from '@tachybase/client';
import _ from 'lodash';
import { useFlowContext } from '../../../../../FlowContext';
export const CarbonCopyDetailAddBlock = () => {
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,75 @@
import { gridRowColWrap, SchemaInitializer, usePlugin } from '@tachybase/client';
import { useFlowContext } from '../../../../../FlowContext';
import { useAvailableUpstreams, useNodeContext } from '../../../../../nodes';
import { PluginWorkflow } from '../../../../../Plugin';
import { useTrigger } from '../../../../../triggers';
import { tval } from '../../../locale';
import { CarbonCopyDetailAddBlock } from './CarbonCopyDetail.block';
export const CarbonCopyDetailInitializer = new SchemaInitializer({
name: 'CarbonCopyDetailInitializer',
wrap: gridRowColWrap,
title: "{{t('Add block')}}",
items: [
{
name: 'notice',
type: 'itemGroup',
title: tval('Notice blocks'),
children: [
{
name: 'detail',
type: 'item',
title: '{{t("Details")}}',
Component: CarbonCopyDetailAddBlock,
},
],
},
{
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 { CarbonCopyDetailInitializer } from './CarbonCopyDetail.initializer';
export class SCCarbonCopyDetail extends Plugin {
async load() {
this.app.schemaInitializerManager.add(CarbonCopyDetailInitializer);
}
}
function createNewSchema(id) {
return {
type: 'void',
name: id,
'x-uid': id,
'x-component': 'Grid',
'x-initializer': 'CarbonCopyDetailInitializer',
};
}
export const CarbonCopyDetail = (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,44 @@
import React, { useContext, useMemo, useState } from 'react';
import { SchemaComponent, SchemaComponentContext } from '@tachybase/client';
import { uid } from '@tachybase/utils/client';
import { useFlowContext } from '../../../../../FlowContext';
import { tval } from '../../../locale';
import { CarbonCopyDetail } from './CarbonCopyDetail.schema';
import { schemaStyle } from './style';
export const CarbonCopyDetailContainer = () => {
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={{ CarbonCopyDetail: CarbonCopyDetail }} 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: {
showCarbonCopyDetail: {
type: 'string',
'x-component': 'CarbonCopyDetail',
},
},
};

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

@ -1,6 +1,6 @@
import { Plugin } from '@tachybase/client'; import { Plugin } from '@tachybase/client';
import { KitApprovalInstruction } from './instruction/plugin'; import { KitApprovalInstruction } from './instruction-approval/kit';
import { KitApprovalTrigger } from './trigger/plugin'; import { KitApprovalTrigger } from './trigger/plugin';
export default class KitApprovalConfiguration extends Plugin { export default class KitApprovalConfiguration extends Plugin {

View File

@ -1,11 +1,13 @@
import { Plugin } from '@tachybase/client'; import { Plugin } from '@tachybase/client';
import { KitCarbonCopy } from './configuration/instruction-carbon-copy/kit';
import KitApprovalConfiguration from './configuration/plugin'; import KitApprovalConfiguration from './configuration/plugin';
import { KitApprovalUsage } from './usage/plugin'; import { KitApprovalUsage } from './usage/plugin';
export class PluginApproval extends Plugin { export class PluginApproval extends Plugin {
async afterAdd() { async afterAdd() {
this.pm.add(KitApprovalConfiguration); this.pm.add(KitApprovalConfiguration);
this.pm.add(KitCarbonCopy);
this.pm.add(KitApprovalUsage); this.pm.add(KitApprovalUsage);
} }
async beforeLoad() {} async beforeLoad() {}

View File

@ -1,10 +1,10 @@
import React from 'react'; import React from 'react';
import { ExtendCollectionsProvider, TableBlockProvider, useRecord } from '@tachybase/client'; import { ExtendCollectionsProvider, TableBlockProvider, useRecord } from '@tachybase/client';
import { CollectionApprovalTodos } from '../../common/ApprovalTodos.collection'; import { CollectionApprovalTodos } from '../../collections/approvalRecords.collection';
import { CollectionApprovals } from '../approval-common/Approvals.collection'; import { CollectionApprovals } from '../../collections/approvals.collection';
import { CollectionFlowNodes } from '../approval-common/FlowNodes.collection'; import { CollectionFlowNodes } from '../../collections/flowNodes.collection';
import { CollectionWorkflows } from '../approval-common/Workflows.collection'; import { CollectionWorkflows } from '../../collections/workflows.collection';
export const ApprovalBlockProvider = ({ children, ...props }) => { export const ApprovalBlockProvider = ({ children, ...props }) => {
const { const {

View File

@ -4,8 +4,11 @@ import { uid } from '@tachybase/utils/client';
import { TableOutlined } from '@ant-design/icons'; import { TableOutlined } from '@ant-design/icons';
import { COLLECTION_NAME_APPROVAL_CARBON_COPY } from '../../../common/constants';
import { NAMESPACE } from '../../locale'; import { NAMESPACE } from '../../locale';
import { ApprovalBlockProvider } from './ApprovalBlock.provider'; import { ApprovalBlockProvider } from './ApprovalBlock.provider';
import { CarbonCopyBlockProvider } from './carbon-copy/CarbonCopyBlock.provider';
import { CarbonCopyCenter } from './carbon-copy/CarbonCopyCenter.schema';
import { ApprovalBlockLaunch } from './launch/VC.ApprovalBlockLaunch'; import { ApprovalBlockLaunch } from './launch/VC.ApprovalBlockLaunch';
import { ApprovalBlockLaunchApplication } from './launch/VC.ApprovalBlockLaunchApplication'; import { ApprovalBlockLaunchApplication } from './launch/VC.ApprovalBlockLaunchApplication';
import { ApprovalBlockTodos } from './todos/VC.ApprovalBlockTodos'; import { ApprovalBlockTodos } from './todos/VC.ApprovalBlockTodos';
@ -17,6 +20,8 @@ export class SCApprovalBlock extends Plugin {
'ApprovalBlock.Launch': ApprovalBlockLaunch, 'ApprovalBlock.Launch': ApprovalBlockLaunch,
'ApprovalBlock.Launch.Application': ApprovalBlockLaunchApplication, 'ApprovalBlock.Launch.Application': ApprovalBlockLaunchApplication,
'ApprovalBlock.Todos': ApprovalBlockTodos, 'ApprovalBlock.Todos': ApprovalBlockTodos,
CarbonCopyBlockProvider: CarbonCopyBlockProvider,
CarbonCopyCenter: CarbonCopyCenter,
}); });
} }
} }
@ -70,23 +75,61 @@ const schemaItems = [
], ],
}, },
}, },
{
type: 'item',
title: `{{t("CarbonCopy", { ns: "${NAMESPACE}" })}}`,
'x-decorator': 'CarbonCopyBlockProvider',
'x-component': 'CarbonCopyCenter',
'x-toolbar': 'BlockSchemaToolbar',
'x-settings': 'blockSettings:table',
collection: COLLECTION_NAME_APPROVAL_CARBON_COPY,
params: {
appends: [
'createdBy.id',
'createdBy.nickname',
'approval.status',
'user.id',
'user.nickname',
'node.id',
'node.title',
'job.id',
'job.status',
'job.result',
'workflow.id',
'workflow.title',
'workflow.enabled',
'execution.id',
'execution.status',
],
},
},
]; ];
const getSchemaInsert = ({ item }) => { const getSchemaInsert = ({ item }) => {
const id = uid(); const id = uid();
const { collection, params, ['x-component']: xcomponent, action } = item; const {
collection,
params,
['x-component']: xcomponent,
action,
['x-decorator']: decorator,
['x-toolbar']: toolbar,
['x-settings']: settings,
} = item;
return { return {
type: 'void', type: 'void',
name: id, name: id,
'x-uid': id, 'x-uid': id,
'x-designer': 'TableBlockDesigner', 'x-designer': 'TableBlockDesigner',
'x-decorator': 'ApprovalBlock.Decorator', 'x-decorator': decorator || 'ApprovalBlock.Decorator',
'x-decorator-props': { 'x-decorator-props': {
collection, collection,
params, params,
action: action || 'listCentralized', action: action || 'listCentralized',
}, },
'x-component': 'CardItem', 'x-component': 'CardItem',
'x-toolbar': toolbar,
'x-settings': settings,
properties: { properties: {
block: { block: {
type: 'void', type: 'void',

View File

@ -0,0 +1,45 @@
import React from 'react';
import { ExtendCollectionsProvider, TableBlockProvider, useRecord } from '@tachybase/client';
import { CollectionApprovalCarbonCopy } from '../../../collections/approvalCarbonCopy.collection';
import { CollectionFlowNodes } from '../../../collections/flowNodes.collection';
import { CollectionWorkflows } from '../../../collections/workflows.collection';
export const CarbonCopyBlockProvider = ({ 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, CollectionApprovalCarbonCopy]}>
<TableBlockProvider name={collection} {...config}>
{children}
</TableBlockProvider>
</ExtendCollectionsProvider>
);
};

View File

@ -0,0 +1,217 @@
import React from 'react';
import { css, SchemaComponent } from '@tachybase/client';
import { NAMESPACE, tval } from '../../../locale';
import { ColumnApprovalStatus } from '../../approval-common/approval-columns/approvalStatus.column';
import { ColumnAction } from '../../approval-common/notice-columns/column.action';
import { ColumnNode } from '../../approval-common/notice-columns/column.node';
import { ColumnStatus } from '../../approval-common/notice-columns/column.status';
import { ColumnUser } from '../../approval-common/notice-columns/column.user';
import { ColumnWorkflow } from '../../approval-common/notice-columns/column.workflow';
const schemaStyles = {
ActionbarStyle: css`
margin-bottom: 16px;
`,
TableV2Style: css`
.ant-table-cell {
text-align: center;
}
`,
};
export const CarbonCopyCenter = () => (
<SchemaComponent
components={{
ColumnNode,
ColumnWorkflow,
ColumnUser,
ColumnStatus,
ColumnAction,
ColumnApprovalStatus,
}}
schema={schema}
/>
);
const schema = {
type: 'void',
name: 'approval-carbon-copy-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',
},
},
},
approvalId: {
type: 'void',
'x-decorator': 'TableV2.Column.Decorator',
'x-component': 'TableV2.Column',
'x-component-props': {
width: 80,
},
title: tval('ID'),
properties: {
approvalId: {
type: 'number',
'x-component': 'Input',
'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,
},
},
},
createdBy: {
type: 'void',
title: tval('Initiator'),
'x-decorator': 'TableV2.Column.Decorator',
'x-component': 'TableV2.Column',
'x-component-props': { width: 160 },
properties: {
createdBy: {
type: 'string',
'x-component': 'ColumnUser',
'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,
},
},
},
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': 'ColumnApprovalStatus',
'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,
},
},
},
},
},
},
};

View File

@ -74,7 +74,7 @@ export const SchemaApprovalBlockLaunch = {
'x-decorator': 'TableV2.Column.Decorator', 'x-decorator': 'TableV2.Column.Decorator',
'x-component': 'TableV2.Column', 'x-component': 'TableV2.Column',
'x-component-props': { width: 80 }, 'x-component-props': { width: 80 },
title: '{{t("ID")}}', title: tval('ID'),
properties: { properties: {
id: { id: {
type: 'number', type: 'number',

View File

@ -1,9 +1,9 @@
import React from 'react'; import React from 'react';
import { SchemaComponent } from '@tachybase/client'; import { SchemaComponent } from '@tachybase/client';
import { NodeColumn } from '../../approval-common/approval-columns/column.node'; import { NodeColumn } from '../../approval-common/approval-columns/node.column';
import { UserColumn } from '../../approval-common/approval-columns/column.user'; import { UserColumn } from '../../approval-common/approval-columns/user.column';
import { WorkflowColumn } from '../../approval-common/approval-columns/column.workflow'; import { WorkflowColumn } from '../../approval-common/approval-columns/workflow.column';
import { ApplyButton } from './apply-button/VC.ApplyButton'; import { ApplyButton } from './apply-button/VC.ApplyButton';
import { SchemaApprovalBlockLaunch } from './Sm.ApprovalBlockLaunch'; import { SchemaApprovalBlockLaunch } from './Sm.ApprovalBlockLaunch';

View File

@ -61,7 +61,7 @@ export const ApprovalBlockLaunchApplication = (props) => {
useEffect(() => { useEffect(() => {
run(); run();
}, []); }, [decorator?.params?.filter]);
const onClick = useCallback( const onClick = useCallback(
(targetItems) => { (targetItems) => {

View File

@ -66,7 +66,7 @@ export const SchemaApprovalBlockTodos = {
'x-component-props': { 'x-component-props': {
width: 80, width: 80,
}, },
title: '{{t("ID")}}', title: tval('ID'),
properties: { properties: {
approvalId: { approvalId: {
type: 'number', type: 'number',

View File

@ -1,10 +1,10 @@
import React from 'react'; import React from 'react';
import { SchemaComponent } from '@tachybase/client'; import { SchemaComponent } from '@tachybase/client';
import { NodeColumn } from '../../approval-common/approval-columns/column.node'; import { NodeColumn } from '../../approval-common/approval-columns/node.column';
import { ApprovalRecordStatusColumn } from '../../approval-common/approval-columns/column.status'; import { ApprovalRecordStatusColumn } from '../../approval-common/approval-columns/status.column';
import { UserColumn } from '../../approval-common/approval-columns/column.user'; import { UserColumn } from '../../approval-common/approval-columns/user.column';
import { WorkflowColumn } from '../../approval-common/approval-columns/column.workflow'; import { WorkflowColumn } from '../../approval-common/approval-columns/workflow.column';
import { SchemaApprovalBlockTodos } from './Sm.ApprovalBlockTodos'; import { SchemaApprovalBlockTodos } from './Sm.ApprovalBlockTodos';
// 审批-待办 // 审批-待办

View File

@ -0,0 +1,26 @@
import React from 'react';
import { useCollectionRecordData, useCompile } from '@tachybase/client';
import { observer } from '@tachybase/schema';
import { Tag } from 'antd';
import _ from 'lodash';
import { ApprovalStatusEnumDict } from '../../../constants';
export const ColumnApprovalStatus = observer(
() => {
const record = useCollectionRecordData();
const { approval } = record;
return <ColumnStatusComponent value={approval?.status} record={record} />;
},
{ displayName: 'ColumnApprovalStatus' },
);
// 审批-待办:状态列:值
export const ColumnStatusComponent = (props) => {
const compile = useCompile();
const { value } = props;
const option = ApprovalStatusEnumDict[value];
return <Tag color={option.color}>{compile(option.label)}</Tag>;
};

View File

@ -0,0 +1,42 @@
import React from 'react';
import { SchemaComponent, useCollectionRecordData } from '@tachybase/client';
import { NoticeDetailContent } from '../notice-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 { Field, observer, useField } 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 { Field, observer, useField } from '@tachybase/schema';
import { Tag } from 'antd';
import _ from 'lodash';
import { EXECUTION_STATUS } from '../../../../../constants';
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 { Field, observer, useField } 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,29 @@
import React from 'react';
import { css } from '@tachybase/client';
import { Field, observer, useField } from '@tachybase/schema';
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,78 @@
import React from 'react';
import {
SchemaComponentContext,
useCollectionRecordData,
useRequest,
useSchemaComponentContext,
} from '@tachybase/client';
import { Result, Spin } from 'antd';
import { ExecutionContextProvider } from '../../../../../ExecutionContextProvider';
import { COLLECTION_NAME_APPROVAL_CARBON_COPY } 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.showCarbonCopyDetail;
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_NAME_APPROVAL_CARBON_COPY,
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,66 @@
import React from 'react';
import {
RemoteSchemaComponent,
SchemaComponent,
SchemaComponentProvider,
useFormBlockContext,
} from '@tachybase/client';
import { DetailsBlockProvider } from '../../../../../components';
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,
SchemaComponentProvider,
DetailsBlockProvider,
}}
scope={{
usePropsNoticeDetail,
useDetailsBlockProps: useFormBlockContext,
}}
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

@ -4,7 +4,7 @@ import { useCompile } from '@tachybase/client';
import { Tag } from 'antd'; import { Tag } from 'antd';
import { APPROVAL_STATUS, approvalStatusConfigObj, ApprovalStatusEnumDict } from '../../../constants'; import { APPROVAL_STATUS, approvalStatusConfigObj, ApprovalStatusEnumDict } from '../../../constants';
import { ColumnStatusComponent } from '../approval-columns/column.status'; import { ColumnStatusComponent } from '../approval-columns/status.column';
export function renderColumnStatus(value, record, exist) { export function renderColumnStatus(value, record, exist) {
// return null; // return null;

View File

@ -8,10 +8,10 @@ import {
useRecord, useRecord,
} from '@tachybase/client'; } from '@tachybase/client';
import { CollectionApprovalTodos } from '../../common/ApprovalTodos.collection'; import { CollectionApprovalTodos } from '../../collections/approvalRecords.collection';
import { CollectionApprovals } from '../approval-common/Approvals.collection'; import { CollectionApprovals } from '../../collections/approvals.collection';
import { CollectionFlowNodes } from '../approval-common/FlowNodes.collection'; import { CollectionFlowNodes } from '../../collections/flowNodes.collection';
import { CollectionWorkflows } from '../approval-common/Workflows.collection'; import { CollectionWorkflows } from '../../collections/workflows.collection';
export function RecordApprovalsDecorator({ params, children }) { export function RecordApprovalsDecorator({ params, children }) {
const collection = useCollection_deprecated(); const collection = useCollection_deprecated();

View File

@ -1,9 +1,9 @@
import React from 'react'; import React from 'react';
import { SchemaComponent } from '@tachybase/client'; import { SchemaComponent } from '@tachybase/client';
import { ApprovalRecordStatusColumn } from '../approval-common/approval-columns/column.status'; import { ApprovalRecordStatusColumn } from '../approval-common/approval-columns/status.column';
import { UserColumn } from '../approval-common/approval-columns/column.user'; import { UserColumn } from '../approval-common/approval-columns/user.column';
import { WorkflowColumn } from '../approval-common/approval-columns/column.workflow'; import { WorkflowColumn } from '../approval-common/approval-columns/workflow.column';
import { SchemaRecordApprovals } from './RecordApprovals.schema'; import { SchemaRecordApprovals } from './RecordApprovals.schema';
// 视图组件,创建区块-相关审批 // 视图组件,创建区块-相关审批

View File

@ -1,2 +1,3 @@
export const NOTICE_INSTRUCTION_NAMESPACE = 'notice'; export const NOTICE_INSTRUCTION_NAMESPACE = 'notice';
export const COLLECTION_NOTICE_NAME = 'workflowNotice'; export const COLLECTION_NOTICE_NAME = 'workflowNotice';
export const COLLECTION_NAME_APPROVAL_CARBON_COPY = 'approvalCarbonCopy';

View File

@ -278,5 +278,7 @@
"ms": "ms", "ms": "ms",
"Resubmit": "Resubmit", "Resubmit": "Resubmit",
"Are you sure you want to resubmit it?": "Are you sure you want to resubmit it?", "Are you sure you want to resubmit it?": "Are you sure you want to resubmit it?",
"MyLaunch":"MyLaunch" "MyLaunch":"MyLaunch",
"CarbonCopy":"CarbonCopy",
"ApprovalCarbonCopy":"Approval:CarbonCopy"
} }

View File

@ -359,5 +359,7 @@
"ms": "毫秒", "ms": "毫秒",
"Resubmit": "重新发起", "Resubmit": "重新发起",
"Are you sure you want to resubmit it?": "是否确定重新发起?", "Are you sure you want to resubmit it?": "是否确定重新发起?",
"MyLaunch":"我的发起" "MyLaunch":"我的发起",
"CarbonCopy":"抄送",
"ApprovalCarbonCopy":"审批:抄送"
} }

View File

@ -0,0 +1,69 @@
import { UiSchemaRepository } from '@tachybase/plugin-ui-schema-storage';
import { JOB_STATUS } from '../../constants';
import Instruction from '../../instructions';
import { COLLECTION_NAME_APPROVAL_CARBON_COPY } from '../common/constants';
import { parsePerson } from './tools';
export default class ApprovalCarbonCopyInstruction 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 targetPersonList = await parsePerson({ node, processor, keyName: 'carbonCopyPerson' });
if (targetPersonList && targetPersonList.length > 0) {
const { db } = processor.options.plugin;
const ApprovalRepo = db.getRepository('approvals');
const approval = await ApprovalRepo.findOne({
filter: {
'executions.id': processor.execution.id,
},
fields: ['id', 'status', 'data', 'summary', 'collectionName'],
appends: ['approvalExecutions', 'createdBy'],
except: ['data'],
});
const CarbonCopyModel = db.getModel(COLLECTION_NAME_APPROVAL_CARBON_COPY);
const notifiedPersonDataMap = targetPersonList.map((userId, index) => ({
userId,
jobId: job.id,
nodeId: node.id,
executionId: job.executionId,
workflowId: node.workflowId,
index,
// TODO: 怎么做到状态实时更新, 观察下审批发起的表的状态是怎么做的...
createdById: approval.createdBy?.id,
approvalId: approval.id,
status: approval.status,
snapshot: approval.data,
summary: approval.summary,
collectionName: approval.collectionName,
dataKey: approval.dataKey,
}));
await CarbonCopyModel.bulkCreate(notifiedPersonDataMap, {
transaction: processor.transaction,
});
}
return job;
}
async duplicateConfig(node, { transaction }) {
const keyName = 'showCarbonCopyDetail';
const uiSchemaRepo = this.workflow.app.db.getRepository<UiSchemaRepository>('uiSchemas');
if (!node.config[keyName]) {
return node.config;
}
const resultSchema = await uiSchemaRepo.duplicate(node.config[keyName], {
transaction,
});
return {
...node.config,
[keyName]: resultSchema?.['x-uid'],
};
}
}

View File

@ -3,6 +3,7 @@ import { parseCollectionName } from '@tachybase/data-source-manager';
import { traverseJSON } from '@tachybase/database'; import { traverseJSON } from '@tachybase/database';
import { EXECUTION_STATUS, JOB_STATUS, PluginWorkflow } from '../..'; import { EXECUTION_STATUS, JOB_STATUS, PluginWorkflow } from '../..';
import { COLLECTION_NAME_APPROVAL_CARBON_COPY, COLLECTION_WORKFLOWS_NAME } from '../common/constants';
import { APPROVAL_ACTION_STATUS, APPROVAL_STATUS } from './constants'; import { APPROVAL_ACTION_STATUS, APPROVAL_STATUS } from './constants';
import { getSummary } from './tools'; import { getSummary } from './tools';
import { getAssociationName, jsonParse } from './utils'; import { getAssociationName, jsonParse } from './utils';
@ -357,6 +358,23 @@ const approvalRecords = {
workflow.resume(approvalRecord.job); workflow.resume(approvalRecord.job);
}, },
}; };
const approvalCarbonCopy = {
async listCentralized(context, next) {
const centralizedApprovalFlow = await context.db.getRepository(COLLECTION_WORKFLOWS_NAME).find({
filter: {
type: 'approval',
'config.centralized': true,
},
fields: ['id'],
});
context.action.mergeParams({
filter: {
workflowId: centralizedApprovalFlow.map((item) => item.id),
},
});
return actions.list(context, next);
},
};
function make(name, mod) { function make(name, mod) {
return Object.keys(mod).reduce( return Object.keys(mod).reduce(
(result, key) => ({ (result, key) => ({
@ -371,5 +389,6 @@ export function init({ app }) {
...make('workflows', workflows), ...make('workflows', workflows),
...make('approvals', approvals), ...make('approvals', approvals),
...make('approvalRecords', approvalRecords), ...make('approvalRecords', approvalRecords),
...make(COLLECTION_NAME_APPROVAL_CARBON_COPY, approvalCarbonCopy),
}); });
} }

View File

@ -0,0 +1,70 @@
import { defineCollection } from '@tachybase/database';
import { COLLECTION_NAME_APPROVAL_CARBON_COPY } from '../../common/constants';
// 审批-抄送
export default defineCollection({
namespace: 'workflow.approvalCarbonCopy',
dumpRules: 'required',
name: COLLECTION_NAME_APPROVAL_CARBON_COPY,
createdBy: true,
updatedBy: true,
fields: [
{
type: 'string',
name: 'collectionName',
},
{
type: 'string',
name: 'index',
},
{
type: 'string',
name: 'dataKey',
},
{
type: 'belongsTo',
name: 'approval',
},
{
type: 'integer',
name: 'status',
},
{
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',
},
{
type: 'jsonb',
name: 'summary',
defaultValue: {},
},
{
type: 'jsonb',
name: 'snapshot',
defaultValue: {},
},
],
});

View File

@ -1,6 +1,6 @@
import { defineCollection } from '@tachybase/database'; import { defineCollection } from '@tachybase/database';
// 审批 // 审批-执行态
export default defineCollection({ export default defineCollection({
namespace: 'workflow.approvalExecutions', namespace: 'workflow.approvalExecutions',
dumpRules: 'required', dumpRules: 'required',

View File

@ -1,6 +1,6 @@
import { defineCollection } from '@tachybase/database'; import { defineCollection } from '@tachybase/database';
// 审批待办 // 审批-待办
export default defineCollection({ export default defineCollection({
namespace: 'workflow.approvalRecords', namespace: 'workflow.approvalRecords',
dumpRules: 'required', dumpRules: 'required',

View File

@ -1,6 +1,6 @@
import { defineCollection } from '@tachybase/database'; import { defineCollection } from '@tachybase/database';
// 审批发起 // 审批-发起
export default defineCollection({ export default defineCollection({
namespace: 'workflow.approvals', namespace: 'workflow.approvals',
dumpRules: 'required', dumpRules: 'required',

View File

@ -2,7 +2,9 @@ import path from 'path';
import { Plugin } from '@tachybase/server'; import { Plugin } from '@tachybase/server';
import { PluginWorkflow } from '../..'; import { PluginWorkflow } from '../..';
import { COLLECTION_NAME_APPROVAL_CARBON_COPY } from '../common/constants';
import { init } from './actions'; import { init } from './actions';
import ApprovalCarbonCopyInstruction from './ApprovalCarbonCopy.instruction';
import ApprovalInstruction from './ApprovalInstruction'; import ApprovalInstruction from './ApprovalInstruction';
import ApprovalTrigger from './ApprovalTrigger'; import ApprovalTrigger from './ApprovalTrigger';
@ -17,6 +19,7 @@ export class PluginWorkflowApproval extends Plugin {
this.workflow = plugin; this.workflow = plugin;
plugin.triggers.register('approval', new ApprovalTrigger(plugin)); plugin.triggers.register('approval', new ApprovalTrigger(plugin));
plugin.instructions.register('approval', new ApprovalInstruction(plugin)); plugin.instructions.register('approval', new ApprovalInstruction(plugin));
plugin.instructions.register(COLLECTION_NAME_APPROVAL_CARBON_COPY, new ApprovalCarbonCopyInstruction(plugin));
}); });
} }
async load() { async load() {
@ -36,6 +39,9 @@ export class PluginWorkflowApproval extends Plugin {
this.app.acl.allow('approvals', '*', 'loggedIn'); this.app.acl.allow('approvals', '*', 'loggedIn');
this.app.acl.allow('approvalExecutions', ['get'], 'loggedIn'); this.app.acl.allow('approvalExecutions', ['get'], 'loggedIn');
this.app.acl.allow('approvalRecords', ['get', 'list', 'listCentralized', 'submit'], 'loggedIn'); this.app.acl.allow('approvalRecords', ['get', 'list', 'listCentralized', 'submit'], 'loggedIn');
// NOTE: 这种命名尽量改为引用同一个变量的形式,
// 这里因为不妨碍快速获得变量名, 所以没有保持风格一致, 直接使用推荐的方式
this.app.acl.allow(COLLECTION_NAME_APPROVAL_CARBON_COPY, ['get', 'list', 'listCentralized', 'submit'], 'loggedIn');
} }
async install(options) {} async install(options) {}
async afterEnable() {} async afterEnable() {}

View File

@ -19,3 +19,26 @@ export function getSummary(params: ParamsType): object {
return result; return result;
} }
export async function parsePerson({ node, processor, keyName }) {
const configPerson = processor
.getParsedValue(node.config?.[keyName] ?? [], node.id)
.flat()
.filter(Boolean);
const targetPerson = new Set();
const UserRepo = processor.options.plugin.app.db.getRepository('users');
for (const item of configPerson) {
if (typeof item === 'object') {
const users = await UserRepo.find({
...item,
fields: ['id'],
transaction: processor.transaction,
});
users.forEach((userData) => targetPerson.add(userData.id));
} else {
targetPerson.add(item);
}
}
return [...targetPerson];
}

View File

@ -1,2 +1,4 @@
export const NOTICE_INSTRUCTION_NAMESPACE = 'notice'; export const NOTICE_INSTRUCTION_NAMESPACE = 'notice';
export const COLLECTION_NOTICE_NAME = 'workflowNotice'; export const COLLECTION_NOTICE_NAME = 'workflowNotice';
export const COLLECTION_NAME_APPROVAL_CARBON_COPY = 'approvalCarbonCopy';
export const COLLECTION_WORKFLOWS_NAME = 'workflows';