feat: refactor resubmit (#1290)

Co-authored-by: sealday <sealday@gmail.com>
Reviewed-on: https://git.daoyoucloud.com:8443/daoyoucloud/tachybase/pulls/1290
This commit is contained in:
sealday 2024-07-09 03:53:01 +08:00
parent 0b958cf528
commit 1d5306d427
17 changed files with 268 additions and 304 deletions

View File

@ -38,3 +38,5 @@ export default class DatabaseUtils {
return this.db.options.schema || 'public';
}
}
export * from './traverseJSON';

View File

@ -0,0 +1,112 @@
import { Collection } from '@tachybase/database';
type TraverseOptions = {
collection: Collection;
exclude?: string[];
include?: string[];
through?: string;
excludePk?: boolean;
};
const traverseHasMany = (arr: any[], { collection, exclude = [], include = [] }: TraverseOptions) => {
if (!arr) {
return arr;
}
return arr.map((item) => traverseJSON(item, { collection, exclude, include }));
};
const traverseBelongsToMany = (arr: any[], { collection, exclude = [], through }: TraverseOptions) => {
if (!arr) {
return arr;
}
const throughCollection = collection.db.getCollection(through);
return arr.map((item) => {
const data = traverseJSON(item[through], { collection: throughCollection, exclude });
if (data && Object.keys(data).length) {
item[through] = data;
} else {
delete item[through];
}
return traverseJSON(item, {
collection,
excludePk: false,
});
});
};
const parseInclude = (keys: string[]) => {
const map = {};
for (const key of keys) {
const args = key.split('.');
const field = args.shift();
map[field] = map[field] || [];
if (args.length) {
map[field].push(args.join('.'));
}
}
return map;
};
export const traverseJSON = (data, options: TraverseOptions) => {
if (!data) {
return data;
}
const { collection, exclude = [], include = [], excludePk = true } = options;
const map = parseInclude(include);
const result = {};
for (const key of Object.keys(data || {})) {
const subInclude = map[key];
if (include.length > 0 && !subInclude) {
continue;
}
if (exclude.includes(key)) {
continue;
}
if (['createdAt', 'updatedAt', 'createdBy', 'createdById', 'updatedById', 'updatedBy'].includes(key)) {
continue;
}
const field = collection.getField(key);
if (!field) {
result[key] = data[key];
continue;
}
if (field.options.primaryKey && excludePk) {
continue;
}
if (field.options.isForeignKey) {
continue;
}
if (['sort', 'password', 'sequence'].includes(field.type)) {
continue;
}
if (field.type === 'hasOne') {
result[key] = traverseJSON(data[key], {
collection: collection.db.getCollection(field.target),
exclude: [field.foreignKey],
include: subInclude,
});
} else if (field.type === 'hasMany') {
result[key] = traverseHasMany(data[key], {
collection: collection.db.getCollection(field.target),
exclude: [field.foreignKey],
include: subInclude,
});
} else if (field.type === 'belongsTo') {
result[key] = traverseJSON(data[key], {
collection: collection.db.getCollection(field.target),
// exclude: [field.foreignKey],
include: subInclude,
excludePk: false,
});
} else if (field.type === 'belongsToMany') {
result[key] = traverseBelongsToMany(data[key], {
collection: collection.db.getCollection(field.target),
exclude: [field.foreignKey, field.otherKey],
through: field.through,
});
} else {
result[key] = data[key];
}
}
return result;
};

View File

@ -38,6 +38,7 @@ export * from './relation-repository/single-relation-repository';
export * from './repository';
export * from './update-associations';
export { snakeCase } from './utils';
export * from './database-utils';
export * from './value-parsers';
export * from './view-collection';
export * from './view/view-inference';

View File

@ -1,5 +1,5 @@
import { Context } from '@tachybase/actions';
import { Collection } from '@tachybase/database';
import { traverseJSON } from '@tachybase/database';
export const dateTemplate = async (ctx: Context, next) => {
const { resourceName, actionName } = ctx.action;
@ -14,114 +14,3 @@ export const dateTemplate = async (ctx: Context, next) => {
});
}
};
type TraverseOptions = {
collection: Collection;
exclude?: string[];
include?: string[];
through?: string;
excludePk?: boolean;
};
const traverseHasMany = (arr: any[], { collection, exclude = [], include = [] }: TraverseOptions) => {
if (!arr) {
return arr;
}
return arr.map((item) => traverseJSON(item, { collection, exclude, include }));
};
const traverseBelongsToMany = (arr: any[], { collection, exclude = [], through }: TraverseOptions) => {
if (!arr) {
return arr;
}
const throughCollection = collection.db.getCollection(through);
return arr.map((item) => {
const data = traverseJSON(item[through], { collection: throughCollection, exclude });
if (data && Object.keys(data).length) {
item[through] = data;
} else {
delete item[through];
}
return traverseJSON(item, {
collection,
excludePk: false,
});
});
};
const parseInclude = (keys: string[]) => {
const map = {};
for (const key of keys) {
const args = key.split('.');
const field = args.shift();
map[field] = map[field] || [];
if (args.length) {
map[field].push(args.join('.'));
}
}
return map;
};
const traverseJSON = (data, options: TraverseOptions) => {
if (!data) {
return data;
}
const { collection, exclude = [], include = [], excludePk = true } = options;
const map = parseInclude(include);
const result = {};
for (const key of Object.keys(data || {})) {
const subInclude = map[key];
if (include.length > 0 && !subInclude) {
continue;
}
if (exclude.includes(key)) {
continue;
}
if (['createdAt', 'updatedAt', 'createdBy', 'createdById', 'updatedById', 'updatedBy'].includes(key)) {
continue;
}
const field = collection.getField(key);
if (!field) {
result[key] = data[key];
continue;
}
if (field.options.primaryKey && excludePk) {
continue;
}
if (field.options.isForeignKey) {
continue;
}
if (['sort', 'password', 'sequence'].includes(field.type)) {
continue;
}
if (field.type === 'hasOne') {
result[key] = traverseJSON(data[key], {
collection: collection.db.getCollection(field.target),
exclude: [field.foreignKey],
include: subInclude,
});
} else if (field.type === 'hasMany') {
result[key] = traverseHasMany(data[key], {
collection: collection.db.getCollection(field.target),
exclude: [field.foreignKey],
include: subInclude,
});
} else if (field.type === 'belongsTo') {
result[key] = traverseJSON(data[key], {
collection: collection.db.getCollection(field.target),
// exclude: [field.foreignKey],
include: subInclude,
excludePk: false,
});
} else if (field.type === 'belongsToMany') {
result[key] = traverseBelongsToMany(data[key], {
collection: collection.db.getCollection(field.target),
exclude: [field.foreignKey, field.otherKey],
through: field.through,
});
} else {
result[key] = data[key];
}
}
return result;
};

View File

@ -9,8 +9,6 @@ import {
} from '@tachybase/client';
import { uid } from '@tachybase/utils/client';
import { Spin } from 'antd';
import { useFlowContext } from '../../../../../FlowContext';
// 发起人操作界面-创建区块
@ -19,30 +17,26 @@ export const SchemaAddBlock = ({ value, onChange }) => {
const { workflow } = useFlowContext();
const { components } = useContext(SchemaComponentContext);
// TODO:
// 获取对应数据表的表单Schema
const { data, loading } = useRequest(() =>
E(this, null, function* () {
let m;
if (value) {
const { data: h } = yield api.request({ url: `uiSchemas:getJsonSchema/${value}` });
if (((m = h.data) == null ? void 0 : m['x-uid']) === value) return h.data;
}
const l = uid(),
d = {
type: 'void',
name: l,
'x-uid': l,
'x-component': 'Grid',
'x-initializer': 'ApprovalApplyAddBlockButton',
properties: {},
};
return yield api.resource('uiSchemas').insert({ values: d }), onChange(l), d;
}),
);
const { data, loading } = useRequest(async () => {
if (value) {
const { data } = await api.request({ url: `uiSchemas:getJsonSchema/${value}` });
if (data.data?.['x-uid'] === value) return data.data;
}
const name = uid();
const values = {
type: 'void',
name: name,
'x-uid': name,
'x-component': 'Grid',
'x-initializer': 'ApprovalApplyAddBlockButton',
properties: {},
};
return await api.resource('uiSchemas').insert({ values }), onChange(name), values;
});
if (loading) {
return <Spin />;
return;
}
return (
@ -61,7 +55,6 @@ export const SchemaAddBlock = ({ value, onChange }) => {
useFormBlockProps,
useActionResubmit,
}}
// @ts-ignore ugly
schema={data}
/>
</SchemaComponentProvider>
@ -89,24 +82,3 @@ function useActionResubmit() {
function ProviderActionResubmit(props) {
return props.children;
}
// TODO:
const E = (f, o, p) =>
// eslint-disable-next-line promise/param-names
new Promise((r, G) => {
const u = ($) => {
try {
A(p.next($));
} catch (U) {
G(U);
}
},
y = ($) => {
try {
A(p.throw($));
} catch (U) {
G(U);
}
},
A = ($) => ($.done ? r($.value) : Promise.resolve($.value).then(u, y));
A((p = p.apply(f, o)).next());
});

View File

@ -1,19 +1,17 @@
import { useCurrentUserContext } from '@tachybase/client';
import { useFlowContext } from '../../../../../FlowContext';
import { APPROVAL_STATUS } from '../../../constants';
import { useApproval } from '../../approval-common/ApprovalData.provider';
import { useResubmit } from '../../approval-common/Resubmit.provider';
export function ProviderActionResubmit(props) {
const { data } = useCurrentUserContext();
const { isResubmit } = useResubmit();
const { status, createdById } = useApproval();
const { workflow } = useFlowContext();
const isSameId = data.data.id === createdById;
// const isEnabledWithdraw = workflow.enabled && workflow.config.withdrawable;
// const isNotDraft = status !== APPROVAL_STATUS.DRAFT;
if (isSameId) {
const isDraft = status === APPROVAL_STATUS.DRAFT;
if (isSameId && !isResubmit && !isDraft) {
return props.children;
}

View File

@ -11,14 +11,8 @@ export function ActionBarProvider(props) {
const isSameId = data.data.id === createdById;
const isSameExcutionId = latestExecutionId === approvalExecution.id;
const isExcutionDid = [
APPROVAL_STATUS.DRAFT,
APPROVAL_STATUS.RETURNED,
APPROVAL_STATUS.SUBMITTED,
APPROVAL_STATUS.RESUBMIT,
].includes(status);
if (!isSameId || !isSameExcutionId || !isExcutionDid) {
if (!isSameId || !isSameExcutionId) {
return null;
}

View File

@ -4,6 +4,7 @@ import { useCurrentUserContext } from '@tachybase/client';
import { useFlowContext } from '../../../../../FlowContext';
import { APPROVAL_STATUS } from '../../../constants';
import { useApproval } from '../../approval-common/ApprovalData.provider';
import { useResubmit } from '../../approval-common/Resubmit.provider';
const ContextApprovalStatus = createContext(APPROVAL_STATUS.SUBMITTED);
@ -16,11 +17,16 @@ export function ApplyActionStatusProvider(props) {
const { status, createdById } = useApproval();
const { workflow } = useFlowContext();
const { data } = useCurrentUserContext();
const { isResubmit } = useResubmit();
const isSameId = data.data.id === createdById;
const isEnbled = workflow.enabled;
const isEnabled = workflow.enabled;
const isStatusDid = [APPROVAL_STATUS.RESUBMIT, APPROVAL_STATUS.DRAFT, APPROVAL_STATUS.RETURNED].includes(status);
if (isSameId && isEnbled && isStatusDid) {
if (value === APPROVAL_STATUS.DRAFT && status === APPROVAL_STATUS.DRAFT) {
return null;
}
if ((isSameId && isEnabled && isStatusDid) || (isSameId && isEnabled && isResubmit)) {
return <ContextApprovalStatus.Provider value={value}>{children}</ContextApprovalStatus.Provider>;
}

View File

@ -3,17 +3,19 @@ import { useCurrentUserContext } from '@tachybase/client';
import { useFlowContext } from '../../../../../FlowContext';
import { APPROVAL_STATUS } from '../../../constants';
import { useApproval } from '../../approval-common/ApprovalData.provider';
import { useResubmit } from '../../approval-common/Resubmit.provider';
export function WithdrawActionProvider({ children }) {
const { data } = useCurrentUserContext();
const { status, createdById } = useApproval();
const { workflow } = useFlowContext();
const { isResubmit } = useResubmit();
const isSameId = data.data.id === createdById;
const isEnabledWithdraw = workflow.enabled && workflow.config.withdrawable;
const isStatusSubmitted = APPROVAL_STATUS.SUBMITTED === status;
if (isSameId && isEnabledWithdraw && isStatusSubmitted) {
if (isSameId && isEnabledWithdraw && isStatusSubmitted && !isResubmit) {
return children;
}

View File

@ -16,6 +16,7 @@ import { FormBlockProvider } from '../../../common/FormBlock.provider';
import { APPROVAL_STATUS } from '../../../constants';
import { NAMESPACE } from '../../../locale';
import { ApprovalContext } from '../../approval-common/ApprovalData.provider';
import { ResubmitProvider } from '../../approval-common/Resubmit.provider';
import { ContextWithActionEnabled } from '../../approval-common/WithActionEnabled.provider';
import { ContextApprovalExecution } from '../common/ApprovalExecution.provider';
import { FlowContextProvider } from '../common/FlowContext.provider';
@ -85,77 +86,79 @@ export const ViewActionLaunchContent = () => {
>
<ApprovalContext.Provider value={approval}>
<ContextApprovalExecution.Provider value={approvalValue}>
<SchemaComponent
components={{
SchemaComponentProvider: SchemaComponentProvider,
RemoteSchemaComponent: RemoteSchemaComponent,
SchemaComponentContextProvider,
FormBlockProvider,
ActionBarProvider,
ApplyActionStatusProvider,
WithdrawActionProvider,
DetailsBlockProvider,
ProviderActionResubmit,
}}
scope={{
useForm,
useSubmit: useSubmit,
useFormBlockProps,
useDetailsBlockProps: useFormBlockContext,
useWithdrawAction,
useDestroyAction,
useActionResubmit,
}}
schema={{
name: `view-${approval == null ? void 0 : approval.id}`,
type: 'void',
properties: {
tabs: {
type: 'void',
'x-component': 'Tabs',
properties: Object.assign(
{
detail: {
type: 'void',
title: `{{t('Application content', { ns: '${NAMESPACE}' })}}`,
'x-component': 'Tabs.TabPane',
properties: {
detail: {
type: 'void',
'x-decorator': 'SchemaComponentContextProvider',
'x-decorator-props': {
designable: false,
},
'x-component': 'RemoteSchemaComponent',
'x-component-props': {
uid: workflow?.config.applyForm,
noForm: true,
},
},
},
},
},
needHideProcess
? {}
: {
process: {
type: 'void',
title: `{{t('Approval process', { ns: '${NAMESPACE}' })}}`,
'x-component': 'Tabs.TabPane',
properties: {
process: {
type: 'void',
'x-decorator': 'CardItem',
'x-component': 'ApprovalCommon.ViewComponent.ApprovalProcess',
<ResubmitProvider>
<SchemaComponent
components={{
SchemaComponentProvider,
RemoteSchemaComponent,
SchemaComponentContextProvider,
FormBlockProvider,
ActionBarProvider,
ApplyActionStatusProvider,
WithdrawActionProvider,
DetailsBlockProvider,
ProviderActionResubmit,
}}
scope={{
useForm,
useSubmit,
useFormBlockProps,
useDetailsBlockProps: useFormBlockContext,
useWithdrawAction,
useDestroyAction,
useActionResubmit,
}}
schema={{
name: `view-${approval == null ? void 0 : approval.id}`,
type: 'void',
properties: {
tabs: {
type: 'void',
'x-component': 'Tabs',
properties: Object.assign(
{
detail: {
type: 'void',
title: `{{t('Application content', { ns: '${NAMESPACE}' })}}`,
'x-component': 'Tabs.TabPane',
properties: {
detail: {
type: 'void',
'x-decorator': 'SchemaComponentContextProvider',
'x-decorator-props': {
designable: false,
},
'x-component': 'RemoteSchemaComponent',
'x-component-props': {
uid: workflow?.config.applyForm,
noForm: true,
},
},
},
},
),
},
needHideProcess
? {}
: {
process: {
type: 'void',
title: `{{t('Approval process', { ns: '${NAMESPACE}' })}}`,
'x-component': 'Tabs.TabPane',
properties: {
process: {
type: 'void',
'x-decorator': 'CardItem',
'x-component': 'ApprovalCommon.ViewComponent.ApprovalProcess',
},
},
},
},
),
},
},
},
}}
/>
}}
/>
</ResubmitProvider>
</ContextApprovalExecution.Provider>
</ApprovalContext.Provider>
</FlowContextProvider>

View File

@ -16,7 +16,7 @@ import { Button, Dropdown } from 'antd';
import { useTranslation } from '../../../../locale';
import { FlowContextProvider } from '../../common/FlowContext.provider';
import { useActionResubmit } from '../hooks/useActionResubmit';
import { useSubmit } from './hooks/useSubmit';
import { useCreateSubmit } from './hooks/useSubmit';
import { useWithdrawAction } from './hooks/useWithdrawAction';
import { ActionBarProvider } from './Pd.ActionBar';
import { ApplyActionStatusProvider } from './Pd.ActionStatus';
@ -108,15 +108,16 @@ export const ApplyButton = () => {
<SchemaComponent
schema={schema}
components={{
RemoteSchemaComponent: RemoteSchemaComponent,
CollectionProvider_deprecated: CollectionProvider_deprecated,
FlowContextProvider: FlowContextProvider,
ApplyActionStatusProvider: ApplyActionStatusProvider,
RemoteSchemaComponent,
CollectionProvider_deprecated,
FlowContextProvider,
ApplyActionStatusProvider,
ActionBarProvider,
WithdrawActionProvider: WithdrawActionProvider,
ProviderActionResubmit: () => null,
WithdrawActionProvider,
}}
scope={{
useSubmit: useSubmit,
useSubmit: useCreateSubmit,
useWithdrawAction,
useActionResubmit,
}}

View File

@ -7,21 +7,23 @@ import {
} from '@tachybase/client';
import { useField, useForm } from '@tachybase/schema';
import _ from 'lodash';
import { useFlowContext } from '../../../../../../../FlowContext';
import { useContextApprovalStatus } from '../Pd.ActionStatus';
export function useSubmit() {
export function useCreateSubmit() {
const from = useForm();
const field = useField();
const { setVisible } = useActionContext();
const { __parent } = useBlockRequestContext();
const collection = useCollection_deprecated();
const contextWe = useContextApprovalStatus();
const status = useContextApprovalStatus();
const apiClient = useAPIClient();
const { workflow } = useFlowContext();
return {
async run() {
async run({ approvalStatus }) {
try {
from.submit();
field.data = field.data || {};
@ -30,8 +32,8 @@ export function useSubmit() {
await apiClient.resource('approvals').create({
values: {
collectionName: joinCollectionName(collection.dataSource, collection.name),
data: from.values,
status: contextWe,
data: _.omit(from.values, [collection.getPrimaryKey()]),
status: typeof approvalStatus !== 'undefined' ? approvalStatus : status,
workflowId: workflow.id,
},
});
@ -43,7 +45,7 @@ export function useSubmit() {
if (service) {
service.refresh();
}
} catch (h) {
} catch (error) {
field.data && (field.data.loading = false);
}
},

View File

@ -1,59 +1,14 @@
import { useAPIClient } from '@tachybase/client';
import { useFlowContext } from '@tachybase/plugin-workflow/client';
import { useField } from '@tachybase/schema';
import _ from 'lodash';
import { APPROVAL_STATUS } from '../../../../constants';
import { useApproval } from '../../../approval-common/ApprovalData.provider';
import { useHandleRefresh } from '../../common/useHandleRefresh';
import { useResubmit } from '../../../approval-common/Resubmit.provider';
// 重新发起
export function useActionResubmit() {
const { refreshTable } = useHandleRefresh();
const field = useField();
const approval = useApproval();
const api = useAPIClient();
const { workflow } = useFlowContext();
const { setResubmit } = useResubmit();
return {
async run() {
try {
field.data = field.data ?? {};
field.data.loading = true;
const appendsConfigs = [...workflow.config.appends];
const summaryConfigs = [...workflow.config.summary];
const appends = _.intersection(appendsConfigs, summaryConfigs);
const { data: approvalData } = await api.resource('approvals').get({
filterByTk: approval.id,
});
const { collectionName, data, workflowId } = approvalData.data;
const newData = _.omit(data, ['id', 'createdAt', 'updatedAt', 'createdBy', 'updatedBy']);
await api.resource('approvals').reSubmit({
values: {
collectionName: collectionName,
data: newData,
status: APPROVAL_STATUS.RESUBMIT,
workflowId: workflowId,
collectionAppends: appends,
},
});
field.data.loading = false;
refreshTable();
} catch (v) {
if (field.data) {
field.data.loading = false;
}
}
setResubmit(true);
},
};
}

View File

@ -5,6 +5,7 @@ import { useForm } from '@tachybase/schema';
import { useFlowContext } from '../../../../../../FlowContext';
import { ApprovalStatusEnumDict } from '../../../../constants';
import { useApproval } from '../../../approval-common/ApprovalData.provider';
import { useResubmit } from '../../../approval-common/Resubmit.provider';
import { useContextApprovalExecution } from '../../common/ApprovalExecution.provider';
export function useFormBlockProps() {
@ -13,11 +14,12 @@ export function useFormBlockProps() {
const { workflow } = useFlowContext();
const form = useForm();
const { data } = useCurrentUserContext();
const { isResubmit } = useResubmit();
const { editable } = ApprovalStatusEnumDict[approval.status];
const needEditable =
editable &&
(isResubmit || editable) &&
approval?.latestExecutionId === approvalExecution.id &&
approval?.createdById === data?.data.id &&
workflow.enabled;

View File

@ -5,12 +5,17 @@ import _ from 'lodash';
import { useFlowContext } from '../../../../../../FlowContext';
import { useApproval } from '../../../approval-common/ApprovalData.provider';
import { useResubmit } from '../../../approval-common/Resubmit.provider';
import { useHandleRefresh } from '../../common/useHandleRefresh';
import { useCreateSubmit } from '../apply-button/hooks/useSubmit';
import { useContextApprovalStatus } from '../Pd.ApplyActionStatus';
export function useSubmit() {
const { refreshTable } = useHandleRefresh();
const apiClient = useAPIClient();
const { isResubmit } = useResubmit();
const { run: create } = useCreateSubmit();
const status = useContextApprovalStatus();
const form = useForm();
const field = useField();
@ -20,7 +25,10 @@ export function useSubmit() {
const contextApprovalStatus = useContextApprovalStatus();
return {
async run() {
async run(props) {
if (isResubmit) {
return await create({ approvalStatus: status });
}
try {
form.submit();
_.set(field, ['data', 'loading'], true);

View File

@ -0,0 +1,17 @@
import React, { useContext, useState } from 'react';
interface ResubmitProps {
isResubmit?: boolean;
setResubmit?: any;
}
const ResubmitContext = React.createContext<ResubmitProps>({});
export const ResubmitProvider = ({ children }) => {
const [isResubmit, setResubmit] = useState(false);
return <ResubmitContext.Provider value={{ isResubmit, setResubmit }}>{children}</ResubmitContext.Provider>;
};
export const useResubmit = () => {
return useContext(ResubmitContext);
};

View File

@ -1,8 +1,8 @@
import actions, { utils } from '@tachybase/actions';
import { parseCollectionName } from '@tachybase/data-source-manager';
import { traverseJSON } from '@tachybase/database';
import { EXECUTION_STATUS, JOB_STATUS, PluginWorkflow } from '../..';
import { appends } from '../../../client/schemas/collection';
import { APPROVAL_ACTION_STATUS, APPROVAL_STATUS } from './constants';
import { getSummary } from './tools';
import { getAssociationName, jsonParse } from './utils';
@ -47,7 +47,7 @@ const approvals = {
const { repository, model } = collection;
const values = await repository.create({
values: {
...data,
...traverseJSON(data, { collection }),
createdBy: context.state.currentUser.id,
updatedBy: context.state.currentUser.id,
},