feat: duplicate action (#1973)

* feat: depulicate action

* feat: depulicate action

* refactor: duplicate action

* refactor: duplicate action

* refactor: duplicate action

* refactor:  support quick duplicate

* refactor: local improve

* refactor: local improve

* fix: duplicate intialValue

* refactor: detail action support duplicate

* refactor: action role check

* fix: duplicate support linkage rule

* fix: duplicate action support openMode and openSize

* fix: duplicate action support linkage rule

* refactor: locale

* refactor: detail block support duplicate action

* refactor: code improve

* refactor: code improve

* refactor: duplicate action improve

* refactor:  wait time after click is too long, a Duplicating is required

---------

Co-authored-by: chenos <chenlinxh@gmail.com>
This commit is contained in:
katherinehhh 2023-06-07 11:25:32 +08:00 committed by GitHub
parent 8809df3f51
commit c3c02d8925
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 379 additions and 73 deletions

View File

@ -689,4 +689,9 @@ export default {
"Feedback": "Feedback",
"Try again": "Try again",
"Data template": "Data template",
"Duplicate":"Duplicate",
"Duplicate mode":"Duplicate mode",
"Quick duplicate":"Quick duplicate",
"Duplicate and continue":"Duplicate and continue",
"Please configure the duplicate fields":"Please configure the duplicate fields"
};

View File

@ -600,4 +600,9 @@ export default {
'Display data template selector': 'データテンプレートセレクターを表示',
'Form data templates': 'フォームデータテンプレート',
"Data template": "データテンプレート",
"Duplicate":"レプリケーション",
"Duplicate mode":"コピーモード",
"Quick duplicate":"今すぐコピー",
"Duplicate and continue":"コピーして続行",
"Please configure the duplicate fields":"コピーするフィールドを設定してください"
}

View File

@ -765,4 +765,9 @@ export default {
'The will interrupt service, it may take a few seconds to restart. Are you sure to continue?': '重启将会中断当前服务,这个过程可能需要一点时间,确定要继续吗?',
'Reboot': '重启',
'Clear cache': '清除缓存',
"Duplicate":"复制",
"Duplicate mode":"复制方式",
"Quick duplicate":"快速复制",
"Duplicate and continue":"复制并继续",
"Please configure the duplicate fields":"请配置要复制的字段"
}

View File

@ -1,6 +1,7 @@
import { ISchema, useField, useFieldSchema } from '@formily/react';
import { ISchema, useField, useFieldSchema, connect, mapProps } from '@formily/react';
import { isValid, uid } from '@formily/shared';
import { Menu } from 'antd';
import { Menu, Tree as AntdTree } from 'antd';
import { cloneDeep } from 'lodash';
import React, { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useDesignable } from '../..';
@ -8,9 +9,20 @@ import { useCollection, useCollectionManager } from '../../../collection-manager
import { OpenModeSchemaItems } from '../../../schema-items';
import { GeneralSchemaDesigner, SchemaSettings } from '../../../schema-settings';
import { useLinkageAction } from './hooks';
import { useCollectionState } from '../../../schema-settings/DataTemplates/hooks/useCollectionState';
import { requestSettingsSchema } from './utils';
const Tree = connect(
AntdTree,
mapProps((props, field: any) => {
return {
...props,
onCheck: (checkedKeys) => {
field.value = checkedKeys;
},
};
}),
);
const MenuGroup = (props) => {
const fieldSchema = useFieldSchema();
const actionType = fieldSchema['x-action'] || '';
@ -41,19 +53,22 @@ export const ActionDesigner = (props) => {
const field = useField();
const fieldSchema = useFieldSchema();
const { name } = useCollection();
const { getChildrenCollections } = useCollectionManager();
const { getChildrenCollections, getCollection, getCollectionField } = useCollectionManager();
const { dn } = useDesignable();
const { t } = useTranslation();
const isAction = useLinkageAction();
const isPopupAction = ['create', 'update', 'view', 'customize:popup'].includes(fieldSchema['x-action'] || '');
const isPopupAction = ['create', 'update', 'view', 'customize:popup','duplicate'].includes(fieldSchema['x-action'] || '');
const isUpdateModePopupAction = ['customize:bulkUpdate', 'customize:bulkEdit'].includes(fieldSchema['x-action']);
const [initialSchema, setInitialSchema] = useState<ISchema>();
const actionType = fieldSchema['x-action'] ?? '';
const isLinkageAction = linkageAction || isAction;
const isChildCollectionAction = getChildrenCollections(name).length > 0 && fieldSchema['x-action'] === 'create';
const isLink = fieldSchema['x-component'] === 'Action.Link';
const isDelete = fieldSchema?.parent['x-component'] === 'CollectionField';
const isDraggable=fieldSchema?.parent['x-component'] !== 'CollectionField';
const isDelete = fieldSchema?.parent?.['x-component'] === 'CollectionField';
const isDraggable = fieldSchema?.parent?.['x-component'] !== 'CollectionField';
const isDuplicateAction = fieldSchema['x-action'] === 'duplicate';
const { collectionList, getEnableFieldTree, onLoadData, onCheck } = useCollectionState(name);
const duplicateValues = cloneDeep(fieldSchema['x-component-props'].duplicateFields || []);
useEffect(() => {
const schemaUid = uid();
const schema: ISchema = {
@ -73,6 +88,7 @@ export const ActionDesigner = (props) => {
'After clicking the custom button, the following fields of the current record will be saved according to the following form.',
),
};
return (
<GeneralSchemaDesigner {...restProps} disableInitializer draggable={isDraggable}>
<MenuGroup>
@ -142,6 +158,100 @@ export const ActionDesigner = (props) => {
}}
/>
{isLinkageAction && <SchemaSettings.LinkageRules collectionName={name} />}
{isDuplicateAction && [
<SchemaSettings.ModalItem
title={t('Duplicate mode')}
components={{ Tree }}
scope={{ getEnableFieldTree, collectionName: name }}
schema={
{
type: 'object',
title: t('Duplicate mode'),
properties: {
duplicateMode: {
'x-decorator': 'FormItem',
'x-component': 'Radio.Group',
title: t('Duplicate mode'),
default: fieldSchema['x-component-props']?.duplicateMode || 'quickDulicate',
enum: [
{ value: 'quickDulicate', label: '{{t("Quick duplicate")}}' },
{ value: 'continueduplicate', label: '{{t("Duplicate and continue")}}' },
],
},
collection: {
type: 'string',
title: '{{ t("Collection") }}',
required: true,
description: t('If collection inherits, choose inherited collections as templates'),
default: '{{ collectionName }}',
'x-display': collectionList.length > 1 ? 'visible' : 'hidden',
'x-decorator': 'FormItem',
'x-component': 'Select',
'x-component-props': {
options: collectionList,
},
},
duplicateFields: {
type: 'array',
title: '{{ t("Data fields") }}',
required: true,
default: duplicateValues,
description: t('Only the selected fields will be used as the initialization data for the form'),
'x-decorator': 'FormItem',
'x-component': Tree,
'x-component-props': {
defaultCheckedKeys: duplicateValues,
treeData: [],
checkable: true,
checkStrictly: true,
selectable: false,
loadData: onLoadData,
onCheck,
rootStyle: {
padding: '8px 0',
border: '1px solid #d9d9d9',
borderRadius: '2px',
maxHeight: '30vh',
overflow: 'auto',
margin: '2px 0',
},
},
'x-reactions': [
{
dependencies: ['.collection'],
fulfill: {
state: {
disabled: '{{ !$deps[0] }}',
componentProps: {
treeData: '{{ getEnableFieldTree($deps[0], $self) }}',
},
},
},
},
],
},
},
} as ISchema
}
onSubmit={({ duplicateMode, duplicateFields }) => {
const fields = Array.isArray(duplicateFields) ? duplicateFields : duplicateFields.checked || [];
field.componentProps.duplicateMode = duplicateMode;
field.componentProps.duplicateFields = fields;
fieldSchema['x-component-props'] = fieldSchema['x-component-props'] || {};
fieldSchema['x-component-props'].duplicateMode = duplicateMode;
fieldSchema['x-component-props'].duplicateFields = fields;
dn.emit('patch', {
schema: {
['x-uid']: fieldSchema['x-uid'],
'x-component-props': {
...fieldSchema['x-component-props'],
},
},
});
dn.refresh();
}}
/>,
]}
<OpenModeSchemaItems openMode={isPopupAction} openSize={isPopupAction}></OpenModeSchemaItems>
{isUpdateModePopupAction && (
<SchemaSettings.SelectItem
@ -217,39 +327,6 @@ export const ActionDesigner = (props) => {
}}
/>
)}
{/* {isValid(fieldSchema?.['x-action-settings']?.overwriteValues) && (
<SchemaSettings.ModalItem
title={t('Form values')}
schema={
{
type: 'object',
properties: {
overwriteValues: {
title: t('When submitting the following fields, the saved values are'),
'x-decorator': 'FormItem',
'x-component': 'Input.TextArea',
default: JSON.stringify(fieldSchema?.['x-action-settings']?.overwriteValues),
},
},
} as ISchema
}
onSubmit={({ overwriteValues }) => {
try {
const values = JSON.parse(overwriteValues);
fieldSchema['x-action-settings'].overwriteValues = values;
dn.emit('patch', {
schema: {
['x-uid']: fieldSchema['x-uid'],
'x-action-settings': {
...fieldSchema['x-action-settings'],
},
},
});
dn.refresh();
} catch (e) {}
}}
/>
)} */}
{isValid(fieldSchema?.['x-action-settings']?.['onSuccess']) && (
<SchemaSettings.ModalItem
title={

View File

@ -5,7 +5,7 @@ import { ComposedAction } from './types';
export const ActionLink: ComposedAction = observer(
(props: any) => {
return <Action {...props} component={'a'} className={'nb-action-link'} />;
return <Action {...props} component={props.component || 'a'} className={'nb-action-link'} />;
},
{ displayName: 'ActionLink' },
);

View File

@ -7,6 +7,7 @@ import { useTranslation } from 'react-i18next';
import { useAPIClient } from '../../../api-client';
import { findFormBlock } from '../../../block-provider';
import { useCollectionManager } from '../../../collection-manager';
import { useDuplicatefieldsContext } from '../../../schema-initializer/components';
export interface ITemplate {
config?: {
@ -32,9 +33,12 @@ export interface ITemplate {
const useDataTemplates = () => {
const fieldSchema = useFieldSchema();
const { t } = useTranslation();
const { items = [], display = true } = findDataTemplates(fieldSchema);
const data = useDuplicatefieldsContext();
const { getCollectionJoinField } = useCollectionManager();
if (data) {
return data;
}
const { items = [], display = true } = findDataTemplates(fieldSchema);
// 过滤掉已经被删除的字段
items.forEach((item) => {
try {
@ -147,7 +151,7 @@ function findDataTemplates(fieldSchema): ITemplate {
return {} as ITemplate;
}
async function fetchTemplateData(api, template: { collection: string; dataId: number; fields: string[] }, t) {
export async function fetchTemplateData(api, template: { collection: string; dataId: number; fields: string[] }, t) {
if (template.fields.length === 0) {
return;
}

View File

@ -31,6 +31,19 @@ export const DetailsActionInitializers = {
'x-decorator': 'ACLActionProvider',
},
},
{
type: 'item',
title: '{{t("Duplicate")}}',
component: 'DuplicateActionInitializer',
schema: {
'x-component': 'Action',
'x-action': 'duplicate',
'x-decorator': 'ACLActionProvider',
'x-component-props': {
type: 'primary',
},
},
},
],
},
],

View File

@ -1,5 +1,9 @@
import { useCollection } from '../..';
const useVisibleCollection = () => {
const collection = useCollection();
return collection.template !== 'view';
};
// 表单的操作配置
export const ReadPrettyFormActionInitializers = {
title: '{{t("Configure actions")}}',
@ -23,10 +27,7 @@ export const ReadPrettyFormActionInitializers = {
type: 'primary',
},
},
visible: () => {
const collection = useCollection();
return (collection as any).template !== 'view';
},
visible: useVisibleCollection,
},
{
type: 'item',
@ -36,10 +37,21 @@ export const ReadPrettyFormActionInitializers = {
'x-component': 'Action',
'x-decorator': 'ACLActionProvider',
},
visible: () => {
const collection = useCollection();
return (collection as any).template !== 'view';
visible: useVisibleCollection,
},
{
type: 'item',
title: '{{t("Duplicate")}}',
component: 'DuplicateActionInitializer',
schema: {
'x-component': 'Action',
'x-action': 'duplicate',
'x-decorator': 'ACLActionProvider',
'x-component-props': {
type: 'primary',
},
},
visible: useVisibleCollection,
},
{
type: 'item',
@ -132,10 +144,7 @@ export const ReadPrettyFormActionInitializers = {
useProps: '{{ useCustomizeUpdateActionProps }}',
},
},
visible: () => {
const collection = useCollection();
return (collection as any).template !== 'view';
},
visible: useVisibleCollection,
},
{
type: 'item',
@ -159,10 +168,7 @@ export const ReadPrettyFormActionInitializers = {
useProps: '{{ useCustomizeRequestActionProps }}',
},
},
visible: () => {
const collection = useCollection();
return (collection as any).template !== 'view';
},
visible: useVisibleCollection,
},
],
},

View File

@ -102,10 +102,10 @@ export const TableActionColumnInitializers = (props: any) => {
'x-decorator': 'ACLActionProvider',
},
visible: () => {
const collection = useCollection();
return (collection as any).template !== 'view';
},
},
modifyFlag && {
type: 'item',
title: t('Delete'),
@ -127,6 +127,19 @@ export const TableActionColumnInitializers = (props: any) => {
'x-decorator': 'ACLActionProvider',
},
},
{
type: 'item',
title: t('Duplicate'),
component: 'DuplicateActionInitializer',
schema: {
'x-component': 'Action.Link',
'x-action': 'duplicate',
'x-decorator': 'ACLActionProvider',
},
visible: () => {
return (collection as any).template !== 'view';
},
},
],
},
{
@ -210,7 +223,6 @@ export const TableActionColumnInitializers = (props: any) => {
},
},
visible: () => {
const collection = useCollection();
return (collection as any).template !== 'view';
},
},
@ -236,7 +248,6 @@ export const TableActionColumnInitializers = (props: any) => {
},
},
visible: () => {
const collection = useCollection();
return (collection as any).template !== 'view';
},
},

View File

@ -0,0 +1,121 @@
import React, { createContext, useContext, useState } from 'react';
import { observer, RecursionField, useField, useFieldSchema } from '@formily/react';
import { RecordProvider, ActionContext, useActionContext, useRecord, useCollection } from '../../';
import { useTranslation } from 'react-i18next';
import { css, cx } from '@emotion/css';
import { useAPIClient, useBlockRequestContext, useDesignable } from '../../';
import { actionDesignerCss } from './CreateRecordAction';
import { fetchTemplateData } from '../../schema-component/antd/form-v2/Templates';
import { Button, message } from 'antd';
const DuplicatefieldsContext = createContext(null);
export const useDuplicatefieldsContext = () => {
return useContext(DuplicatefieldsContext);
};
export const DuplicateAction = observer((props: any) => {
const { children } = props;
const field = useField();
const fieldSchema = useFieldSchema();
const api = useAPIClient();
const disabled: boolean = field.disabled || props.disabled;
const { designable } = useDesignable();
const [visible, setVisible] = useState(false);
const [loading, setLoading] = useState(false);
const { resource, service, __parent, block } = useBlockRequestContext();
const { duplicateFields, duplicateMode = 'quickDulicate' } = fieldSchema['x-component-props'];
const { id } = useRecord();
const ctx = useActionContext();
const { name } = useCollection();
const { t } = useTranslation();
const template = { key: 'duplicate', dataId: id, default: true, fields: duplicateFields || [], collection: name };
const isLinkBtn = fieldSchema['x-component'] === 'Action.Link';
const handelQuickDuplicate = async () => {
setLoading(true);
try {
const data = await fetchTemplateData(api, template, t);
await resource.create({
values: {
...data,
},
});
message.success(t('Saved successfully'));
if (block === 'form') {
__parent?.service?.refresh?.();
} else {
await service?.refresh?.();
}
setLoading(false);
} catch (error) {
setLoading(false);
console.error(error); // Handle or log the error appropriately
}
};
const handelDuplicate = () => {
if (!disabled && !loading) {
if (duplicateFields?.length > 0) {
if (duplicateMode === 'quickDulicate') {
handelQuickDuplicate();
} else {
setVisible(true);
}
} else {
message.error(t('Please configure the duplicate fields'));
}
}
};
return (
<div
className={cx(actionDesignerCss, {
[css`
.general-schema-designer {
top: -10px;
bottom: -10px;
left: -10px;
right: -10px;
}
`]: isLinkBtn,
})}
>
<DuplicatefieldsContext.Provider
value={{
display: false,
enabled: true,
defaultTemplate: template,
}}
>
<div>
{isLinkBtn ? (
<a
//@ts-ignore
disabled={disabled}
style={{
opacity: designable && field?.data?.hidden && 0.1,
cursor: loading ? 'not-allowed' : 'pointer',
}}
onClick={handelDuplicate}
>
{loading ? t('Duplicating') : children || t('Duplicate')}
</a>
) : (
<Button
disabled={disabled}
style={{
opacity: designable && field?.data?.hidden && 0.1,
}}
{...props}
onClick={handelDuplicate}
>
{loading ? t('Duplicating') : children || t('Duplicate')}
</Button>
)}
<ActionContext.Provider value={{ ...ctx, visible, setVisible }}>
<RecursionField schema={fieldSchema} basePath={field.address} onlyRenderProperties />
</ActionContext.Provider>
</div>
</DuplicatefieldsContext.Provider>
</div>
);
});

View File

@ -1,3 +1,4 @@
export * from './assigned-field';
export * from './BulkEditField';
export * from './CreateRecordAction';
export * from './DuplicateAction'

View File

@ -0,0 +1,54 @@
import React from 'react';
import { ActionInitializer } from './ActionInitializer';
export const DuplicateActionInitializer = (props) => {
const schema = {
type: 'void',
'x-action': 'duplicate',
'x-acl-action':'create',
title: '{{ t("Duplicate") }}',
'x-designer': 'Action.Designer',
'x-component': 'Action.Link',
'x-decorator': 'ACLActionProvider',
'x-component-props': {
openMode: 'drawer',
component: 'DuplicateAction',
},
properties: {
drawer: {
type: 'void',
title: '{{ t("Duplicate") }}',
'x-component': 'Action.Container',
'x-component-props': {
className: 'nb-action-popup',
},
properties: {
tabs: {
type: 'void',
'x-component': 'Tabs',
'x-component-props': {},
'x-initializer': 'TabPaneInitializers',
properties: {
tab1: {
type: 'void',
title: '{{t("Duplicate")}}',
'x-component': 'Tabs.TabPane',
'x-designer': 'Tabs.Designer',
'x-component-props': {},
properties: {
grid: {
type: 'void',
'x-component': 'Grid',
'x-initializer': 'CreateFormBlockInitializers',
properties: {},
},
},
},
},
},
},
},
},
};
return <ActionInitializer {...props} schema={schema} />;
};

View File

@ -51,4 +51,5 @@ export * from './TableCollectionFieldInitializer';
export * from './TableSelectorInitializer';
export * from './UpdateActionInitializer';
export * from './UpdateSubmitActionInitializer';
export * from './DuplicateActionInitializer';
export * from './ViewActionInitializer';

View File

@ -29,15 +29,16 @@ export const OpenModeSchemaItems: React.FC<Options> = (options) => {
value={openModeValue}
onChange={(value) => {
field.componentProps.openMode = value;
fieldSchema['x-component-props'] = field.componentProps;
const schema = {
'x-uid': fieldSchema['x-uid'],
};
schema['x-component-props'] = fieldSchema['x-component-props'] || {};
schema['x-component-props'].openMode = value;
fieldSchema['x-component-props'].openMode = value;
// when openMode change, set openSize value to default
Reflect.deleteProperty(fieldSchema['x-component-props'], 'openSize');
dn.emit('patch', {
schema: {
'x-uid': fieldSchema['x-uid'],
'x-component-props': fieldSchema['x-component-props'],
},
schema: schema,
});
dn.refresh();
}}
@ -60,12 +61,14 @@ export const OpenModeSchemaItems: React.FC<Options> = (options) => {
}
onChange={(value) => {
field.componentProps.openSize = value;
fieldSchema['x-component-props'] = field.componentProps;
dn.emit('patch', {
schema: {
const schema = {
'x-uid': fieldSchema['x-uid'],
'x-component-props': fieldSchema['x-component-props'],
},
};
schema['x-component-props'] = fieldSchema['x-component-props'] || {};
schema['x-component-props'].openSize = value;
fieldSchema['x-component-props'].openSize = value;
dn.emit('patch', {
schema: schema,
});
dn.refresh();
}}