diff --git a/packages/core/client/src/locale/en_US.ts b/packages/core/client/src/locale/en_US.ts index 1b0b25e62..47cb600b6 100644 --- a/packages/core/client/src/locale/en_US.ts +++ b/packages/core/client/src/locale/en_US.ts @@ -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" }; diff --git a/packages/core/client/src/locale/ja_JP.ts b/packages/core/client/src/locale/ja_JP.ts index b6d57c72a..b72b6334c 100644 --- a/packages/core/client/src/locale/ja_JP.ts +++ b/packages/core/client/src/locale/ja_JP.ts @@ -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":"コピーするフィールドを設定してください" } diff --git a/packages/core/client/src/locale/zh_CN.ts b/packages/core/client/src/locale/zh_CN.ts index 3a91dd0a9..417ff59c3 100644 --- a/packages/core/client/src/locale/zh_CN.ts +++ b/packages/core/client/src/locale/zh_CN.ts @@ -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":"请配置要复制的字段" } diff --git a/packages/core/client/src/schema-component/antd/action/Action.Designer.tsx b/packages/core/client/src/schema-component/antd/action/Action.Designer.tsx index a2b69eb03..0c179b8d3 100644 --- a/packages/core/client/src/schema-component/antd/action/Action.Designer.tsx +++ b/packages/core/client/src/schema-component/antd/action/Action.Designer.tsx @@ -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(); 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 ( @@ -142,6 +158,100 @@ export const ActionDesigner = (props) => { }} /> {isLinkageAction && } + {isDuplicateAction && [ + 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(); + }} + />, + ]} {isUpdateModePopupAction && ( { }} /> )} - {/* {isValid(fieldSchema?.['x-action-settings']?.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']) && ( { - return ; + return ; }, { displayName: 'ActionLink' }, ); diff --git a/packages/core/client/src/schema-component/antd/form-v2/Templates.tsx b/packages/core/client/src/schema-component/antd/form-v2/Templates.tsx index 9aca6f86a..4e218e63b 100644 --- a/packages/core/client/src/schema-component/antd/form-v2/Templates.tsx +++ b/packages/core/client/src/schema-component/antd/form-v2/Templates.tsx @@ -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; } diff --git a/packages/core/client/src/schema-initializer/buttons/DetailsActionInitializers.tsx b/packages/core/client/src/schema-initializer/buttons/DetailsActionInitializers.tsx index 97de05340..7aea1f400 100644 --- a/packages/core/client/src/schema-initializer/buttons/DetailsActionInitializers.tsx +++ b/packages/core/client/src/schema-initializer/buttons/DetailsActionInitializers.tsx @@ -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', + }, + }, + }, ], }, ], diff --git a/packages/core/client/src/schema-initializer/buttons/ReadPrettyFormActionInitializers.tsx b/packages/core/client/src/schema-initializer/buttons/ReadPrettyFormActionInitializers.tsx index 2cca7b01e..319d6ba5e 100644 --- a/packages/core/client/src/schema-initializer/buttons/ReadPrettyFormActionInitializers.tsx +++ b/packages/core/client/src/schema-initializer/buttons/ReadPrettyFormActionInitializers.tsx @@ -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, }, ], }, diff --git a/packages/core/client/src/schema-initializer/buttons/TableActionColumnInitializers.tsx b/packages/core/client/src/schema-initializer/buttons/TableActionColumnInitializers.tsx index bc52bdf9c..dea3b2e27 100644 --- a/packages/core/client/src/schema-initializer/buttons/TableActionColumnInitializers.tsx +++ b/packages/core/client/src/schema-initializer/buttons/TableActionColumnInitializers.tsx @@ -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'; }, }, diff --git a/packages/core/client/src/schema-initializer/components/DuplicateAction.tsx b/packages/core/client/src/schema-initializer/components/DuplicateAction.tsx new file mode 100644 index 000000000..23666c8df --- /dev/null +++ b/packages/core/client/src/schema-initializer/components/DuplicateAction.tsx @@ -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 ( +
+ +
+ {isLinkBtn ? ( + + {loading ? t('Duplicating') : children || t('Duplicate')} + + ) : ( + + )} + + + +
+
+
+ ); +}); diff --git a/packages/core/client/src/schema-initializer/components/index.ts b/packages/core/client/src/schema-initializer/components/index.ts index 4073716e5..8b40e5b5d 100644 --- a/packages/core/client/src/schema-initializer/components/index.ts +++ b/packages/core/client/src/schema-initializer/components/index.ts @@ -1,3 +1,4 @@ export * from './assigned-field'; export * from './BulkEditField'; export * from './CreateRecordAction'; +export * from './DuplicateAction' diff --git a/packages/core/client/src/schema-initializer/items/DuplicateActionInitializer.tsx b/packages/core/client/src/schema-initializer/items/DuplicateActionInitializer.tsx new file mode 100644 index 000000000..132aca341 --- /dev/null +++ b/packages/core/client/src/schema-initializer/items/DuplicateActionInitializer.tsx @@ -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 ; +}; diff --git a/packages/core/client/src/schema-initializer/items/index.tsx b/packages/core/client/src/schema-initializer/items/index.tsx index 5fab23dec..bb5456048 100644 --- a/packages/core/client/src/schema-initializer/items/index.tsx +++ b/packages/core/client/src/schema-initializer/items/index.tsx @@ -51,4 +51,5 @@ export * from './TableCollectionFieldInitializer'; export * from './TableSelectorInitializer'; export * from './UpdateActionInitializer'; export * from './UpdateSubmitActionInitializer'; +export * from './DuplicateActionInitializer'; export * from './ViewActionInitializer'; diff --git a/packages/core/client/src/schema-items/OpenModeSchemaItems.tsx b/packages/core/client/src/schema-items/OpenModeSchemaItems.tsx index bc67a8d58..93c48f263 100644 --- a/packages/core/client/src/schema-items/OpenModeSchemaItems.tsx +++ b/packages/core/client/src/schema-items/OpenModeSchemaItems.tsx @@ -29,15 +29,16 @@ export const OpenModeSchemaItems: React.FC = (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) => { } onChange={(value) => { field.componentProps.openSize = 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'].openSize = value; + fieldSchema['x-component-props'].openSize = value; dn.emit('patch', { - schema: { - 'x-uid': fieldSchema['x-uid'], - 'x-component-props': fieldSchema['x-component-props'], - }, + schema: schema, }); dn.refresh(); }}