feat: add custom action (#396)

* feat: add custom operation

* feat: add custom operation

* fix: fix cancel button hide popup

* refactor: save part schema of ActionModal

* fix: can not refresh ui

* fix: save part schema

* fix: revert code

* fix: fix pop up

* fix: fix pop up

* feat: add form custom

* fix: acl allow plugins:getPinned

* fix: add pop up

* feat: fix show callback

* fix: update data

* refactor: reviert code

* fix: update text

* feat: finish table custom operation

* feat: add form custom operation

* fix: refresh update date

* refactor: remove default button design item

* fix: refresh form values

* fix: remove default edit action design item

* fix: fix save form values logic error

* refactor: refactor code

* fix: format schema

* fix: table and form update will change all data

* fix: insert schema non refresh

* fix: fix remote load schema

* fix: fix non request while uid unchange

* fix: locale change

* fix: change x-action to customizePopup

* fix: support custom action modal tip content

* refactor: refactor custom operation

* fix: remove form.setValues

* fix: remove stopPropagation

* feat: improve translation

* feat: popup

Co-authored-by: chenos <chenlinxh@gmail.com>
This commit is contained in:
SemmyWong 2022-05-21 15:08:11 +08:00 committed by GitHub
parent 53482c5b22
commit 4cc162d1c1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 945 additions and 215 deletions

View File

@ -1,9 +1,11 @@
import { useField, useFieldSchema, useForm } from '@formily/react'; import { useField, useFieldSchema, useForm } from '@formily/react';
import { Modal } from 'antd'; import { message, Modal } from 'antd';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useHistory } from 'react-router-dom'; import { useHistory } from 'react-router-dom';
import { useCollection } from '../../collection-manager'; import { useCollection } from '../../collection-manager';
import { useRecord } from '../../record-provider';
import { useActionContext, useCompile } from '../../schema-component'; import { useActionContext, useCompile } from '../../schema-component';
import { useCurrentUserContext } from '../../user';
import { useBlockRequestContext, useFilterByTk } from '../BlockProvider'; import { useBlockRequestContext, useFilterByTk } from '../BlockProvider';
import { useDetailsBlockContext } from '../DetailsBlockProvider'; import { useDetailsBlockContext } from '../DetailsBlockProvider';
import { TableFieldResource } from '../TableFieldProvider'; import { TableFieldResource } from '../TableFieldProvider';
@ -65,8 +67,8 @@ export const useCreateActionProps = () => {
return { return {
async onClick() { async onClick() {
const fieldNames = fields.map((field) => field.name); const fieldNames = fields.map((field) => field.name);
const skipValidator = actionSchema?.['x-action-settings']?.skipValidator; const { assignedValues, onSuccess, overwriteValues, skipValidator } = actionSchema?.['x-action-settings'] ?? {};
const overwriteValues = actionSchema?.['x-action-settings']?.overwriteValues;
if (!skipValidator) { if (!skipValidator) {
await form.submit(); await form.submit();
} }
@ -107,15 +109,16 @@ export const useCreateActionProps = () => {
values: { values: {
...values, ...values,
...overwriteValues, ...overwriteValues,
...assignedValues,
}, },
}); });
actionField.data.loading = false; actionField.data.loading = false;
__parent?.service?.refresh?.(); __parent?.service?.refresh?.();
setVisible?.(false); setVisible?.(false);
const onSuccess = actionSchema?.['x-action-settings']?.onSuccess;
if (!onSuccess?.successMessage) { if (!onSuccess?.successMessage) {
return; return;
} }
if (onSuccess?.manualClose) {
Modal.success({ Modal.success({
title: compile(onSuccess?.successMessage), title: compile(onSuccess?.successMessage),
onOk: async () => { onOk: async () => {
@ -129,6 +132,56 @@ export const useCreateActionProps = () => {
} }
}, },
}); });
} else {
message.success(compile(onSuccess?.successMessage));
}
},
};
};
export const useCustomizeUpdateActionProps = () => {
const { resource, __parent, service } = useBlockRequestContext();
const filterByTk = useFilterByTk();
const actionSchema = useFieldSchema();
const currentRecord = useRecord();
const ctx = useCurrentUserContext();
const history = useHistory();
const compile = useCompile();
const form = useForm();
return {
async onClick() {
const { assignedValues, onSuccess, skipValidator } = actionSchema?.['x-action-settings'] ?? {};
if (skipValidator === false) {
await form.submit();
}
await resource.update({
filterByTk,
values: { ...assignedValues },
});
service?.refresh?.();
if (!(resource instanceof TableFieldResource)) {
__parent?.service?.refresh?.();
}
if (!onSuccess?.successMessage) {
return;
}
if (onSuccess?.manualClose) {
Modal.success({
title: compile(onSuccess?.successMessage),
onOk: async () => {
if (onSuccess?.redirecting && onSuccess?.redirectTo) {
if (isURL(onSuccess.redirectTo)) {
window.location.href = onSuccess.redirectTo;
} else {
history.push(onSuccess.redirectTo);
}
}
},
});
} else {
message.success(compile(onSuccess?.successMessage));
}
}, },
}; };
}; };
@ -145,8 +198,8 @@ export const useUpdateActionProps = () => {
const actionField = useField(); const actionField = useField();
return { return {
async onClick() { async onClick() {
const skipValidator = actionSchema?.['x-action-settings']?.skipValidator; const { assignedValues, onSuccess, overwriteValues, skipValidator } = actionSchema?.['x-action-settings'] ?? {};
const overwriteValues = actionSchema?.['x-action-settings']?.overwriteValues;
if (!skipValidator) { if (!skipValidator) {
await form.submit(); await form.submit();
} }
@ -196,6 +249,7 @@ export const useUpdateActionProps = () => {
values: { values: {
...values, ...values,
...overwriteValues, ...overwriteValues,
...assignedValues,
}, },
}); });
actionField.data.loading = false; actionField.data.loading = false;
@ -204,8 +258,10 @@ export const useUpdateActionProps = () => {
__parent?.__parent?.service?.refresh?.(); __parent?.__parent?.service?.refresh?.();
} }
setVisible?.(false); setVisible?.(false);
const onSuccess = actionSchema?.['x-action-settings']?.onSuccess; if (!onSuccess?.successMessage) {
if (onSuccess?.successMessage) { return;
}
if (onSuccess?.manualClose) {
Modal.success({ Modal.success({
title: compile(onSuccess?.successMessage), title: compile(onSuccess?.successMessage),
onOk: async () => { onOk: async () => {
@ -219,6 +275,8 @@ export const useUpdateActionProps = () => {
} }
}, },
}); });
} else {
message.success(compile(onSuccess?.successMessage));
} }
}, },
}; };

View File

@ -9,6 +9,7 @@ import { useCollectionField } from './hooks';
// TODO: 初步适配 // TODO: 初步适配
const InternalField: React.FC = (props) => { const InternalField: React.FC = (props) => {
const field = useField<Field>(); const field = useField<Field>();
const fieldSchema = useFieldSchema(); const fieldSchema = useFieldSchema();
const { name, interface: interfaceType, uiSchema } = useCollectionField(); const { name, interface: interfaceType, uiSchema } = useCollectionField();
const component = useComponent(uiSchema?.['x-component']); const component = useComponent(uiSchema?.['x-component']);

View File

@ -288,4 +288,15 @@ export default {
'Unsaved changes': 'Unsaved changes', 'Unsaved changes': 'Unsaved changes',
'Are you sure you don\'t want to save?': 'Are you sure you don\'t want to save?', 'Are you sure you don\'t want to save?': 'Are you sure you don\'t want to save?',
'Popup': 'Popup',
'Trigger workflow': 'Trigger workflow',
'Request API': 'Request API',
'Assign field values': 'Assign field values',
'Popup close method': 'Popup close method',
'Automatic close': 'Automatic close',
'Manually close': 'Manually close',
'After successful update': 'After successful update',
'Save record': 'Save record',
'After successful save': 'After successful save',
} }

View File

@ -365,7 +365,7 @@ export default {
'Tab name': '标签名称', 'Tab name': '标签名称',
'Current record blocks': '当前数据区块', 'Current record blocks': '当前数据区块',
'Pop-up message': '弹窗提示消息', 'Popup message': '弹窗提示消息',
'Delete role': '删除角色', 'Delete role': '删除角色',
@ -487,4 +487,22 @@ export default {
'Unsaved changes': '未保存修改', 'Unsaved changes': '未保存修改',
'Are you sure you don\'t want to save?': '你确定不保存修改吗?', 'Are you sure you don\'t want to save?': '你确定不保存修改吗?',
'Dragging': '拖拽中', 'Dragging': '拖拽中',
'Popup': '打开弹窗',
'Trigger workflow': '触发工作流',
'Request API': '请求API',
'Assign field values': '字段赋值',
'Constant value': '静态值',
'Dynamic value': '动态值',
'Current user': '当前用户',
'Current record': '当前记录',
'Popup close method': '弹窗关闭方式',
'Automatic close': '自动关闭',
'Manually close': '手动关闭',
'After successful update': '更新成功后',
'Save record': '保存数据',
'Updated successfully': '更新成功',
'After successful save': '保存成功后',
'After clicking the custom button, the following field values will be assigned according to the following form.': '点击当前自定义按钮时,以下字段值将按照以下表单赋值。',
'After clicking the custom button, The following fields of the current record will be saved according to the following form.': '点击当前自定义按钮时,当前数据以下字段将按照以下表单保存。',
} }

View File

@ -1,17 +1,61 @@
import { ISchema, useField, useFieldSchema } from '@formily/react'; import { ISchema, useField, useFieldSchema } from '@formily/react';
import React from 'react'; import { isValid } from '@formily/shared';
import { uid } from '@nocobase/utils';
import { Menu } from 'antd';
import React, { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useDesignable } from '../..'; import { useActionContext, useCompile, useDesignable } from '../..';
import { GeneralSchemaDesigner, SchemaSettings } from '../../../schema-settings'; import { GeneralSchemaDesigner, SchemaSettings } from '../../../schema-settings';
const MenuGroup = (props) => {
const fieldSchema = useFieldSchema();
const actionType = fieldSchema['x-action'] || '';
const { t } = useTranslation();
const actionTitles = {
'customize:popup': t('Popup'),
'customize:update': t('Update record'),
'customize:save': t('Save record'),
};
if (!['customize:popup', 'customize:update', 'customize:save'].includes(actionType)) {
return <>{props.children}</>;
}
return <Menu.ItemGroup title={`${t('Customize')} > ${actionTitles[actionType]}`}>{props.children}</Menu.ItemGroup>;
};
export const ActionDesigner = (props) => { export const ActionDesigner = (props) => {
const { modalTip, ...restProps } = props;
const field = useField(); const field = useField();
const fieldSchema = useFieldSchema(); const fieldSchema = useFieldSchema();
const { dn } = useDesignable(); const { dn } = useDesignable();
const { t } = useTranslation(); const { t } = useTranslation();
const isPopupAction = ['create', 'update', 'view'].includes(fieldSchema['x-action'] || ''); const compile = useCompile();
const isPopupAction = ['create', 'update', 'view', 'customize:popup'].includes(fieldSchema['x-action'] || '');
const context = useActionContext();
const [initialSchema, setInitialSchema] = useState<ISchema>();
const actionType = fieldSchema['x-action'] || '';
useEffect(() => {
const schemaUid = uid();
const schema: ISchema = {
type: 'void',
'x-uid': schemaUid,
'x-component': 'Grid',
'x-initializer': 'CustomFormItemInitializers',
};
setInitialSchema(schema);
}, [field.address]);
const tips = {
'customize:update': t(
'After clicking the custom button, The following fields of the current record will be saved according to the following form.',
),
'customize:save': t(
'After clicking the custom button, The following fields of the current record will be saved according to the following form.',
),
};
return ( return (
<GeneralSchemaDesigner {...props} disableInitializer> <GeneralSchemaDesigner {...restProps} disableInitializer>
<MenuGroup>
<SchemaSettings.ModalItem <SchemaSettings.ModalItem
title={t('Edit button')} title={t('Edit button')}
schema={ schema={
@ -79,7 +123,27 @@ export const ActionDesigner = (props) => {
}} }}
/> />
)} )}
{fieldSchema?.['x-action-settings'] && (
{isValid(fieldSchema?.['x-action-settings']?.assignedValues) && (
<SchemaSettings.ActionModalItem
title={t('Assign field values')}
initialSchema={initialSchema}
initialValues={fieldSchema?.['x-action-settings']?.assignedValues}
modalTip={tips[actionType]}
uid={fieldSchema?.['x-action-settings']?.schemaUid}
onSubmit={(assignedValues) => {
fieldSchema['x-action-settings']['assignedValues'] = assignedValues;
dn.emit('patch', {
schema: {
['x-uid']: fieldSchema['x-uid'],
'x-action-settings': fieldSchema['x-action-settings'],
},
});
dn.refresh();
}}
/>
)}
{isValid(fieldSchema?.['x-action-settings']?.skipValidator) && (
<SchemaSettings.SwitchItem <SchemaSettings.SwitchItem
title={t('Skip required validation')} title={t('Skip required validation')}
checked={!!fieldSchema?.['x-action-settings']?.skipValidator} checked={!!fieldSchema?.['x-action-settings']?.skipValidator}
@ -96,7 +160,7 @@ export const ActionDesigner = (props) => {
}} }}
/> />
)} )}
{fieldSchema?.['x-action-settings'] && ( {isValid(fieldSchema?.['x-action-settings']?.overwriteValues) && (
<SchemaSettings.ModalItem <SchemaSettings.ModalItem
title={t('Form values')} title={t('Form values')}
schema={ schema={
@ -129,22 +193,37 @@ export const ActionDesigner = (props) => {
}} }}
/> />
)} )}
{fieldSchema?.['x-action-settings'] && ( {isValid(fieldSchema?.['x-action-settings']?.['onSuccess']) && (
<SchemaSettings.ModalItem <SchemaSettings.ModalItem
title={t('After successful submission')} title={
compile(fieldSchema?.['x-action-settings']?.['onSuccess']?.['title']) ?? t('After successful submission')
}
initialValues={fieldSchema?.['x-action-settings']?.['onSuccess']} initialValues={fieldSchema?.['x-action-settings']?.['onSuccess']}
schema={ schema={
{ {
type: 'object', type: 'object',
title: t('After successful submission'), title: {
'customize:save': t('After successful save'),
'customize:update': t('After successful update'),
}[actionType],
properties: { properties: {
successMessage: { successMessage: {
// default: t('Submitted successfully!'), title: t('Popup message'),
title: t('Pop-up message'),
'x-decorator': 'FormItem', 'x-decorator': 'FormItem',
'x-component': 'Input.TextArea', 'x-component': 'Input.TextArea',
'x-component-props': {}, 'x-component-props': {},
}, },
manualClose: {
title: t('Popup close method'),
default: false,
enum: [
{ label: t('Automatic close'), value: false },
{ label: t('Manually close'), value: true },
],
'x-decorator': 'FormItem',
'x-component': 'Radio.Group',
'x-component-props': {},
},
redirecting: { redirecting: {
title: t('Then'), title: t('Then'),
default: false, default: false,
@ -191,9 +270,10 @@ export const ActionDesigner = (props) => {
return s['x-component'] === 'Space' || s['x-component'].endsWith('ActionBar'); return s['x-component'] === 'Space' || s['x-component'].endsWith('ActionBar');
}} }}
confirm={{ confirm={{
title: t('Delete action') title: t('Delete action'),
}} }}
/> />
</MenuGroup>
</GeneralSchemaDesigner> </GeneralSchemaDesigner>
); );
}; };

View File

@ -25,11 +25,11 @@ export const useActionContext = () => {
content: t("Are you sure you don't want to save?"), content: t("Are you sure you don't want to save?"),
async onOk() { async onOk() {
ctx.setFormValueChanged(false); ctx.setFormValueChanged(false);
ctx.setVisible(false); ctx.setVisible?.(false);
}, },
}); });
} else { } else {
ctx.setVisible(false); ctx?.setVisible?.(false);
} }
} else { } else {
ctx?.setVisible?.(visible); ctx?.setVisible?.(visible);

View File

@ -46,7 +46,7 @@ export const MarkdownVoid: any = observer((props: any) => {
const schema = useFieldSchema(); const schema = useFieldSchema();
const { dn } = useDesignable(); const { dn } = useDesignable();
const { onSave, onCancel } = props; const { onSave, onCancel } = props;
return field.editable ? ( return field?.editable ? (
<MarkdownEditor <MarkdownEditor
{...props} {...props}
defaultValue={content} defaultValue={content}

View File

@ -2,6 +2,7 @@ import { isPlainObj } from '@formily/shared';
import React, { createContext, useContext } from 'react'; import React, { createContext, useContext } from 'react';
import { SchemaComponentOptions } from '../schema-component'; import { SchemaComponentOptions } from '../schema-component';
import * as globals from './buttons'; import * as globals from './buttons';
import * as initializerComponents from './components';
import * as items from './items'; import * as items from './items';
import { SchemaInitializer } from './SchemaInitializer'; import { SchemaInitializer } from './SchemaInitializer';
@ -49,7 +50,9 @@ export const SchemaInitializerProvider: React.FC<SchemaInitializerProviderProps>
const { initializers, components, children } = props; const { initializers, components, children } = props;
return ( return (
<SchemaInitializerContext.Provider value={{ ...globals, ...initializers }}> <SchemaInitializerContext.Provider value={{ ...globals, ...initializers }}>
<SchemaComponentOptions components={{ ...items, ...components }}>{children}</SchemaComponentOptions> <SchemaComponentOptions components={{ ...items, ...components, ...initializerComponents }}>
{children}
</SchemaComponentOptions>
</SchemaInitializerContext.Provider> </SchemaInitializerContext.Provider>
); );
}; };

View File

@ -0,0 +1,26 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { SchemaInitializer } from '../SchemaInitializer';
import { gridRowColWrap, useCustomFormItemInitializerFields } from '../utils';
// 表单里配置字段
export const CustomFormItemInitializers = (props: any) => {
const { t } = useTranslation();
const { insertPosition, component } = props;
return (
<SchemaInitializer.Button
wrap={gridRowColWrap}
icon={'SettingOutlined'}
items={[
{
type: 'itemGroup',
title: t('Configure fields'),
children: useCustomFormItemInitializerFields(),
},
]}
insertPosition={insertPosition}
component={component}
title={component ? null : t('Configure fields')}
/>
);
};

View File

@ -1,4 +1,3 @@
// 表单的操作配置 // 表单的操作配置
export const FormActionInitializers = { export const FormActionInitializers = {
title: '{{t("Configure actions")}}', title: '{{t("Configure actions")}}',
@ -13,12 +12,7 @@ export const FormActionInitializers = {
title: '{{t("Submit")}}', title: '{{t("Submit")}}',
component: 'CreateSubmitActionInitializer', component: 'CreateSubmitActionInitializer',
schema: { schema: {
'x-action-settings': { 'x-action-settings': {},
initialValues: {},
onSuccess: {
successMessage: '{{t("Submitted successfully")}}',
},
},
}, },
}, },
], ],
@ -32,16 +26,74 @@ export const FormActionInitializers = {
children: [ children: [
{ {
type: 'item', type: 'item',
title: '{{t("Save action")}}', title: '{{t("Popup")}}',
component: 'CustomizeActionInitializer', component: 'CustomizeActionInitializer',
schema: { schema: {
title: '{{ t("Save") }}', type: 'void',
title: '{{ t("Popup") }}',
'x-action': 'customize:popup',
'x-designer': 'Action.Designer',
'x-component': 'Action',
'x-component-props': {
openMode: 'drawer',
},
properties: {
drawer: {
type: 'void',
title: '{{ t("Popup") }}',
'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("Details")}}',
'x-component': 'Tabs.TabPane',
'x-designer': 'Tabs.Designer',
'x-component-props': {},
properties: {
grid: {
type: 'void',
'x-component': 'Grid',
'x-initializer': 'RecordBlockInitializers',
properties: {},
},
},
},
},
},
},
},
},
},
},
{
type: 'item',
title: '{{t("Save record")}}',
component: 'CustomizeActionInitializer',
schema: {
title: '{{ t("Save record") }}',
'x-action': 'customize:save',
'x-component': 'Action', 'x-component': 'Action',
'x-designer': 'Action.Designer', 'x-designer': 'Action.Designer',
'x-designer-props': {
modalTip:
'{{ t("When the button is clicked, the following fields will be assigned and saved together with the fields in the form. If there are overlapping fields, the value here will overwrite the value in the form.") }}',
},
'x-action-settings': { 'x-action-settings': {
initialValues: {}, assignedValues: {},
skipValidator: false,
onSuccess: { onSuccess: {
successMessage: '{{t("Submitted successfully")}}', manualClose: true,
redirecting: false,
successMessage: '{{t("Saved successfully")}}',
}, },
}, },
'x-component-props': { 'x-component-props': {
@ -67,9 +119,7 @@ export const CreateFormActionInitializers = {
title: '{{t("Submit")}}', title: '{{t("Submit")}}',
component: 'CreateSubmitActionInitializer', component: 'CreateSubmitActionInitializer',
schema: { schema: {
'x-action-settings': { 'x-action-settings': {},
overwriteValues: {},
},
}, },
}, },
], ],
@ -83,14 +133,75 @@ export const CreateFormActionInitializers = {
children: [ children: [
{ {
type: 'item', type: 'item',
title: '{{t("Save action")}}', title: '{{t("Popup")}}',
component: 'CustomizeActionInitializer', component: 'CustomizeActionInitializer',
schema: { schema: {
title: '{{ t("Save") }}', type: 'void',
title: '{{ t("Popup") }}',
'x-action': 'customize:popup',
'x-designer': 'Action.Designer',
'x-component': 'Action',
'x-component-props': {
openMode: 'drawer',
},
properties: {
drawer: {
type: 'void',
title: '{{ t("Popup") }}',
'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("Details")}}',
'x-component': 'Tabs.TabPane',
'x-designer': 'Tabs.Designer',
'x-component-props': {},
properties: {
grid: {
type: 'void',
'x-component': 'Grid',
'x-initializer': 'RecordBlockInitializers',
properties: {},
},
},
},
},
},
},
},
},
},
},
{
type: 'item',
title: '{{t("Save record")}}',
component: 'CustomizeActionInitializer',
schema: {
title: '{{ t("Save record") }}',
'x-action': 'customize:save',
'x-component': 'Action', 'x-component': 'Action',
'x-designer': 'Action.Designer', 'x-designer': 'Action.Designer',
'x-designer-props': {
modalTip:
'{{ t("When the button is clicked, the following fields will be assigned and saved together with the fields in the form. If there are overlapping fields, the value here will overwrite the value in the form.") }}',
},
'x-action-settings': { 'x-action-settings': {
overwriteValues: {}, assignedValues: {},
skipValidator: false,
onSuccess: {
manualClose: true,
redirecting: false,
successMessage: '{{t("Saved successfully")}}',
},
}, },
'x-component-props': { 'x-component-props': {
useProps: '{{ useCreateActionProps }}', useProps: '{{ useCreateActionProps }}',
@ -115,9 +226,7 @@ export const UpdateFormActionInitializers = {
title: '{{t("Submit")}}', title: '{{t("Submit")}}',
component: 'UpdateSubmitActionInitializer', component: 'UpdateSubmitActionInitializer',
schema: { schema: {
'x-action-settings': { 'x-action-settings': {},
overwriteValues: {},
},
}, },
}, },
], ],
@ -131,14 +240,75 @@ export const UpdateFormActionInitializers = {
children: [ children: [
{ {
type: 'item', type: 'item',
title: '{{t("Save action")}}', title: '{{t("Popup")}}',
component: 'CustomizeActionInitializer',
schema: {
type: 'void',
title: '{{ t("Popup") }}',
'x-action': 'customize:popup',
'x-designer': 'Action.Designer',
'x-component': 'Action',
'x-component-props': {
openMode: 'drawer',
},
properties: {
drawer: {
type: 'void',
title: '{{ t("Popup") }}',
'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("Details")}}',
'x-component': 'Tabs.TabPane',
'x-designer': 'Tabs.Designer',
'x-component-props': {},
properties: {
grid: {
type: 'void',
'x-component': 'Grid',
'x-initializer': 'RecordBlockInitializers',
properties: {},
},
},
},
},
},
},
},
},
},
},
{
type: 'item',
title: '{{t("Save record")}}',
component: 'CustomizeActionInitializer', component: 'CustomizeActionInitializer',
schema: { schema: {
title: '{{ t("Save") }}', title: '{{ t("Save") }}',
'x-component': 'Action', 'x-component': 'Action',
'x-action': 'customize:save',
'x-designer': 'Action.Designer', 'x-designer': 'Action.Designer',
'x-designer-props': {
modalTip:
'{{ t("When the button is clicked, the following fields will be assigned and saved together with the fields in the form. If there are overlapping fields, the value here will overwrite the value in the form.") }}',
},
'x-action-settings': { 'x-action-settings': {
overwriteValues: {}, assignedValues: {},
skipValidator: false,
onSuccess: {
manualClose: true,
redirecting: false,
successMessage: '{{t("Saved successfully")}}',
},
}, },
'x-component-props': { 'x-component-props': {
useProps: '{{ useUpdateActionProps }}', useProps: '{{ useUpdateActionProps }}',

View File

@ -33,5 +33,87 @@ export const ReadPrettyFormActionInitializers = {
}, },
], ],
}, },
{
type: 'divider',
},
{
type: 'subMenu',
title: '{{t("Customize")}}',
children: [
{
type: 'item',
title: '{{t("Popup")}}',
component: 'CustomizeActionInitializer',
schema: {
type: 'void',
title: '{{ t("Popup") }}',
'x-action': 'customize:popup',
'x-designer': 'Action.Designer',
'x-component': 'Action',
'x-component-props': {
openMode: 'drawer',
},
properties: {
drawer: {
type: 'void',
title: '{{ t("Popup") }}',
'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("Details")}}',
'x-component': 'Tabs.TabPane',
'x-designer': 'Tabs.Designer',
'x-component-props': {},
properties: {
grid: {
type: 'void',
'x-component': 'Grid',
'x-initializer': 'RecordBlockInitializers',
properties: {},
},
},
},
},
},
},
},
},
},
},
{
type: 'item',
title: '{{t("Update record")}}',
component: 'CustomizeActionInitializer',
schema: {
title: '{{ t("Update record") }}',
'x-component': 'Action',
'x-designer': 'Action.Designer',
'x-action': 'customize:update',
'x-action-settings': {
assignedValues: {},
onSuccess: {
manualClose: true,
redirecting: false,
successMessage: '{{t("Updated successfully")}}',
},
},
'x-component-props': {
useProps: '{{ useCustomizeUpdateActionProps }}',
},
},
},
],
},
], ],
}; };

View File

@ -70,6 +70,87 @@ export const TableActionColumnInitializers = (props: any) => {
}, },
], ],
}, },
{
type: 'divider',
},
{
type: 'subMenu',
title: '{{t("Customize")}}',
children: [
{
type: 'item',
title: '{{t("Popup")}}',
component: 'CustomizeActionInitializer',
schema: {
type: 'void',
title: '{{ t("Popup") }}',
'x-action': 'customize:popup',
'x-designer': 'Action.Designer',
'x-component': 'Action.Link',
'x-component-props': {
openMode: 'drawer',
},
properties: {
drawer: {
type: 'void',
title: '{{ t("Popup") }}',
'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("Details")}}',
'x-component': 'Tabs.TabPane',
'x-designer': 'Tabs.Designer',
'x-component-props': {},
properties: {
grid: {
type: 'void',
'x-component': 'Grid',
'x-initializer': 'RecordBlockInitializers',
properties: {},
},
},
},
},
},
},
},
},
},
},
{
type: 'item',
title: '{{t("Update record")}}',
component: 'CustomizeActionInitializer',
schema: {
title: '{{ t("Update record") }}',
'x-component': 'Action.Link',
'x-action': 'customize:update',
'x-designer': 'Action.Designer',
'x-action-settings': {
assignedValues: {},
onSuccess: {
manualClose: true,
redirecting: false,
successMessage: '{{t("Updated successfully")}}',
},
},
'x-component-props': {
useProps: '{{ useCustomizeUpdateActionProps }}',
},
},
},
],
},
]} ]}
component={<MenuOutlined style={{ cursor: 'pointer' }} />} component={<MenuOutlined style={{ cursor: 'pointer' }} />}
/> />

View File

@ -1,6 +1,7 @@
export * from './BlockInitializers'; export * from './BlockInitializers';
export * from './CalendarActionInitializers'; export * from './CalendarActionInitializers';
export * from './CreateFormBlockInitializers'; export * from './CreateFormBlockInitializers';
export * from './CustomFormItemInitializers';
export * from './DetailsActionInitializers'; export * from './DetailsActionInitializers';
export * from './FormActionInitializers'; export * from './FormActionInitializers';
export * from './FormItemInitializers'; export * from './FormItemInitializers';
@ -15,4 +16,3 @@ export * from './TableActionInitializers';
export * from './TableColumnInitializers'; export * from './TableColumnInitializers';
export * from './TableSelectorInitializers'; export * from './TableSelectorInitializers';
export * from './TabPaneInitializers'; export * from './TabPaneInitializers';

View File

@ -0,0 +1,70 @@
import { Field } from '@formily/core';
import { useField, useFieldSchema } from '@formily/react';
import React, { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { CollectionField, useCollection } from '../../../collection-manager';
import { useCompile, useFilterOptions } from '../../../schema-component';
export const AssignedField = (props: any) => {
const { t } = useTranslation();
const compile = useCompile();
const field = useField<Field>();
const fieldSchema = useFieldSchema();
const [type, setType] = useState<string>('constantValue');
const [value, setValue] = useState(field?.value?.value ?? '');
const [options, setOptions] = useState<any[]>([]);
const { getField } = useCollection();
const collectionField = getField(fieldSchema.name);
const { uiSchema } = collectionField;
const currentUser = useFilterOptions('users');
const currentRecord = useFilterOptions(collectionField.collectionName);
useEffect(() => {
const opt = [
{
name: 'currentUser',
title: t('Current user'),
children: [...currentUser],
},
{
name: 'currentRecord',
title: t('Current record'),
children: [...currentRecord],
},
];
setOptions(compile(opt));
}, []);
const valueChangeHandler = (val) => {
setValue(val);
};
return <CollectionField {...props} value={field.value} onChange={valueChangeHandler} />;
// return (
// <Space>
// <Select defaultValue={type} value={type} style={{ width: 120 }} onChange={typeChangeHandler}>
// <Select.Option value="constantValue">{t('Constant value')}</Select.Option>
// <Select.Option value="dynamicValue">{t('Dynamic value')}</Select.Option>
// </Select>
// {type === 'constantValue' ? (
// <CollectionField {...props} onChange={valueChangeHandler} />
// ) : (
// <Cascader
// fieldNames={{
// label: 'title',
// value: 'name',
// children: 'children',
// }}
// style={{
// width: 150,
// }}
// options={options}
// onChange={valueChangeHandler}
// defaultValue={value}
// />
// )}
// </Space>
// );
};

View File

@ -0,0 +1 @@
export * from './AssignedField';

View File

@ -0,0 +1 @@
export * from './assigned-field';

View File

@ -20,7 +20,7 @@ import {
createTableSelectorSchema, createTableSelectorSchema,
useCollectionDataSourceItems, useCollectionDataSourceItems,
useCurrentSchema, useCurrentSchema,
useRecordCollectionDataSourceItems useRecordCollectionDataSourceItems,
} from '../utils'; } from '../utils';
// Block // Block

View File

@ -39,6 +39,19 @@ export const removeGridFormItem = (schema, cb) => {
}); });
}; };
export const useRemoveGridFormItem = () => {
const form = useForm();
return (schema, cb) => {
cb(schema, {
removeParentsIfNoChildren: true,
breakRemoveOn: {
'x-component': 'Grid',
},
});
delete form.values?.[schema.name];
};
};
export const findTableColumn = (schema: Schema, key: string, action: string, deepth: number = 0) => { export const findTableColumn = (schema: Schema, key: string, action: string, deepth: number = 0) => {
return schema.reduceProperties((buf, s) => { return schema.reduceProperties((buf, s) => {
if (s[key] === action) { if (s[key] === action) {
@ -92,6 +105,7 @@ export const useFormItemInitializerFields = (options?: any) => {
const { getInterface } = useCollectionManager(); const { getInterface } = useCollectionManager();
const form = useForm(); const form = useForm();
const { readPretty = form.readPretty, block = 'Form' } = options || {}; const { readPretty = form.readPretty, block = 'Form' } = options || {};
return fields return fields
?.filter((field) => field?.interface) ?.filter((field) => field?.interface)
?.map((field) => { ?.map((field) => {
@ -119,6 +133,40 @@ export const useFormItemInitializerFields = (options?: any) => {
}); });
}; };
export const useCustomFormItemInitializerFields = (options?: any) => {
const { name, fields } = useCollection();
const { getInterface } = useCollectionManager();
const form = useForm();
const { readPretty = form.readPretty, block = 'Form' } = options || {};
const remove = useRemoveGridFormItem();
return fields
?.filter((field) => {
return field?.interface && !field?.uiSchema?.['x-read-pretty'];
})
?.map((field) => {
const interfaceConfig = getInterface(field.interface);
const schema = {
type: 'string',
name: field.name,
title: field?.uiSchema?.title || field.name,
'x-designer': 'FormItem.Designer',
'x-component': 'AssignedField',
'x-decorator': 'FormItem',
'x-collection-field': `${name}.${field.name}`,
};
return {
type: 'item',
title: field?.uiSchema?.title || field.name,
component: 'CollectionFieldInitializer',
remove: remove,
schemaInitialize: (s) => {
interfaceConfig?.schemaInitialize?.(s, { field, block, readPretty });
},
schema,
} as SchemaInitializerItemOptions;
});
};
const findSchema = (schema: Schema, key: string, action: string) => { const findSchema = (schema: Schema, key: string, action: string) => {
return schema.reduceProperties((buf, s) => { return schema.reduceProperties((buf, s) => {
if (s[key] === action) { if (s[key] === action) {

View File

@ -1,25 +1,28 @@
import { css } from '@emotion/css'; import { css } from '@emotion/css';
import { FormDialog, FormItem, FormLayout, Input } from '@formily/antd'; import { FormDialog, FormItem, FormLayout, Input } from '@formily/antd';
import { GeneralField } from '@formily/core'; import { createForm, GeneralField } from '@formily/core';
import { ISchema, Schema, SchemaOptionsContext } from '@formily/react'; import { ISchema, Schema, SchemaOptionsContext, useFieldSchema } from '@formily/react';
import { uid } from '@formily/shared'; import { uid } from '@formily/shared';
import { Dropdown, Menu, MenuItemProps, Modal, Select, Switch } from 'antd'; import { Alert, Button, Dropdown, Menu, MenuItemProps, Modal, Select, Space, Switch } from 'antd';
import classNames from 'classnames'; import classNames from 'classnames';
import React, { createContext, useContext, useState } from 'react'; import { cloneDeep } from 'lodash';
import React, { createContext, useContext, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { import {
ActionContext, ActionContext,
createDesignable, createDesignable,
Designable, Designable,
FormProvider,
RemoteSchemaComponent,
SchemaComponent, SchemaComponent,
SchemaComponentOptions, SchemaComponentOptions,
useActionContext, useActionContext,
useAPIClient, useAPIClient,
useCollection useCollection,
useCompile
} from '..'; } from '..';
import { useSchemaTemplateManager } from '../schema-templates'; import { useSchemaTemplateManager } from '../schema-templates';
import { useBlockTemplateContext } from '../schema-templates/BlockTemplate'; import { useBlockTemplateContext } from '../schema-templates/BlockTemplate';
interface SchemaSettingsProps { interface SchemaSettingsProps {
title?: any; title?: any;
dn?: Designable; dn?: Designable;
@ -462,6 +465,83 @@ SchemaSettings.PopupItem = (props) => {
); );
}; };
SchemaSettings.ActionModalItem = React.memo((props: any) => {
const { title, onSubmit, initialValues, initialSchema, modalTip, ...others } = props;
const [visible, setVisible] = useState(false);
const [schemaUid, setSchemaUid] = useState<string>(props.uid);
const { t } = useTranslation();
const fieldSchema = useFieldSchema();
const ctx = useContext(SchemaSettingsContext);
const { dn } = useSchemaSettings();
const compile = useCompile();
const api = useAPIClient();
const form = useMemo(
() =>
createForm({
initialValues: cloneDeep(initialValues),
}),
[],
);
const cancelHandler = () => {
setVisible(false);
};
const submitHandler = () => {
onSubmit?.(cloneDeep(form.values));
setVisible(false);
};
const openAssignedFieldValueHandler = async () => {
if (!schemaUid && initialSchema['x-uid']) {
fieldSchema['x-action-settings'].schemaUid = initialSchema['x-uid'];
dn.emit('patch', { schema: fieldSchema });
await api.resource('uiSchemas').insert({ values: initialSchema });
setSchemaUid(initialSchema['x-uid']);
}
ctx.setVisible(false);
setVisible(true);
};
return (
<div
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
}}
>
<SchemaSettings.Item {...others} onClick={openAssignedFieldValueHandler}>
{props.children || props.title}
</SchemaSettings.Item>
<Modal
width={'50%'}
title={compile(title)}
{...others}
destroyOnClose
visible={visible}
onCancel={cancelHandler}
footer={
<Space>
<Button onClick={cancelHandler}>{t('Cancel')}</Button>
<Button type="primary" onClick={submitHandler}>
{t('Submit')}
</Button>
</Space>
}
>
<FormProvider form={form}>
<FormLayout layout={'vertical'}>
{modalTip && <Alert message={modalTip} />}
{modalTip && <br />}
{visible && <RemoteSchemaComponent noForm uid={schemaUid} />}
</FormLayout>
</FormProvider>
</Modal>
</div>
);
});
SchemaSettings.ModalItem = (props) => { SchemaSettings.ModalItem = (props) => {
const { hidden, title, components, scope, effects, schema, onSubmit, initialValues, ...others } = props; const { hidden, title, components, scope, effects, schema, onSubmit, initialValues, ...others } = props;
const options = useContext(SchemaOptionsContext); const options = useContext(SchemaOptionsContext);

View File

@ -202,7 +202,7 @@ SettingsForm.Drawer = () => {
style={{ width: 200 }} style={{ width: 200 }}
onClick={async () => { onClick={async () => {
setDropdownVisible(false); setDropdownVisible(false);
const values = await FormDrawer('Pop-up form', () => { const values = await FormDrawer('Popup form', () => {
return ( return (
<SchemaComponentOptions scope={options.scope} components={{ ...options.components, FormItem }}> <SchemaComponentOptions scope={options.scope} components={{ ...options.components, FormItem }}>
<FormLayout layout={'vertical'}> <FormLayout layout={'vertical'}>