feat(plugin-custom-request): support better custom-request (#2536)
* feat(plugin-custom-request): support better custom-request * feat: re implement custom request * feat: complete configuration * feat: support request in server * feat: improve usage * feat: support request * feat: remove customRequests when action is deleted * feat: avoid override * fix: the data is not latest * feat: should autoSize in url * feat: support title and acl * fix: i18n * feat: support display title * feat: i18n supports * refactor: acl check * feat: better method * fix: style * fix: remove unused code * fix: acl * docs: update * fix: upgrade * feat: hidden skip validator option and display should display data * feat: support root access all custom-request * chore: update deps * fix: limit height of modal body, Close T-1682 * fix: parse incorrect, Close T-1686 * fix: i18n, Close T-1684 * fix: linkage rules incorrect, Close T-1719 * fix: missing pass appends, Close T-1720 * fix: should update initialValues when it changed * feat: update better message * fix: permission incorrect * test: setup * refactor: fix locale * chore: update deps * feat: move to @nocobase * fix: dir * fix: ui.* * feat: add listByCurrentRole action, add decorator instead of acl * fix: remove title field * feat: compatible pervious version * feat: support passing x-* headers to custom-request * feat: support list all keys * fix: support baseURL, close T-2090 * fix: remove unused code * fix: sqlite, Close T-2092 * fix: close T-2089 * feat: alert error when request settings doesn't configure, Close T-2101 * feat: improve message * chore: fix eslint error * fix: fix version of plugin-custom-request * fix: build error --------- Co-authored-by: chenos <chenlinxh@gmail.com> Co-authored-by: Rain <958414905@qq.com>
This commit is contained in:
		
							parent
							
								
									04db43985c
								
							
						
					
					
						commit
						bf099a4364
					
				| @ -4,10 +4,7 @@ export const useStyles = createStyles(({ css }) => { | |||||||
|   return css` |   return css` | ||||||
|     .ant-table-cell { |     .ant-table-cell { | ||||||
|       > .ant-space-horizontal { |       > .ant-space-horizontal { | ||||||
|         .ant-space-item:empty:not(:last-child) + .ant-space-item-split { |         .ant-space-item-split:has(+ .ant-space-item:empty) { | ||||||
|           display: none; |  | ||||||
|         } |  | ||||||
|         .ant-space-item-split:has(+ .ant-space-item:last-child:empty) { |  | ||||||
|           display: none; |           display: none; | ||||||
|         } |         } | ||||||
|       } |       } | ||||||
|  | |||||||
| @ -66,7 +66,7 @@ const filterValue = (value) => { | |||||||
|   return obj; |   return obj; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| function getFormValues({ | export function getFormValues({ | ||||||
|   filterByTk, |   filterByTk, | ||||||
|   field, |   field, | ||||||
|   form, |   form, | ||||||
|  | |||||||
| @ -7,9 +7,8 @@ import { CollectionManagerContext } from '../context'; | |||||||
| import { CollectionFieldOptions, CollectionOptions } from '../types'; | import { CollectionFieldOptions, CollectionOptions } from '../types'; | ||||||
| 
 | 
 | ||||||
| export const useCollectionManager = () => { | export const useCollectionManager = () => { | ||||||
|   const { refreshCM, updateCollection, service, interfaces, collections, templates } = useContext( |   const { refreshCM, updateCollection, service, interfaces, collections, templates } = | ||||||
|     CollectionManagerContext, |     useContext(CollectionManagerContext); | ||||||
|   ); |  | ||||||
|   const compile = useCompile(); |   const compile = useCompile(); | ||||||
|   const getInheritedFields = (name) => { |   const getInheritedFields = (name) => { | ||||||
|     const inheritKeys = getInheritCollections(name); |     const inheritKeys = getInheritCollections(name); | ||||||
|  | |||||||
| @ -2,7 +2,7 @@ import { ArrayTable } from '@formily/antd-v5'; | |||||||
| import { onFieldValueChange, onFieldInputValueChange } from '@formily/core'; | import { onFieldValueChange, onFieldInputValueChange } from '@formily/core'; | ||||||
| import { connect, ISchema, mapProps, useField, useFieldSchema, useForm, useFormEffects } from '@formily/react'; | import { connect, ISchema, mapProps, useField, useFieldSchema, useForm, useFormEffects } from '@formily/react'; | ||||||
| import { isValid, uid } from '@formily/shared'; | import { isValid, uid } from '@formily/shared'; | ||||||
| import { Alert, Tree as AntdTree } from 'antd'; | import { Alert, Tree as AntdTree, ModalProps } from 'antd'; | ||||||
| import { cloneDeep } from 'lodash'; | import { cloneDeep } from 'lodash'; | ||||||
| import React, { useCallback, useEffect, useMemo, useState } from 'react'; | import React, { useCallback, useEffect, useMemo, useState } from 'react'; | ||||||
| import { useTranslation } from 'react-i18next'; | import { useTranslation } from 'react-i18next'; | ||||||
| @ -68,12 +68,12 @@ const MenuGroup = (props) => { | |||||||
|   ); |   ); | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| function ButtonEditor() { | function ButtonEditor(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 isLink = fieldSchema['x-component'] === 'Action.Link'; |   const isLink = props?.isLink || fieldSchema['x-component'] === 'Action.Link'; | ||||||
| 
 | 
 | ||||||
|   return ( |   return ( | ||||||
|     <SchemaSettings.ModalItem |     <SchemaSettings.ModalItem | ||||||
| @ -689,7 +689,11 @@ function AfterSuccess() { | |||||||
|   ); |   ); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| function RemoveButton() { | function RemoveButton( | ||||||
|  |   props: { | ||||||
|  |     onConfirmOk?: ModalProps['onOk']; | ||||||
|  |   } = {}, | ||||||
|  | ) { | ||||||
|   const { t } = useTranslation(); |   const { t } = useTranslation(); | ||||||
|   const fieldSchema = useFieldSchema(); |   const fieldSchema = useFieldSchema(); | ||||||
|   const isDeletable = fieldSchema?.parent['x-component'] === 'CollectionField'; |   const isDeletable = fieldSchema?.parent['x-component'] === 'CollectionField'; | ||||||
| @ -704,6 +708,7 @@ function RemoveButton() { | |||||||
|           }} |           }} | ||||||
|           confirm={{ |           confirm={{ | ||||||
|             title: t('Delete action'), |             title: t('Delete action'), | ||||||
|  |             onOk: props.onConfirmOk, | ||||||
|           }} |           }} | ||||||
|         /> |         /> | ||||||
|       </> |       </> | ||||||
| @ -890,7 +895,7 @@ function WorkflowConfig() { | |||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export const ActionDesigner = (props) => { | export const ActionDesigner = (props) => { | ||||||
|   const { modalTip, linkageAction, ...restProps } = props; |   const { modalTip, linkageAction, removeButtonProps, buttonEditorProps, linkageRulesProps, ...restProps } = props; | ||||||
|   const fieldSchema = useFieldSchema(); |   const fieldSchema = useFieldSchema(); | ||||||
|   const { name } = useCollection(); |   const { name } = useCollection(); | ||||||
|   const { getChildrenCollections } = useCollectionManager(); |   const { getChildrenCollections } = useCollectionManager(); | ||||||
| @ -907,10 +912,10 @@ export const ActionDesigner = (props) => { | |||||||
|   return ( |   return ( | ||||||
|     <GeneralSchemaDesigner {...restProps} disableInitializer draggable={isDraggable}> |     <GeneralSchemaDesigner {...restProps} disableInitializer draggable={isDraggable}> | ||||||
|       <MenuGroup> |       <MenuGroup> | ||||||
|         <ButtonEditor /> |         <ButtonEditor {...buttonEditorProps} /> | ||||||
|         {fieldSchema['x-action'] === 'submit' && |         {fieldSchema['x-action'] === 'submit' && | ||||||
|           fieldSchema.parent?.['x-initializer'] === 'CreateFormActionInitializers' && <SaveMode />} |           fieldSchema.parent?.['x-initializer'] === 'CreateFormActionInitializers' && <SaveMode />} | ||||||
|         {isLinkageAction && <SchemaSettings.LinkageRules collectionName={name} />} |         {isLinkageAction && <SchemaSettings.LinkageRules {...linkageRulesProps} collectionName={name} />} | ||||||
|         {isDuplicateAction && <DuplicationMode />} |         {isDuplicateAction && <DuplicationMode />} | ||||||
|         <OpenModeSchemaItems openMode={isPopupAction} openSize={isPopupAction} /> |         <OpenModeSchemaItems openMode={isPopupAction} openSize={isPopupAction} /> | ||||||
|         {isUpdateModePopupAction && <UpdateMode />} |         {isUpdateModePopupAction && <UpdateMode />} | ||||||
| @ -919,10 +924,10 @@ export const ActionDesigner = (props) => { | |||||||
|         {isValid(fieldSchema?.['x-action-settings']?.skipValidator) && <SkipValidation />} |         {isValid(fieldSchema?.['x-action-settings']?.skipValidator) && <SkipValidation />} | ||||||
|         {isValid(fieldSchema?.['x-action-settings']?.['onSuccess']) && <AfterSuccess />} |         {isValid(fieldSchema?.['x-action-settings']?.['onSuccess']) && <AfterSuccess />} | ||||||
|         {isValid(fieldSchema?.['x-action-settings']?.triggerWorkflows) && <WorkflowConfig />} |         {isValid(fieldSchema?.['x-action-settings']?.triggerWorkflows) && <WorkflowConfig />} | ||||||
| 
 |         {restProps.children} | ||||||
|         {isChildCollectionAction && <SchemaSettings.EnableChildCollections collectionName={name} />} |         {isChildCollectionAction && <SchemaSettings.EnableChildCollections collectionName={name} />} | ||||||
| 
 | 
 | ||||||
|         {<RemoveButton />} |         {<RemoveButton {...removeButtonProps} />} | ||||||
|       </MenuGroup> |       </MenuGroup> | ||||||
|     </GeneralSchemaDesigner> |     </GeneralSchemaDesigner> | ||||||
|   ); |   ); | ||||||
|  | |||||||
| @ -23,6 +23,7 @@ import { ErrorFallback } from '../error-fallback'; | |||||||
| import FixedBlock from './FixedBlock'; | import FixedBlock from './FixedBlock'; | ||||||
| import { PageDesigner, PageTabDesigner } from './PageTabDesigner'; | import { PageDesigner, PageTabDesigner } from './PageTabDesigner'; | ||||||
| import { useStyles } from './style'; | import { useStyles } from './style'; | ||||||
|  | import { useStyles as useAClStyles } from '../../../acl/style'; | ||||||
| 
 | 
 | ||||||
| export const Page = (props) => { | export const Page = (props) => { | ||||||
|   const { children, ...others } = props; |   const { children, ...others } = props; | ||||||
| @ -58,6 +59,7 @@ export const Page = (props) => { | |||||||
|   ); |   ); | ||||||
|   const [height, setHeight] = useState(0); |   const [height, setHeight] = useState(0); | ||||||
|   const { wrapSSR, hashId, componentCls } = useStyles(); |   const { wrapSSR, hashId, componentCls } = useStyles(); | ||||||
|  |   const aclStyles = useAClStyles(); | ||||||
| 
 | 
 | ||||||
|   const handleErrors = (error) => { |   const handleErrors = (error) => { | ||||||
|     console.error(error); |     console.error(error); | ||||||
| @ -66,7 +68,7 @@ export const Page = (props) => { | |||||||
|   const pageHeaderTitle = hidePageTitle ? undefined : fieldSchema.title || compile(title); |   const pageHeaderTitle = hidePageTitle ? undefined : fieldSchema.title || compile(title); | ||||||
|   return wrapSSR( |   return wrapSSR( | ||||||
|     <FilterBlockProvider> |     <FilterBlockProvider> | ||||||
|       <div className={`${componentCls} ${hashId}`}> |       <div className={`${componentCls} ${hashId} ${aclStyles.styles}`}> | ||||||
|         <PageDesigner title={fieldSchema.title || title} /> |         <PageDesigner title={fieldSchema.title || title} /> | ||||||
|         <div |         <div | ||||||
|           ref={(ref) => { |           ref={(ref) => { | ||||||
|  | |||||||
| @ -131,7 +131,6 @@ function getTypedConstantOption(type: string, types: true | string[], fieldNames | |||||||
| export function Input(props) { | export function Input(props) { | ||||||
|   const { |   const { | ||||||
|     value = '', |     value = '', | ||||||
|     scope, |  | ||||||
|     onChange, |     onChange, | ||||||
|     children, |     children, | ||||||
|     button, |     button, | ||||||
| @ -141,6 +140,7 @@ export function Input(props) { | |||||||
|     changeOnSelect, |     changeOnSelect, | ||||||
|     fieldNames, |     fieldNames, | ||||||
|   } = props; |   } = props; | ||||||
|  |   const scope = typeof props.scope === 'function' ? props.scope() : props.scope; | ||||||
|   const { wrapSSR, hashId, componentCls, rootPrefixCls } = useStyles(); |   const { wrapSSR, hashId, componentCls, rootPrefixCls } = useStyles(); | ||||||
| 
 | 
 | ||||||
|   // 添加 antd input 样式,防止样式缺失
 |   // 添加 antd input 样式,防止样式缺失
 | ||||||
|  | |||||||
| @ -18,7 +18,8 @@ function setNativeInputValue(input, value) { | |||||||
| 
 | 
 | ||||||
| export function RawTextArea(props): JSX.Element { | export function RawTextArea(props): JSX.Element { | ||||||
|   const inputRef = useRef<any>(null); |   const inputRef = useRef<any>(null); | ||||||
|   const { scope, changeOnSelect, component: Component = Input.TextArea, ...others } = props; |   const { changeOnSelect, component: Component = Input.TextArea, ...others } = props; | ||||||
|  |   const scope = typeof props.scope === 'function' ? props.scope() : props.scope; | ||||||
|   const [options, setOptions] = useState(scope ? cloneDeep(scope) : []); |   const [options, setOptions] = useState(scope ? cloneDeep(scope) : []); | ||||||
| 
 | 
 | ||||||
|   function onInsert(selected) { |   function onInsert(selected) { | ||||||
| @ -56,7 +57,13 @@ export function RawTextArea(props): JSX.Element { | |||||||
|           } |           } | ||||||
|         `}
 |         `}
 | ||||||
|       > |       > | ||||||
|         <VariableSelect options={options} setOptions={setOptions} onInsert={onInsert} changeOnSelect={changeOnSelect} /> |         <VariableSelect | ||||||
|  |           fieldNames={props.fieldNames} | ||||||
|  |           options={options} | ||||||
|  |           setOptions={setOptions} | ||||||
|  |           onInsert={onInsert} | ||||||
|  |           changeOnSelect={changeOnSelect} | ||||||
|  |         /> | ||||||
|       </Button.Group> |       </Button.Group> | ||||||
|     </div> |     </div> | ||||||
|   ); |   ); | ||||||
|  | |||||||
| @ -428,7 +428,9 @@ async function preloadOptions(scope, value) { | |||||||
| } | } | ||||||
| 
 | 
 | ||||||
| TextArea.ReadPretty = function ReadPretty(props): JSX.Element { | TextArea.ReadPretty = function ReadPretty(props): JSX.Element { | ||||||
|   const { value, scope } = props; |   const { value } = props; | ||||||
|  |   const scope = typeof props.scope === 'function' ? props.scope() : props.scope; | ||||||
|  | 
 | ||||||
|   const [options, setOptions] = useState([]); |   const [options, setOptions] = useState([]); | ||||||
|   const keyLabelMap = useMemo(() => createOptionsValueLabelMap(options), [options]); |   const keyLabelMap = useMemo(() => createOptionsValueLabelMap(options), [options]); | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -5,7 +5,13 @@ import { useTranslation } from 'react-i18next'; | |||||||
| import { useToken } from '../__builtins__'; | import { useToken } from '../__builtins__'; | ||||||
| import useStyles from './VariableSelect.style'; | import useStyles from './VariableSelect.style'; | ||||||
| 
 | 
 | ||||||
| export function VariableSelect({ options, setOptions, onInsert, changeOnSelect = false }): JSX.Element { | export function VariableSelect({ | ||||||
|  |   options, | ||||||
|  |   setOptions, | ||||||
|  |   onInsert, | ||||||
|  |   changeOnSelect = false, | ||||||
|  |   fieldNames = {}, | ||||||
|  | }): JSX.Element { | ||||||
|   const { t } = useTranslation(); |   const { t } = useTranslation(); | ||||||
|   const [selectedVar, setSelectedVar] = useState<string[]>([]); |   const [selectedVar, setSelectedVar] = useState<string[]>([]); | ||||||
|   const { wrapSSR, componentCls, hashId } = useStyles(); |   const { wrapSSR, componentCls, hashId } = useStyles(); | ||||||
| @ -26,6 +32,7 @@ export function VariableSelect({ options, setOptions, onInsert, changeOnSelect = | |||||||
|         placeholder={t('Select a variable')} |         placeholder={t('Select a variable')} | ||||||
|         value={[]} |         value={[]} | ||||||
|         options={options} |         options={options} | ||||||
|  |         fieldNames={fieldNames} | ||||||
|         loadData={loadData} |         loadData={loadData} | ||||||
|         onChange={(keyPaths = [], selectedOptions = []) => { |         onChange={(keyPaths = [], selectedOptions = []) => { | ||||||
|           setSelectedVar(keyPaths as string[]); |           setSelectedVar(keyPaths as string[]); | ||||||
|  | |||||||
| @ -153,25 +153,7 @@ export const CalendarFormActionInitializers = { | |||||||
|         { |         { | ||||||
|           type: 'item', |           type: 'item', | ||||||
|           title: '{{t("Custom request")}}', |           title: '{{t("Custom request")}}', | ||||||
|           component: 'CustomizeActionInitializer', |           component: 'CustomRequestInitializer', | ||||||
|           schema: { |  | ||||||
|             title: '{{ t("Custom request") }}', |  | ||||||
|             'x-component': 'Action', |  | ||||||
|             'x-action': 'customize:form:request', |  | ||||||
|             'x-designer': 'Action.Designer', |  | ||||||
|             'x-action-settings': { |  | ||||||
|               requestSettings: {}, |  | ||||||
|               skipValidator: false, |  | ||||||
|               onSuccess: { |  | ||||||
|                 manualClose: false, |  | ||||||
|                 redirecting: false, |  | ||||||
|                 successMessage: '{{t("Request success")}}', |  | ||||||
|               }, |  | ||||||
|             }, |  | ||||||
|             'x-component-props': { |  | ||||||
|               useProps: '{{ useCustomizeRequestActionProps }}', |  | ||||||
|             }, |  | ||||||
|           }, |  | ||||||
|           visible: function useVisible() { |           visible: function useVisible() { | ||||||
|             const collection = useCollection(); |             const collection = useCollection(); | ||||||
|             return (collection.template !== 'view' || collection?.writableView) && collection.template !== 'sql'; |             return (collection.template !== 'view' || collection?.writableView) && collection.template !== 'sql'; | ||||||
|  | |||||||
| @ -134,25 +134,7 @@ export const FormActionInitializers = { | |||||||
|         { |         { | ||||||
|           type: 'item', |           type: 'item', | ||||||
|           title: '{{t("Custom request")}}', |           title: '{{t("Custom request")}}', | ||||||
|           component: 'CustomizeActionInitializer', |           component: 'CustomRequestInitializer', | ||||||
|           schema: { |  | ||||||
|             title: '{{ t("Custom request") }}', |  | ||||||
|             'x-component': 'Action', |  | ||||||
|             'x-action': 'customize:form:request', |  | ||||||
|             'x-designer': 'Action.Designer', |  | ||||||
|             'x-action-settings': { |  | ||||||
|               requestSettings: {}, |  | ||||||
|               skipValidator: false, |  | ||||||
|               onSuccess: { |  | ||||||
|                 manualClose: false, |  | ||||||
|                 redirecting: false, |  | ||||||
|                 successMessage: '{{t("Request success")}}', |  | ||||||
|               }, |  | ||||||
|             }, |  | ||||||
|             'x-component-props': { |  | ||||||
|               useProps: '{{ useCustomizeRequestActionProps }}', |  | ||||||
|             }, |  | ||||||
|           }, |  | ||||||
|         }, |         }, | ||||||
|       ], |       ], | ||||||
|     }, |     }, | ||||||
| @ -268,25 +250,7 @@ export const CreateFormActionInitializers = { | |||||||
|         { |         { | ||||||
|           type: 'item', |           type: 'item', | ||||||
|           title: '{{t("Custom request")}}', |           title: '{{t("Custom request")}}', | ||||||
|           component: 'CustomizeActionInitializer', |           component: 'CustomRequestInitializer', | ||||||
|           schema: { |  | ||||||
|             title: '{{ t("Custom request") }}', |  | ||||||
|             'x-component': 'Action', |  | ||||||
|             'x-action': 'customize:form:request', |  | ||||||
|             'x-designer': 'Action.Designer', |  | ||||||
|             'x-action-settings': { |  | ||||||
|               requestSettings: {}, |  | ||||||
|               skipValidator: false, |  | ||||||
|               onSuccess: { |  | ||||||
|                 manualClose: false, |  | ||||||
|                 redirecting: false, |  | ||||||
|                 successMessage: '{{t("Request success")}}', |  | ||||||
|               }, |  | ||||||
|             }, |  | ||||||
|             'x-component-props': { |  | ||||||
|               useProps: '{{ useCustomizeRequestActionProps }}', |  | ||||||
|             }, |  | ||||||
|           }, |  | ||||||
|         }, |         }, | ||||||
|       ], |       ], | ||||||
|     }, |     }, | ||||||
| @ -401,25 +365,7 @@ export const UpdateFormActionInitializers = { | |||||||
|         { |         { | ||||||
|           type: 'item', |           type: 'item', | ||||||
|           title: '{{t("Custom request")}}', |           title: '{{t("Custom request")}}', | ||||||
|           component: 'CustomizeActionInitializer', |           component: 'CustomRequestInitializer', | ||||||
|           schema: { |  | ||||||
|             title: '{{ t("Custom request") }}', |  | ||||||
|             'x-component': 'Action', |  | ||||||
|             'x-action': 'customize:form:request', |  | ||||||
|             'x-designer': 'Action.Designer', |  | ||||||
|             'x-action-settings': { |  | ||||||
|               requestSettings: {}, |  | ||||||
|               skipValidator: false, |  | ||||||
|               onSuccess: { |  | ||||||
|                 manualClose: false, |  | ||||||
|                 redirecting: false, |  | ||||||
|                 successMessage: '{{t("Submitted successfully")}}', |  | ||||||
|               }, |  | ||||||
|             }, |  | ||||||
|             'x-component-props': { |  | ||||||
|               useProps: '{{ useCustomizeRequestActionProps }}', |  | ||||||
|             }, |  | ||||||
|           }, |  | ||||||
|         }, |         }, | ||||||
|       ], |       ], | ||||||
|     }, |     }, | ||||||
| @ -532,25 +478,7 @@ export const BulkEditFormActionInitializers = { | |||||||
|         { |         { | ||||||
|           type: 'item', |           type: 'item', | ||||||
|           title: '{{t("Custom request")}}', |           title: '{{t("Custom request")}}', | ||||||
|           component: 'CustomizeActionInitializer', |           component: 'CustomRequestInitializer', | ||||||
|           schema: { |  | ||||||
|             title: '{{ t("Custom request") }}', |  | ||||||
|             'x-component': 'Action', |  | ||||||
|             'x-action': 'customize:form:request', |  | ||||||
|             'x-designer': 'Action.Designer', |  | ||||||
|             'x-action-settings': { |  | ||||||
|               requestSettings: {}, |  | ||||||
|               skipValidator: false, |  | ||||||
|               onSuccess: { |  | ||||||
|                 manualClose: false, |  | ||||||
|                 redirecting: false, |  | ||||||
|                 successMessage: '{{t("Request success")}}', |  | ||||||
|               }, |  | ||||||
|             }, |  | ||||||
|             'x-component-props': { |  | ||||||
|               useProps: '{{ useCustomizeRequestActionProps }}', |  | ||||||
|             }, |  | ||||||
|           }, |  | ||||||
|         }, |         }, | ||||||
|       ], |       ], | ||||||
|     }, |     }, | ||||||
|  | |||||||
| @ -279,23 +279,9 @@ export const GridCardItemActionInitializers = { | |||||||
|         { |         { | ||||||
|           type: 'item', |           type: 'item', | ||||||
|           title: '{{t("Custom request")}}', |           title: '{{t("Custom request")}}', | ||||||
|           component: 'CustomizeActionInitializer', |           component: 'CustomRequestInitializer', | ||||||
|           schema: { |           schema: { | ||||||
|             title: '{{ t("Custom request") }}', |  | ||||||
|             'x-component': 'Action.Link', |  | ||||||
|             'x-action': 'customize:table:request', |             'x-action': 'customize:table:request', | ||||||
|             'x-designer': 'Action.Designer', |  | ||||||
|             'x-action-settings': { |  | ||||||
|               requestSettings: {}, |  | ||||||
|               onSuccess: { |  | ||||||
|                 manualClose: false, |  | ||||||
|                 redirecting: false, |  | ||||||
|                 successMessage: '{{t("Request success")}}', |  | ||||||
|               }, |  | ||||||
|             }, |  | ||||||
|             'x-component-props': { |  | ||||||
|               useProps: '{{ useCustomizeRequestActionProps }}', |  | ||||||
|             }, |  | ||||||
|           }, |           }, | ||||||
|           visible: function useVisible() { |           visible: function useVisible() { | ||||||
|             const collection = useCollection(); |             const collection = useCollection(); | ||||||
|  | |||||||
| @ -283,23 +283,9 @@ export const ListItemActionInitializers = { | |||||||
|         { |         { | ||||||
|           type: 'item', |           type: 'item', | ||||||
|           title: '{{t("Custom request")}}', |           title: '{{t("Custom request")}}', | ||||||
|           component: 'CustomizeActionInitializer', |           component: 'CustomRequestInitializer', | ||||||
|           schema: { |           schema: { | ||||||
|             title: '{{ t("Custom request") }}', |  | ||||||
|             'x-component': 'Action.Link', |  | ||||||
|             'x-action': 'customize:table:request', |             'x-action': 'customize:table:request', | ||||||
|             'x-designer': 'Action.Designer', |  | ||||||
|             'x-action-settings': { |  | ||||||
|               requestSettings: {}, |  | ||||||
|               onSuccess: { |  | ||||||
|                 manualClose: false, |  | ||||||
|                 redirecting: false, |  | ||||||
|                 successMessage: '{{t("Request success")}}', |  | ||||||
|               }, |  | ||||||
|             }, |  | ||||||
|             'x-component-props': { |  | ||||||
|               useProps: '{{ useCustomizeRequestActionProps }}', |  | ||||||
|             }, |  | ||||||
|           }, |           }, | ||||||
|           visible: function useVisible() { |           visible: function useVisible() { | ||||||
|             const collection = useCollection(); |             const collection = useCollection(); | ||||||
|  | |||||||
| @ -150,25 +150,7 @@ export const ReadPrettyFormActionInitializers = { | |||||||
|         { |         { | ||||||
|           type: 'item', |           type: 'item', | ||||||
|           title: '{{t("Custom request")}}', |           title: '{{t("Custom request")}}', | ||||||
|           component: 'CustomizeActionInitializer', |           component: 'CustomRequestInitializer', | ||||||
|           schema: { |  | ||||||
|             title: '{{ t("Custom request") }}', |  | ||||||
|             'x-component': 'Action', |  | ||||||
|             'x-action': 'customize:form:request', |  | ||||||
|             'x-designer': 'Action.Designer', |  | ||||||
|             'x-action-settings': { |  | ||||||
|               requestSettings: {}, |  | ||||||
|               skipValidator: false, |  | ||||||
|               onSuccess: { |  | ||||||
|                 manualClose: false, |  | ||||||
|                 redirecting: false, |  | ||||||
|                 successMessage: '{{t("Request success")}}', |  | ||||||
|               }, |  | ||||||
|             }, |  | ||||||
|             'x-component-props': { |  | ||||||
|               useProps: '{{ useCustomizeRequestActionProps }}', |  | ||||||
|             }, |  | ||||||
|           }, |  | ||||||
|           visible: useVisibleCollection, |           visible: useVisibleCollection, | ||||||
|         }, |         }, | ||||||
|       ], |       ], | ||||||
|  | |||||||
| @ -231,23 +231,9 @@ export const TableActionColumnInitializers = (props: any) => { | |||||||
|             { |             { | ||||||
|               type: 'item', |               type: 'item', | ||||||
|               title: '{{t("Custom request")}}', |               title: '{{t("Custom request")}}', | ||||||
|               component: 'CustomizeActionInitializer', |               component: 'CustomRequestInitializer', | ||||||
|               schema: { |               schema: { | ||||||
|                 title: '{{ t("Custom request") }}', |  | ||||||
|                 'x-component': 'Action.Link', |  | ||||||
|                 'x-action': 'customize:table:request', |                 'x-action': 'customize:table:request', | ||||||
|                 'x-designer': 'Action.Designer', |  | ||||||
|                 'x-action-settings': { |  | ||||||
|                   requestSettings: {}, |  | ||||||
|                   onSuccess: { |  | ||||||
|                     manualClose: false, |  | ||||||
|                     redirecting: false, |  | ||||||
|                     successMessage: '{{t("Request success")}}', |  | ||||||
|                   }, |  | ||||||
|                 }, |  | ||||||
|                 'x-component-props': { |  | ||||||
|                   useProps: '{{ useCustomizeRequestActionProps }}', |  | ||||||
|                 }, |  | ||||||
|               }, |               }, | ||||||
|               visible: () => { |               visible: () => { | ||||||
|                 return (collection.template !== 'view' || collection?.writableView) && collection.template !== 'sql'; |                 return (collection.template !== 'view' || collection?.writableView) && collection.template !== 'sql'; | ||||||
|  | |||||||
| @ -25,6 +25,7 @@ import React, { | |||||||
|   createContext, |   createContext, | ||||||
|   useCallback, |   useCallback, | ||||||
|   useContext, |   useContext, | ||||||
|  |   useEffect, | ||||||
|   useMemo, |   useMemo, | ||||||
|   // @ts-ignore
 |   // @ts-ignore
 | ||||||
|   useTransition as useReactTransition, |   useTransition as useReactTransition, | ||||||
| @ -559,7 +560,7 @@ SchemaSettings.Remove = function Remove(props: any) { | |||||||
|           title: t('Delete block'), |           title: t('Delete block'), | ||||||
|           content: t('Are you sure you want to delete it?'), |           content: t('Are you sure you want to delete it?'), | ||||||
|           ...confirm, |           ...confirm, | ||||||
|           onOk() { |           async onOk() { | ||||||
|             const options = { |             const options = { | ||||||
|               removeParentsIfNoChildren, |               removeParentsIfNoChildren, | ||||||
|               breakRemoveOn, |               breakRemoveOn, | ||||||
| @ -569,10 +570,11 @@ SchemaSettings.Remove = function Remove(props: any) { | |||||||
|               fieldSchema['required'] = false; |               fieldSchema['required'] = false; | ||||||
|             } |             } | ||||||
|             if (template && ctx?.dn) { |             if (template && ctx?.dn) { | ||||||
|               ctx?.dn.remove(null, options); |               await ctx?.dn.remove(null, options); | ||||||
|             } else { |             } else { | ||||||
|               dn.remove(null, options); |               await dn.remove(null, options); | ||||||
|             } |             } | ||||||
|  |             await confirm?.onOk?.(); | ||||||
|             delete form.values[fieldSchema.name]; |             delete form.values[fieldSchema.name]; | ||||||
|             removeActiveFieldName?.(fieldSchema.name as string); |             removeActiveFieldName?.(fieldSchema.name as string); | ||||||
|             if (field?.setInitialValue && field?.reset) { |             if (field?.setInitialValue && field?.reset) { | ||||||
| @ -834,7 +836,7 @@ SchemaSettings.PopupItem = function PopupItem(props) { | |||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| SchemaSettings.ActionModalItem = React.memo((props: any) => { | SchemaSettings.ActionModalItem = React.memo((props: any) => { | ||||||
|   const { title, onSubmit, initialValues, initialSchema, schema, modalTip, components, ...others } = props; |   const { title, onSubmit, initialValues, initialSchema, schema, modalTip, components, scope, ...others } = props; | ||||||
|   const [visible, setVisible] = useState(false); |   const [visible, setVisible] = useState(false); | ||||||
|   const [schemaUid, setSchemaUid] = useState<string>(props.uid); |   const [schemaUid, setSchemaUid] = useState<string>(props.uid); | ||||||
|   const { t } = useTranslation(); |   const { t } = useTranslation(); | ||||||
| @ -854,9 +856,14 @@ SchemaSettings.ActionModalItem = React.memo((props: any) => { | |||||||
|     [initialValues], |     [initialValues], | ||||||
|   ); |   ); | ||||||
| 
 | 
 | ||||||
|  |   useEffect(() => { | ||||||
|  |     form.setInitialValues(cloneDeep(initialValues)); | ||||||
|  |   }, [JSON.stringify(initialValues || {})]); | ||||||
|  | 
 | ||||||
|   const cancelHandler = useCallback(() => { |   const cancelHandler = useCallback(() => { | ||||||
|     setVisible(false); |     setVisible(false); | ||||||
|   }, []); |     form.reset(); | ||||||
|  |   }, [form]); | ||||||
| 
 | 
 | ||||||
|   const submitHandler = useCallback(async () => { |   const submitHandler = useCallback(async () => { | ||||||
|     await form.submit(); |     await form.submit(); | ||||||
| @ -871,6 +878,10 @@ SchemaSettings.ActionModalItem = React.memo((props: any) => { | |||||||
|       await api.resource('uiSchemas').insert({ values: initialSchema }); |       await api.resource('uiSchemas').insert({ values: initialSchema }); | ||||||
|       setSchemaUid(initialSchema['x-uid']); |       setSchemaUid(initialSchema['x-uid']); | ||||||
|     } |     } | ||||||
|  |     if (typeof others?.beforeOpen === 'function') { | ||||||
|  |       others?.beforeOpen?.(); | ||||||
|  |     } | ||||||
|  |     ctx.setVisible(false); | ||||||
|     setVisible(true); |     setVisible(true); | ||||||
|   }, [api, ctx, dn, fieldSchema, initialSchema, schemaUid]); |   }, [api, ctx, dn, fieldSchema, initialSchema, schemaUid]); | ||||||
| 
 | 
 | ||||||
| @ -884,6 +895,10 @@ SchemaSettings.ActionModalItem = React.memo((props: any) => { | |||||||
|         <Modal |         <Modal | ||||||
|           width={'50%'} |           width={'50%'} | ||||||
|           title={compile(title)} |           title={compile(title)} | ||||||
|  |           bodyStyle={{ | ||||||
|  |             maxHeight: '70vh', | ||||||
|  |             overflowY: 'scroll', | ||||||
|  |           }} | ||||||
|           {...others} |           {...others} | ||||||
|           destroyOnClose |           destroyOnClose | ||||||
|           open={visible} |           open={visible} | ||||||
| @ -902,8 +917,10 @@ SchemaSettings.ActionModalItem = React.memo((props: any) => { | |||||||
|               <FormLayout layout={'vertical'}> |               <FormLayout layout={'vertical'}> | ||||||
|                 {modalTip && <Alert message={modalTip} />} |                 {modalTip && <Alert message={modalTip} />} | ||||||
|                 {modalTip && <br />} |                 {modalTip && <br />} | ||||||
|                 {visible && schemaUid && <RemoteSchemaComponent noForm components={components} uid={schemaUid} />} |                 {visible && schemaUid && ( | ||||||
|                 {visible && schema && <SchemaComponent components={components} schema={schema} />} |                   <RemoteSchemaComponent noForm components={components} scope={scope} uid={schemaUid} /> | ||||||
|  |                 )} | ||||||
|  |                 {visible && schema && <SchemaComponent components={components} scope={scope} schema={schema} />} | ||||||
|               </FormLayout> |               </FormLayout> | ||||||
|             </FormProvider> |             </FormProvider> | ||||||
|           </FormActiveFieldsProvider> |           </FormActiveFieldsProvider> | ||||||
| @ -1137,8 +1154,7 @@ SchemaSettings.LinkageRules = function LinkageRules(props) { | |||||||
|   const variables = useVariables(); |   const variables = useVariables(); | ||||||
|   const localVariables = useLocalVariables(); |   const localVariables = useLocalVariables(); | ||||||
|   const record = useRecord(); |   const record = useRecord(); | ||||||
| 
 |   const type = props?.type || ['Action', 'Action.Link'].includes(fieldSchema['x-component']) ? 'button' : 'field'; | ||||||
|   const type = ['Action', 'Action.Link'].includes(fieldSchema['x-component']) ? 'button' : 'field'; |  | ||||||
|   const gridSchema = findGridSchema(fieldSchema) || fieldSchema; |   const gridSchema = findGridSchema(fieldSchema) || fieldSchema; | ||||||
|   const schema = useMemo<ISchema>( |   const schema = useMemo<ISchema>( | ||||||
|     () => ({ |     () => ({ | ||||||
|  | |||||||
| @ -108,7 +108,7 @@ export interface CollectionContext { | |||||||
| 
 | 
 | ||||||
| export class Collection< | export class Collection< | ||||||
|   TModelAttributes extends {} = any, |   TModelAttributes extends {} = any, | ||||||
|   TCreationAttributes extends {} = TModelAttributes |   TCreationAttributes extends {} = TModelAttributes, | ||||||
| > extends EventEmitter { | > extends EventEmitter { | ||||||
|   options: CollectionOptions; |   options: CollectionOptions; | ||||||
|   context: CollectionContext; |   context: CollectionContext; | ||||||
|  | |||||||
| @ -3,7 +3,7 @@ | |||||||
| //
 | //
 | ||||||
| // Created by Curran Kelleher and Chrostophe Serafin.
 | // Created by Curran Kelleher and Chrostophe Serafin.
 | ||||||
| // Contributions from Paul Brewer and Javier Blanco Martinez.
 | // Contributions from Paul Brewer and Javier Blanco Martinez.
 | ||||||
| import objectPath from 'object-path'; | import { get } from 'lodash'; | ||||||
| import dedupe from 'dedupe'; | import dedupe from 'dedupe'; | ||||||
| 
 | 
 | ||||||
| // An enhanced version of `typeof` that handles arrays and dates as well.
 | // An enhanced version of `typeof` that handles arrays and dates as well.
 | ||||||
| @ -91,7 +91,7 @@ const parseString = (() => { | |||||||
|         context = context || {}; |         context = context || {}; | ||||||
|         return matches.reduce((result, match, i) => { |         return matches.reduce((result, match, i) => { | ||||||
|           const parameter = parameters[i]; |           const parameter = parameters[i]; | ||||||
|           let value = objectPath.get(context, parameter.key); |           let value = get(context, parameter.key); | ||||||
| 
 | 
 | ||||||
|           if (typeof value === 'undefined') { |           if (typeof value === 'undefined') { | ||||||
|             value = parameter.defaultValue; |             value = parameter.defaultValue; | ||||||
|  | |||||||
| @ -0,0 +1,2 @@ | |||||||
|  | /node_modules | ||||||
|  | /src | ||||||
							
								
								
									
										2
									
								
								packages/plugins/@nocobase/plugin-custom-request/client.d.ts
									
									
									
									
										vendored
									
									
										Executable file
									
								
							
							
						
						
									
										2
									
								
								packages/plugins/@nocobase/plugin-custom-request/client.d.ts
									
									
									
									
										vendored
									
									
										Executable file
									
								
							| @ -0,0 +1,2 @@ | |||||||
|  | export * from './dist/client'; | ||||||
|  | export { default } from './dist/client'; | ||||||
							
								
								
									
										1
									
								
								packages/plugins/@nocobase/plugin-custom-request/client.js
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										1
									
								
								packages/plugins/@nocobase/plugin-custom-request/client.js
									
									
									
									
									
										Executable file
									
								
							| @ -0,0 +1 @@ | |||||||
|  | module.exports = require('./dist/client/index.js'); | ||||||
| @ -0,0 +1,22 @@ | |||||||
|  | { | ||||||
|  |   "name": "@nocobase/plugin-custom-request", | ||||||
|  |   "version": "0.14.0-alpha.7", | ||||||
|  |   "main": "dist/server/index.js", | ||||||
|  |   "displayName": "Custom request", | ||||||
|  |   "displayName.zh-CN": "自定义请求", | ||||||
|  |   "description": "Custom request action", | ||||||
|  |   "description.zh-CN": "自定义请求操作", | ||||||
|  |   "devDependencies": { | ||||||
|  |     "@formily/react": "2.x", | ||||||
|  |     "@formily/shared": "2.x", | ||||||
|  |     "react-i18next": "^11.15.1", | ||||||
|  |     "react-router-dom": "6.x", | ||||||
|  |     "antd": "5.x", | ||||||
|  |     "lodash": "4.x" | ||||||
|  |   }, | ||||||
|  |   "peerDependencies": { | ||||||
|  |     "@nocobase/client": "0.x", | ||||||
|  |     "@nocobase/server": "0.x", | ||||||
|  |     "@nocobase/test": "0.x" | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										2
									
								
								packages/plugins/@nocobase/plugin-custom-request/server.d.ts
									
									
									
									
										vendored
									
									
										Executable file
									
								
							
							
						
						
									
										2
									
								
								packages/plugins/@nocobase/plugin-custom-request/server.d.ts
									
									
									
									
										vendored
									
									
										Executable file
									
								
							| @ -0,0 +1,2 @@ | |||||||
|  | export * from './dist/server'; | ||||||
|  | export { default } from './dist/server'; | ||||||
							
								
								
									
										1
									
								
								packages/plugins/@nocobase/plugin-custom-request/server.js
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										1
									
								
								packages/plugins/@nocobase/plugin-custom-request/server.js
									
									
									
									
									
										Executable file
									
								
							| @ -0,0 +1 @@ | |||||||
|  | module.exports = require('./dist/server/index.js'); | ||||||
| @ -0,0 +1,41 @@ | |||||||
|  | import { Action, useAPIClient, useRequest } from '@nocobase/client'; | ||||||
|  | import React from 'react'; | ||||||
|  | import { CustomRequestActionDesigner } from './CustomRequestActionDesigner'; | ||||||
|  | import { useFieldSchema } from '@formily/react'; | ||||||
|  | import { listByCurrentRoleUrl } from '../constants'; | ||||||
|  | import { useCustomizeRequestActionProps } from '../hooks'; | ||||||
|  | 
 | ||||||
|  | export const CustomRequestActionACLDecorator = (props) => { | ||||||
|  |   const apiClient = useAPIClient(); | ||||||
|  |   const isRoot = apiClient.auth.role === 'root'; | ||||||
|  |   const fieldSchema = useFieldSchema(); | ||||||
|  |   const { data } = useRequest<{ data: string[] }>( | ||||||
|  |     { | ||||||
|  |       url: listByCurrentRoleUrl, | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       manual: isRoot, | ||||||
|  |       cacheKey: listByCurrentRoleUrl, | ||||||
|  |     }, | ||||||
|  |   ); | ||||||
|  | 
 | ||||||
|  |   if (!isRoot && !data?.data?.includes(fieldSchema?.['x-uid'])) { | ||||||
|  |     return null; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   return props.children; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | const components = { | ||||||
|  |   'customize:table:request': Action.Link, | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | export const CustomRequestAction = (props) => { | ||||||
|  |   const fieldSchema = useFieldSchema(); | ||||||
|  |   const xAction = fieldSchema['x-action']; | ||||||
|  |   const Component = components[xAction] || Action; | ||||||
|  |   return <Component {...props} useProps={useCustomizeRequestActionProps}></Component>; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | CustomRequestAction.Designer = CustomRequestActionDesigner; | ||||||
|  | CustomRequestAction.Decorator = CustomRequestActionACLDecorator; | ||||||
| @ -0,0 +1,122 @@ | |||||||
|  | import { useFieldSchema } from '@formily/react'; | ||||||
|  | import { ArrayItems } from '@formily/antd-v5'; | ||||||
|  | import { Action, SchemaSettings, useCollection, useCurrentRoles, useRequest } from '@nocobase/client'; | ||||||
|  | import React, { useEffect } from 'react'; | ||||||
|  | import { CustomRequestACLSchema, CustomRequestConfigurationFieldsSchema } from '../schemas'; | ||||||
|  | import { useCustomRequestVariableOptions, useGetCustomRequest } from '../hooks'; | ||||||
|  | import { App } from 'antd'; | ||||||
|  | import { useTranslation } from '../locale'; | ||||||
|  | import { listByCurrentRoleUrl } from '../constants'; | ||||||
|  | import { useCustomRequestsResource } from '../hooks/useCustomRequestsResource'; | ||||||
|  | 
 | ||||||
|  | function CustomRequestSettingsItem() { | ||||||
|  |   const { t } = useTranslation(); | ||||||
|  |   const { name } = useCollection(); | ||||||
|  |   const fieldSchema = useFieldSchema(); | ||||||
|  |   const customRequestsResource = useCustomRequestsResource(); | ||||||
|  |   const { message } = App.useApp(); | ||||||
|  |   const { data, refresh } = useGetCustomRequest(); | ||||||
|  | 
 | ||||||
|  |   return ( | ||||||
|  |     <> | ||||||
|  |       <SchemaSettings.ActionModalItem | ||||||
|  |         title={t('Request settings')} | ||||||
|  |         components={{ | ||||||
|  |           ArrayItems, | ||||||
|  |         }} | ||||||
|  |         beforeOpen={() => !data && refresh()} | ||||||
|  |         scope={{ useCustomRequestVariableOptions }} | ||||||
|  |         schema={CustomRequestConfigurationFieldsSchema} | ||||||
|  |         initialValues={{ | ||||||
|  |           ...data?.data?.options, | ||||||
|  |         }} | ||||||
|  |         onSubmit={async (config) => { | ||||||
|  |           const { ...requestSettings } = config; | ||||||
|  |           await customRequestsResource.updateOrCreate({ | ||||||
|  |             values: { | ||||||
|  |               key: fieldSchema['x-uid'], | ||||||
|  |               options: { | ||||||
|  |                 ...requestSettings, | ||||||
|  |                 collectionName: name, | ||||||
|  |               }, | ||||||
|  |             }, | ||||||
|  |             filterKeys: ['key'], | ||||||
|  |           }); | ||||||
|  |           refresh(); | ||||||
|  |           return message.success(t('Saved successfully')); | ||||||
|  |         }} | ||||||
|  |       /> | ||||||
|  |     </> | ||||||
|  |   ); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function CustomRequestACL() { | ||||||
|  |   const { t } = useTranslation(); | ||||||
|  |   const fieldSchema = useFieldSchema(); | ||||||
|  |   const customRequestsResource = useCustomRequestsResource(); | ||||||
|  |   const { message } = App.useApp(); | ||||||
|  |   const { data, refresh } = useGetCustomRequest(); | ||||||
|  |   const { refresh: refreshRoleCustomKeys } = useRequest<{ data: string[] }>( | ||||||
|  |     { | ||||||
|  |       url: listByCurrentRoleUrl, | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       manual: true, | ||||||
|  |       cacheKey: listByCurrentRoleUrl, | ||||||
|  |     }, | ||||||
|  |   ); | ||||||
|  | 
 | ||||||
|  |   const currentRoles = useCurrentRoles(); | ||||||
|  | 
 | ||||||
|  |   return ( | ||||||
|  |     <> | ||||||
|  |       <SchemaSettings.ActionModalItem | ||||||
|  |         title={t('Access Control')} | ||||||
|  |         schema={CustomRequestACLSchema} | ||||||
|  |         scope={{ currentRoles }} | ||||||
|  |         initialValues={{ | ||||||
|  |           roles: data?.data?.roles, | ||||||
|  |         }} | ||||||
|  |         beforeOpen={() => !data && refresh()} | ||||||
|  |         onSubmit={async ({ roles }) => { | ||||||
|  |           await customRequestsResource.updateOrCreate({ | ||||||
|  |             values: { | ||||||
|  |               key: fieldSchema['x-uid'], | ||||||
|  |               roles, | ||||||
|  |             }, | ||||||
|  |             filterKeys: ['key'], | ||||||
|  |           }); | ||||||
|  |           refresh(); | ||||||
|  |           refreshRoleCustomKeys(); | ||||||
|  |           return message.success(t('Saved successfully')); | ||||||
|  |         }} | ||||||
|  |       /> | ||||||
|  |     </> | ||||||
|  |   ); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export const CustomRequestActionDesigner: React.FC = () => { | ||||||
|  |   const customRequestsResource = useCustomRequestsResource(); | ||||||
|  |   const fieldSchema = useFieldSchema(); | ||||||
|  |   return ( | ||||||
|  |     <Action.Designer | ||||||
|  |       linkageAction | ||||||
|  |       buttonEditorProps={{ | ||||||
|  |         isLink: fieldSchema['x-action'] === 'customize:table:request', | ||||||
|  |       }} | ||||||
|  |       linkageRulesProps={{ | ||||||
|  |         type: 'button', | ||||||
|  |       }} | ||||||
|  |       removeButtonProps={{ | ||||||
|  |         onConfirmOk() { | ||||||
|  |           return customRequestsResource.destroy({ | ||||||
|  |             filterByTk: fieldSchema['x-uid'], | ||||||
|  |           }); | ||||||
|  |         }, | ||||||
|  |       }} | ||||||
|  |     > | ||||||
|  |       <CustomRequestSettingsItem /> | ||||||
|  |       <CustomRequestACL /> | ||||||
|  |     </Action.Designer> | ||||||
|  |   ); | ||||||
|  | }; | ||||||
| @ -0,0 +1 @@ | |||||||
|  | export * from './CustomRequestAction'; | ||||||
| @ -0,0 +1 @@ | |||||||
|  | export const listByCurrentRoleUrl = `customRequests:listByCurrentRole`; | ||||||
| @ -0,0 +1,3 @@ | |||||||
|  | export * from './useCustomizeRequestActionProps'; | ||||||
|  | export * from './useGetCustomRequest'; | ||||||
|  | export * from './useCustomRequestVariableOptions'; | ||||||
| @ -0,0 +1,34 @@ | |||||||
|  | import { useCollection, useCollectionFilterOptions, useCompile } from '@nocobase/client'; | ||||||
|  | import { useTranslation } from '../locale'; | ||||||
|  | import { useMemo } from 'react'; | ||||||
|  | 
 | ||||||
|  | export const useCustomRequestVariableOptions = () => { | ||||||
|  |   const collection = useCollection(); | ||||||
|  |   const { t } = useTranslation(); | ||||||
|  |   const fieldsOptions = useCollectionFilterOptions(collection); | ||||||
|  |   const userFieldOptions = useCollectionFilterOptions('users'); | ||||||
|  |   const compile = useCompile(); | ||||||
|  | 
 | ||||||
|  |   const [fields, userFields] = useMemo(() => { | ||||||
|  |     return [compile(fieldsOptions), compile(userFieldOptions)]; | ||||||
|  |   }, [fieldsOptions, userFieldOptions]); | ||||||
|  |   return useMemo(() => { | ||||||
|  |     return [ | ||||||
|  |       { | ||||||
|  |         name: 'currentRecord', | ||||||
|  |         title: t('Current record'), | ||||||
|  |         children: [...fields], | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         name: 'currentUser', | ||||||
|  |         title: t('Current user'), | ||||||
|  |         children: userFields, | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         name: 'currentTime', | ||||||
|  |         title: t('Current time'), | ||||||
|  |         children: null, | ||||||
|  |       }, | ||||||
|  |     ]; | ||||||
|  |   }, [fields, userFields]); | ||||||
|  | }; | ||||||
| @ -0,0 +1,6 @@ | |||||||
|  | import { useAPIClient } from '@nocobase/client'; | ||||||
|  | 
 | ||||||
|  | export const useCustomRequestsResource = () => { | ||||||
|  |   const apiClient = useAPIClient(); | ||||||
|  |   return apiClient.resource('customRequests'); | ||||||
|  | }; | ||||||
| @ -0,0 +1,112 @@ | |||||||
|  | import { useField, useFieldSchema, useForm } from '@formily/react'; | ||||||
|  | import { | ||||||
|  |   TableFieldResource, | ||||||
|  |   getFormValues, | ||||||
|  |   useAPIClient, | ||||||
|  |   useActionContext, | ||||||
|  |   useBlockRequestContext, | ||||||
|  |   useCollection, | ||||||
|  |   useCompile, | ||||||
|  |   useFilterByTk, | ||||||
|  |   useFormActiveFields, | ||||||
|  |   useRecord, | ||||||
|  | } from '@nocobase/client'; | ||||||
|  | import { App } from 'antd'; | ||||||
|  | import { isURL } from '@nocobase/utils/client'; | ||||||
|  | import { useNavigate } from 'react-router-dom'; | ||||||
|  | import { useGetCustomRequest } from './useGetCustomRequest'; | ||||||
|  | import { useTranslation } from '../locale'; | ||||||
|  | 
 | ||||||
|  | export const useCustomizeRequestActionProps = () => { | ||||||
|  |   const apiClient = useAPIClient(); | ||||||
|  |   const navigate = useNavigate(); | ||||||
|  |   const filterByTk = useFilterByTk(); | ||||||
|  |   const actionSchema = useFieldSchema(); | ||||||
|  |   const compile = useCompile(); | ||||||
|  |   const form = useForm(); | ||||||
|  |   const { fields, getField, getPrimaryKey } = useCollection(); | ||||||
|  |   const { field, resource, __parent, service } = useBlockRequestContext(); | ||||||
|  |   const { getActiveFieldsName } = useFormActiveFields() || {}; | ||||||
|  |   const record = useRecord(); | ||||||
|  |   const fieldSchema = useFieldSchema(); | ||||||
|  |   const { data, runAsync } = useGetCustomRequest(); | ||||||
|  |   const actionField = useField(); | ||||||
|  |   const { setVisible } = useActionContext(); | ||||||
|  |   const { modal, message } = App.useApp(); | ||||||
|  |   const { t } = useTranslation(); | ||||||
|  | 
 | ||||||
|  |   return { | ||||||
|  |     async onClick() { | ||||||
|  |       const { skipValidator, onSuccess } = actionSchema?.['x-action-settings'] ?? {}; | ||||||
|  |       const options = data ? data?.data?.options : (await runAsync())?.data?.options; | ||||||
|  |       if (!options?.['url']) { | ||||||
|  |         return message.error(t('Please configure the request settings first')); | ||||||
|  |       } | ||||||
|  |       const xAction = actionSchema?.['x-action']; | ||||||
|  |       if (skipValidator !== true && xAction === 'customize:form:request') { | ||||||
|  |         await form.submit(); | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       const requestConfig = {}; | ||||||
|  |       const methods = ['POST', 'PUT', 'PATCH']; | ||||||
|  |       if (xAction === 'customize:form:request' && methods.includes(options['method'])) { | ||||||
|  |         const fieldNames = fields.map((field) => field.name); | ||||||
|  |         const values = getFormValues({ | ||||||
|  |           filterByTk, | ||||||
|  |           field, | ||||||
|  |           form, | ||||||
|  |           fieldNames, | ||||||
|  |           getField, | ||||||
|  |           resource, | ||||||
|  |           actionFields: getActiveFieldsName?.('form') || [], | ||||||
|  |         }); | ||||||
|  |         requestConfig['data'] = values; | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       actionField.data ??= {}; | ||||||
|  |       actionField.data.loading = true; | ||||||
|  |       try { | ||||||
|  |         await apiClient.request({ | ||||||
|  |           url: `/customRequests:send/${fieldSchema['x-uid']}`, | ||||||
|  |           method: 'POST', | ||||||
|  |           data: { | ||||||
|  |             requestConfig, | ||||||
|  |             currentRecord: { | ||||||
|  |               id: record[getPrimaryKey()], | ||||||
|  |               appends: service.params[0].appends, | ||||||
|  |             }, | ||||||
|  |           }, | ||||||
|  |         }); | ||||||
|  |         actionField.data.loading = false; | ||||||
|  |         if (!(resource instanceof TableFieldResource)) { | ||||||
|  |           __parent?.service?.refresh?.(); | ||||||
|  |         } | ||||||
|  |         service?.refresh?.(); | ||||||
|  |         if (xAction === 'customize:form:request') { | ||||||
|  |           setVisible?.(false); | ||||||
|  |         } | ||||||
|  |         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 { | ||||||
|  |                   navigate(onSuccess.redirectTo); | ||||||
|  |                 } | ||||||
|  |               } | ||||||
|  |             }, | ||||||
|  |           }); | ||||||
|  |         } else { | ||||||
|  |           return message.success(compile(onSuccess?.successMessage)); | ||||||
|  |         } | ||||||
|  |       } finally { | ||||||
|  |         actionField.data.loading = false; | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |   }; | ||||||
|  | }; | ||||||
| @ -0,0 +1,19 @@ | |||||||
|  | import { useFieldSchema } from '@formily/react'; | ||||||
|  | import { useRequest } from '@nocobase/client'; | ||||||
|  | 
 | ||||||
|  | export const useGetCustomRequest = () => { | ||||||
|  |   const fieldSchema = useFieldSchema(); | ||||||
|  |   const url = `customRequests:get/${fieldSchema['x-uid']}`; | ||||||
|  |   return useRequest<{ data: { options: any; title: string; roles: any[] } }>( | ||||||
|  |     { | ||||||
|  |       url, | ||||||
|  |       params: { | ||||||
|  |         appends: ['roles'], | ||||||
|  |       }, | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       manual: true, | ||||||
|  |       cacheKey: url, | ||||||
|  |     }, | ||||||
|  |   ); | ||||||
|  | }; | ||||||
| @ -0,0 +1,38 @@ | |||||||
|  | import { CollectionManagerProvider, Plugin, SchemaComponentOptions } from '@nocobase/client'; | ||||||
|  | import React from 'react'; | ||||||
|  | import { CustomRequestConfigurationFieldsSchema } from './schemas'; | ||||||
|  | import { CustomRequestAction } from './components'; | ||||||
|  | import { CustomRequestInitializer } from './initializer'; | ||||||
|  | 
 | ||||||
|  | const CustomRequestProvider: React.FC = (props) => { | ||||||
|  |   return ( | ||||||
|  |     <SchemaComponentOptions | ||||||
|  |       scope={{ | ||||||
|  |         CustomRequestConfigurationFieldsSchema, | ||||||
|  |       }} | ||||||
|  |       components={{ CustomRequestAction, CustomRequestInitializer }} | ||||||
|  |     > | ||||||
|  |       <CollectionManagerProvider>{props.children}</CollectionManagerProvider> | ||||||
|  |     </SchemaComponentOptions> | ||||||
|  |   ); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | export class CustomRequestPlugin extends Plugin { | ||||||
|  |   async afterAdd() { | ||||||
|  |     // await this.app.pm.add()
 | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   async beforeLoad() {} | ||||||
|  | 
 | ||||||
|  |   // You can get and modify the app instance here
 | ||||||
|  |   async load() { | ||||||
|  |     this.app.use(CustomRequestProvider); | ||||||
|  |     // this.app.addComponents({})
 | ||||||
|  |     // this.app.addScopes({})
 | ||||||
|  |     // this.app.addProvider()
 | ||||||
|  |     // this.app.addProviders()
 | ||||||
|  |     // this.app.router.add()
 | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export default CustomRequestPlugin; | ||||||
| @ -0,0 +1,40 @@ | |||||||
|  | import { uid } from '@formily/shared'; | ||||||
|  | import { BlockInitializer } from '@nocobase/client'; | ||||||
|  | import React from 'react'; | ||||||
|  | import { useCustomRequestsResource } from '../hooks/useCustomRequestsResource'; | ||||||
|  | 
 | ||||||
|  | export const CustomRequestInitializer: React.FC<any> = (props) => { | ||||||
|  |   const customRequestsResource = useCustomRequestsResource(); | ||||||
|  | 
 | ||||||
|  |   const schema = { | ||||||
|  |     title: '{{ t("Custom request") }}', | ||||||
|  |     'x-component': 'CustomRequestAction', | ||||||
|  |     'x-action': 'customize:form:request', | ||||||
|  |     'x-designer': 'CustomRequestAction.Designer', | ||||||
|  |     'x-decorator': 'CustomRequestAction.Decorator', | ||||||
|  |     'x-uid': uid(), | ||||||
|  |     'x-action-settings': { | ||||||
|  |       onSuccess: { | ||||||
|  |         manualClose: false, | ||||||
|  |         redirecting: false, | ||||||
|  |         successMessage: '{{t("Request success")}}', | ||||||
|  |       }, | ||||||
|  |     }, | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|  |   return ( | ||||||
|  |     <BlockInitializer | ||||||
|  |       {...props} | ||||||
|  |       insert={async (s) => { | ||||||
|  |         await customRequestsResource.updateOrCreate({ | ||||||
|  |           values: { | ||||||
|  |             key: s['x-uid'], | ||||||
|  |           }, | ||||||
|  |           filterKeys: ['key'], | ||||||
|  |         }); | ||||||
|  |         await props?.insert(s); | ||||||
|  |       }} | ||||||
|  |       schema={schema} | ||||||
|  |     /> | ||||||
|  |   ); | ||||||
|  | }; | ||||||
| @ -0,0 +1 @@ | |||||||
|  | export * from './CustomRequestInitializer'; | ||||||
| @ -0,0 +1,18 @@ | |||||||
|  | import { i18n } from '@nocobase/client'; | ||||||
|  | import { useTranslation as useT } from 'react-i18next'; | ||||||
|  | 
 | ||||||
|  | export const NAMESPACE = 'custom-request'; | ||||||
|  | 
 | ||||||
|  | export function lang(key: string) { | ||||||
|  |   return i18n.t(key, { ns: NAMESPACE }); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export function generateNTemplate(key: string) { | ||||||
|  |   return `{{t('${key}', { ns: '${NAMESPACE}', nsMode: 'fallback' })}}`; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export function useTranslation() { | ||||||
|  |   return useT(NAMESPACE, { | ||||||
|  |     nsMode: 'fallback', | ||||||
|  |   }); | ||||||
|  | } | ||||||
| @ -0,0 +1,25 @@ | |||||||
|  | import { generateNTemplate } from '../locale'; | ||||||
|  | 
 | ||||||
|  | export const CustomRequestACLSchema = { | ||||||
|  |   type: 'object', | ||||||
|  |   properties: { | ||||||
|  |     roles: { | ||||||
|  |       type: 'array', | ||||||
|  |       title: generateNTemplate('Roles'), | ||||||
|  |       'x-decorator': 'FormItem', | ||||||
|  |       'x-decorator-props': { | ||||||
|  |         tooltip: generateNTemplate('If not set, all roles can see this action'), | ||||||
|  |       }, | ||||||
|  |       'x-component': 'Select', | ||||||
|  |       'x-component-props': { | ||||||
|  |         multiple: true, | ||||||
|  |         fieldNames: { | ||||||
|  |           label: 'title', | ||||||
|  |           value: 'name', | ||||||
|  |         }, | ||||||
|  |         objectValue: true, | ||||||
|  |         options: '{{ currentRoles }}', | ||||||
|  |       }, | ||||||
|  |     }, | ||||||
|  |   }, | ||||||
|  | }; | ||||||
| @ -0,0 +1,182 @@ | |||||||
|  | import { generateNTemplate } from '../locale'; | ||||||
|  | 
 | ||||||
|  | export const CustomRequestConfigurationFieldsSchema = { | ||||||
|  |   type: 'object', | ||||||
|  |   properties: { | ||||||
|  |     method: { | ||||||
|  |       type: 'string', | ||||||
|  |       required: true, | ||||||
|  |       title: generateNTemplate('HTTP method'), | ||||||
|  |       'x-decorator-props': { | ||||||
|  |         tooltip: generateNTemplate( | ||||||
|  |           'When the HTTP method is Post, Put or Patch, and this custom request inside the form, the request body will be automatically filled in with the form data', | ||||||
|  |         ), | ||||||
|  |       }, | ||||||
|  |       'x-decorator': 'FormItem', | ||||||
|  |       'x-component': 'Select', | ||||||
|  |       'x-component-props': { | ||||||
|  |         showSearch: false, | ||||||
|  |         allowClear: false, | ||||||
|  |         className: 'auto-width', | ||||||
|  |       }, | ||||||
|  |       enum: [ | ||||||
|  |         { label: 'GET', value: 'GET' }, | ||||||
|  |         { label: 'POST', value: 'POST' }, | ||||||
|  |         { label: 'PUT', value: 'PUT' }, | ||||||
|  |         { label: 'PATCH', value: 'PATCH' }, | ||||||
|  |         { label: 'DELETE', value: 'DELETE' }, | ||||||
|  |       ], | ||||||
|  |       default: 'POST', | ||||||
|  |     }, | ||||||
|  |     url: { | ||||||
|  |       type: 'string', | ||||||
|  |       required: true, | ||||||
|  |       title: generateNTemplate('URL'), | ||||||
|  |       'x-decorator': 'FormItem', | ||||||
|  |       'x-component': 'Variable.RawTextArea', | ||||||
|  |       'x-component-props': { | ||||||
|  |         scope: '{{useCustomRequestVariableOptions}}', | ||||||
|  |         autoSize: true, | ||||||
|  |         fieldNames: { | ||||||
|  |           value: 'name', | ||||||
|  |           label: 'title', | ||||||
|  |         }, | ||||||
|  |         placeholder: 'https://www.nocobase.com', | ||||||
|  |       }, | ||||||
|  |     }, | ||||||
|  |     headers: { | ||||||
|  |       type: 'array', | ||||||
|  |       'x-component': 'ArrayItems', | ||||||
|  |       'x-decorator': 'FormItem', | ||||||
|  |       title: generateNTemplate('Headers'), | ||||||
|  |       description: generateNTemplate('"Content-Type" only support "application/json", and no need to specify'), | ||||||
|  |       items: { | ||||||
|  |         type: 'object', | ||||||
|  |         properties: { | ||||||
|  |           space: { | ||||||
|  |             type: 'void', | ||||||
|  |             'x-component': 'Space', | ||||||
|  |             properties: { | ||||||
|  |               name: { | ||||||
|  |                 type: 'string', | ||||||
|  |                 'x-decorator': 'FormItem', | ||||||
|  |                 'x-component': 'Input', | ||||||
|  |                 'x-component-props': { | ||||||
|  |                   placeholder: generateNTemplate('Name'), | ||||||
|  |                 }, | ||||||
|  |               }, | ||||||
|  |               value: { | ||||||
|  |                 type: 'string', | ||||||
|  |                 'x-decorator': 'FormItem', | ||||||
|  |                 'x-component': 'Variable.Input', | ||||||
|  |                 'x-component-props': { | ||||||
|  |                   scope: '{{useCustomRequestVariableOptions}}', | ||||||
|  |                   fieldNames: { | ||||||
|  |                     value: 'name', | ||||||
|  |                     label: 'title', | ||||||
|  |                   }, | ||||||
|  |                   useTypedConstant: true, | ||||||
|  |                 }, | ||||||
|  |               }, | ||||||
|  |               remove: { | ||||||
|  |                 type: 'void', | ||||||
|  |                 'x-decorator': 'FormItem', | ||||||
|  |                 'x-component': 'ArrayItems.Remove', | ||||||
|  |               }, | ||||||
|  |             }, | ||||||
|  |           }, | ||||||
|  |         }, | ||||||
|  |       }, | ||||||
|  |       properties: { | ||||||
|  |         add: { | ||||||
|  |           type: 'void', | ||||||
|  |           title: generateNTemplate('Add request header'), | ||||||
|  |           'x-component': 'ArrayItems.Addition', | ||||||
|  |         }, | ||||||
|  |       }, | ||||||
|  |     }, | ||||||
|  |     params: { | ||||||
|  |       type: 'array', | ||||||
|  |       'x-component': 'ArrayItems', | ||||||
|  |       'x-decorator': 'FormItem', | ||||||
|  |       title: generateNTemplate('Parameters'), | ||||||
|  |       items: { | ||||||
|  |         type: 'object', | ||||||
|  |         properties: { | ||||||
|  |           space: { | ||||||
|  |             type: 'void', | ||||||
|  |             'x-component': 'Space', | ||||||
|  |             properties: { | ||||||
|  |               name: { | ||||||
|  |                 type: 'string', | ||||||
|  |                 'x-decorator': 'FormItem', | ||||||
|  |                 'x-component': 'Input', | ||||||
|  |                 'x-component-props': { | ||||||
|  |                   placeholder: generateNTemplate('Name'), | ||||||
|  |                 }, | ||||||
|  |               }, | ||||||
|  |               value: { | ||||||
|  |                 type: 'string', | ||||||
|  |                 'x-decorator': 'FormItem', | ||||||
|  |                 'x-component': 'Variable.Input', | ||||||
|  |                 'x-component-props': { | ||||||
|  |                   scope: '{{useCustomRequestVariableOptions}}', | ||||||
|  |                   fieldNames: { | ||||||
|  |                     value: 'name', | ||||||
|  |                     label: 'title', | ||||||
|  |                   }, | ||||||
|  |                   useTypedConstant: true, | ||||||
|  |                 }, | ||||||
|  |               }, | ||||||
|  |               remove: { | ||||||
|  |                 type: 'void', | ||||||
|  |                 'x-decorator': 'FormItem', | ||||||
|  |                 'x-component': 'ArrayItems.Remove', | ||||||
|  |               }, | ||||||
|  |             }, | ||||||
|  |           }, | ||||||
|  |         }, | ||||||
|  |       }, | ||||||
|  |       properties: { | ||||||
|  |         add: { | ||||||
|  |           type: 'void', | ||||||
|  |           title: generateNTemplate('Add parameter'), | ||||||
|  |           'x-component': 'ArrayItems.Addition', | ||||||
|  |         }, | ||||||
|  |       }, | ||||||
|  |     }, | ||||||
|  |     data: { | ||||||
|  |       type: 'string', | ||||||
|  |       title: generateNTemplate('Body'), | ||||||
|  |       'x-decorator': 'FormItem', | ||||||
|  |       'x-decorator-props': {}, | ||||||
|  |       'x-component': 'Variable.JSON', | ||||||
|  |       'x-component-props': { | ||||||
|  |         scope: '{{useCustomRequestVariableOptions}}', | ||||||
|  |         fieldNames: { | ||||||
|  |           value: 'name', | ||||||
|  |           label: 'title', | ||||||
|  |         }, | ||||||
|  |         changeOnSelect: true, | ||||||
|  |         autoSize: { | ||||||
|  |           minRows: 10, | ||||||
|  |         }, | ||||||
|  |         placeholder: generateNTemplate('Input request data'), | ||||||
|  |       }, | ||||||
|  |       description: generateNTemplate('Only support standard JSON data'), | ||||||
|  |     }, | ||||||
|  |     timeout: { | ||||||
|  |       type: 'number', | ||||||
|  |       title: generateNTemplate('Timeout config'), | ||||||
|  |       'x-decorator': 'FormItem', | ||||||
|  |       'x-decorator-props': {}, | ||||||
|  |       'x-component': 'InputNumber', | ||||||
|  |       'x-component-props': { | ||||||
|  |         addonAfter: generateNTemplate('ms'), | ||||||
|  |         min: 1, | ||||||
|  |         step: 1000, | ||||||
|  |         defaultValue: 5000, | ||||||
|  |       }, | ||||||
|  |     }, | ||||||
|  |   }, | ||||||
|  | }; | ||||||
| @ -0,0 +1,2 @@ | |||||||
|  | export * from './CustomRequestConfigurationFields'; | ||||||
|  | export * from './CustomRequestACL'; | ||||||
| @ -0,0 +1,2 @@ | |||||||
|  | export * from './server'; | ||||||
|  | export { default } from './server'; | ||||||
| @ -0,0 +1,26 @@ | |||||||
|  | export default { | ||||||
|  |   'Access Control': '权限控制', | ||||||
|  |   'Custom Request': '自定义请求', | ||||||
|  |   'Request settings': '请求设置', | ||||||
|  |   'Roles': '角色', | ||||||
|  |   'If not set, all roles can see this action': '如果不设置,所有角色都可以看到这个自定义请求', | ||||||
|  |   "Title": "标题", | ||||||
|  |   'HTTP method': 'HTTP 方法', | ||||||
|  |   'URL': 'URL', | ||||||
|  |   'Headers': '请求头', | ||||||
|  |   'Parameters': '参数', | ||||||
|  |   'Add request header': '添加请求头', | ||||||
|  |   'Add parameter': '添加参数', | ||||||
|  |   'Enter description info': '输入描述信息', | ||||||
|  |   Body: '请求体', | ||||||
|  |   'Use variable': '使用变量', | ||||||
|  |   Format: '格式化', | ||||||
|  |   Insert: '插入', | ||||||
|  |   'Timeout config': '超时设置', | ||||||
|  |   ms: '毫秒', | ||||||
|  |   'Input request data': '输入请求数据', | ||||||
|  |   'Only support standard JSON data': '仅支持标准 JSON 数据', | ||||||
|  |   '"Content-Type" only support "application/json", and no need to specify': '"Content-Type" 请求头仅支持 "application/json",无需填写', | ||||||
|  |   'When the HTTP method is Post, Put or Patch, and this custom request inside the form, the request body will be automatically filled in with the form data': '当请求方法为 Post、Put 或 Patch 时,且此自定义请求在表单内,请求体将自动填充表单数据', | ||||||
|  |   'Please configure the request settings first': '请先配置请求设置', | ||||||
|  | } | ||||||
| @ -0,0 +1,58 @@ | |||||||
|  | import { Context } from '@nocobase/actions'; | ||||||
|  | import Database, { Repository } from '@nocobase/database'; | ||||||
|  | import { MockServer, mockServer, supertest } from '@nocobase/test'; | ||||||
|  | 
 | ||||||
|  | describe('actions', () => { | ||||||
|  |   let app: MockServer; | ||||||
|  |   let db: Database; | ||||||
|  |   let repo: Repository; | ||||||
|  |   let agent: ReturnType<MockServer['agent']>; | ||||||
|  |   let resource: ReturnType<ReturnType<MockServer['agent']>['resource']>; | ||||||
|  | 
 | ||||||
|  |   beforeEach(async () => { | ||||||
|  |     app = mockServer({ | ||||||
|  |       registerActions: true, | ||||||
|  |       acl: true, | ||||||
|  |       plugins: ['users', 'auth', 'acl', 'custom-request'], | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     await app.loadAndInstall({ clean: true }); | ||||||
|  |     db = app.db; | ||||||
|  |     repo = db.getRepository('customRequests'); | ||||||
|  |     agent = app.agent(); | ||||||
|  |     resource = agent.resource('customRequests'); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   describe('send', () => { | ||||||
|  |     let params; | ||||||
|  |     beforeEach(async () => { | ||||||
|  |       app.resource({ | ||||||
|  |         name: 'custom-request-test', | ||||||
|  |         actions: { | ||||||
|  |           test(ctx: Context) { | ||||||
|  |             params = ctx.action.params; | ||||||
|  |             console.log('🚀 ~ file: actions.test.ts:34 ~ test ~ params:', params); | ||||||
|  |             return 'test ok'; | ||||||
|  |           }, | ||||||
|  |         }, | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|  |     beforeEach(async () => { | ||||||
|  |       await repo.create({ | ||||||
|  |         values: { | ||||||
|  |           key: 'test', | ||||||
|  |           options: { | ||||||
|  |             url: 'http://localhost:13000/api/custom-request-test:test', | ||||||
|  |             method: 'GET', | ||||||
|  |           }, | ||||||
|  |         }, | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|  |     test('basic', async () => { | ||||||
|  |       const res = await resource.send({ | ||||||
|  |         filterByTk: 'test', | ||||||
|  |       }); | ||||||
|  |       console.log(res.status); | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  | }); | ||||||
| @ -0,0 +1,23 @@ | |||||||
|  | import { Context } from '@nocobase/actions'; | ||||||
|  | 
 | ||||||
|  | export async function listByCurrentRole(ctx: Context) { | ||||||
|  |   const repo = ctx.db.getRepository('customRequests'); | ||||||
|  |   const data = await repo.find({ | ||||||
|  |     appends: ['roles'], | ||||||
|  |   }); | ||||||
|  |   const crRepo = ctx.db.getRepository('customRequestsRoles'); | ||||||
|  |   ctx.body = data | ||||||
|  |     .filter((item) => { | ||||||
|  |       return !item.roles.length; | ||||||
|  |     }) | ||||||
|  |     .map((item) => item.key) | ||||||
|  |     .concat( | ||||||
|  |       ( | ||||||
|  |         await crRepo.find({ | ||||||
|  |           filter: { | ||||||
|  |             roleName: ctx.state.currentRole, | ||||||
|  |           }, | ||||||
|  |         }) | ||||||
|  |       ).map((item) => item.customRequestKey), | ||||||
|  |     ); | ||||||
|  | } | ||||||
| @ -0,0 +1,115 @@ | |||||||
|  | import { Context, Next } from '@nocobase/actions'; | ||||||
|  | import actions from '@nocobase/actions'; | ||||||
|  | import { parse } from '@nocobase/utils'; | ||||||
|  | 
 | ||||||
|  | import axios from 'axios'; | ||||||
|  | 
 | ||||||
|  | const getHeaders = (headers: Record<string, any>) => { | ||||||
|  |   return Object.keys(headers).reduce((hds, key) => { | ||||||
|  |     if (key.toLocaleLowerCase().startsWith('x-')) { | ||||||
|  |       hds[key] = headers[key]; | ||||||
|  |     } | ||||||
|  |     return hds; | ||||||
|  |   }, {}); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | const arrayToObject = (arr: { name: string; value: string }[]) => { | ||||||
|  |   return arr.reduce((acc, cur) => { | ||||||
|  |     acc[cur.name] = cur.value; | ||||||
|  |     return acc; | ||||||
|  |   }, {}); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | const omitNullAndUndefined = (obj: any) => { | ||||||
|  |   return Object.keys(obj).reduce((acc, cur) => { | ||||||
|  |     if (obj[cur] !== null && typeof obj[cur] !== 'undefined') { | ||||||
|  |       acc[cur] = obj[cur]; | ||||||
|  |     } | ||||||
|  |     return acc; | ||||||
|  |   }, {}); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | export async function send(ctx: Context, next: Next) { | ||||||
|  |   const { filterByTk, resourceName, values = {} } = ctx.action.params; | ||||||
|  |   const { | ||||||
|  |     currentRecord: { id: currentRecordId, appends: currentRecordAppends } = { | ||||||
|  |       id: 0, | ||||||
|  |       appends: [], | ||||||
|  |     }, | ||||||
|  |     requestConfig: requestConfigFirst = {}, | ||||||
|  |   } = values; | ||||||
|  | 
 | ||||||
|  |   // root role has all permissions
 | ||||||
|  |   if (ctx.state.currentRole !== 'root') { | ||||||
|  |     const crRepo = ctx.db.getRepository('customRequestsRoles'); | ||||||
|  |     const hasRoles = await crRepo.find({ | ||||||
|  |       filter: { | ||||||
|  |         customRequestKey: filterByTk, | ||||||
|  |       }, | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     if (hasRoles.length) { | ||||||
|  |       if (!hasRoles.find((item) => item.roleName === ctx.state.currentRole)) { | ||||||
|  |         return ctx.throw(403, 'custom request no permission'); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   const repo = ctx.db.getRepository(resourceName); | ||||||
|  |   const requestConfig = await repo.findOne({ | ||||||
|  |     filter: { | ||||||
|  |       key: filterByTk, | ||||||
|  |     }, | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   if (!requestConfig) { | ||||||
|  |     ctx.throw(404, 'request config not found'); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   ctx.withoutDataWrapping = true; | ||||||
|  | 
 | ||||||
|  |   const { collectionName, url, headers = {}, params = {}, data = {}, ...options } = requestConfig.options; | ||||||
|  |   let currentRecord = {}; | ||||||
|  |   if (collectionName && typeof currentRecordId !== 'undefined') { | ||||||
|  |     const recordRepo = ctx.db.getRepository(collectionName); | ||||||
|  |     currentRecord = await recordRepo.findOne({ | ||||||
|  |       filterByTk: currentRecordId, | ||||||
|  |       appends: currentRecordAppends, | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   const variables = { | ||||||
|  |     currentRecord, | ||||||
|  |     currentUser: ctx.auth.user, | ||||||
|  |     currentTime: new Date().toISOString(), | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|  |   try { | ||||||
|  |     ctx.body = await axios({ | ||||||
|  |       baseURL: ctx.origin, | ||||||
|  |       ...options, | ||||||
|  |       url: parse(url)(variables), | ||||||
|  |       headers: { | ||||||
|  |         Authorization: 'Bearer ' + ctx.getBearerToken(), | ||||||
|  |         ...getHeaders(ctx.headers), | ||||||
|  |         ...omitNullAndUndefined(parse(arrayToObject(headers))(variables)), | ||||||
|  |       }, | ||||||
|  |       params: parse(arrayToObject(params))(variables), | ||||||
|  |       data: parse({ | ||||||
|  |         ...data, | ||||||
|  |         ...requestConfigFirst?.data, | ||||||
|  |       })(variables), | ||||||
|  |     }).then((res) => { | ||||||
|  |       return res.data; | ||||||
|  |     }); | ||||||
|  |   } catch (err: any) { | ||||||
|  |     if (axios.isAxiosError(err)) { | ||||||
|  |       ctx.status = err.response?.status || 500; | ||||||
|  |       ctx.body = err.response?.data || { message: err.message }; | ||||||
|  |     } else { | ||||||
|  |       ctx.throw(500, err?.message); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   return next(); | ||||||
|  | } | ||||||
| @ -0,0 +1,30 @@ | |||||||
|  | import { CollectionOptions } from '@nocobase/client'; | ||||||
|  | 
 | ||||||
|  | export default { | ||||||
|  |   namespace: 'custom-requests', | ||||||
|  |   duplicator: 'optional', | ||||||
|  |   name: 'customRequests', | ||||||
|  |   title: '{{t("Custom request")}}', | ||||||
|  |   fields: [ | ||||||
|  |     { | ||||||
|  |       type: 'uid', | ||||||
|  |       name: 'key', | ||||||
|  |       primaryKey: true, | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       type: 'belongsToMany', | ||||||
|  |       name: 'roles', | ||||||
|  |       onDelete: 'CASCADE', | ||||||
|  |       through: 'customRequestsRoles', | ||||||
|  |       target: 'roles', | ||||||
|  |       foreignKey: 'customRequestKey', | ||||||
|  |       otherKey: 'roleName', | ||||||
|  |       sourceKey: 'key', | ||||||
|  |       targetKey: 'name', | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       type: 'json', | ||||||
|  |       name: 'options', // 配置的请求参数都放这里
 | ||||||
|  |     }, | ||||||
|  |   ], | ||||||
|  | } as CollectionOptions; | ||||||
| @ -0,0 +1,8 @@ | |||||||
|  | import { CollectionOptions } from '@nocobase/client'; | ||||||
|  | 
 | ||||||
|  | export default { | ||||||
|  |   namespace: 'custom-requests', | ||||||
|  |   duplicator: 'optional', | ||||||
|  |   name: 'customRequestsRoles', | ||||||
|  |   title: '{{t("Custom request")}}', | ||||||
|  | } as CollectionOptions; | ||||||
| @ -0,0 +1 @@ | |||||||
|  | export { default } from './plugin'; | ||||||
| @ -0,0 +1,41 @@ | |||||||
|  | import { InstallOptions, Plugin } from '@nocobase/server'; | ||||||
|  | import { resolve } from 'path'; | ||||||
|  | import { send } from './actions/send'; | ||||||
|  | import { listByCurrentRole } from './actions/listByCurrentRole'; | ||||||
|  | 
 | ||||||
|  | export class CustomRequestPlugin extends Plugin { | ||||||
|  |   afterAdd() {} | ||||||
|  | 
 | ||||||
|  |   beforeLoad() {} | ||||||
|  | 
 | ||||||
|  |   async load() { | ||||||
|  |     await this.db.import({ | ||||||
|  |       directory: resolve(__dirname, './collections'), | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     this.app.resource({ | ||||||
|  |       name: 'customRequests', | ||||||
|  |       actions: { | ||||||
|  |         send, | ||||||
|  |         listByCurrentRole, | ||||||
|  |       }, | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     this.app.acl.registerSnippet({ | ||||||
|  |       name: `ui.${this.name}`, | ||||||
|  |       actions: ['customRequests:*'], | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     this.app.acl.allow('customRequests', ['send', 'listByCurrentRole'], 'loggedIn'); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   async install(options?: InstallOptions) {} | ||||||
|  | 
 | ||||||
|  |   async afterEnable() {} | ||||||
|  | 
 | ||||||
|  |   async afterDisable() {} | ||||||
|  | 
 | ||||||
|  |   async remove() {} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export default CustomRequestPlugin; | ||||||
| @ -44,6 +44,7 @@ | |||||||
|     "@nocobase/plugin-users": "0.14.0-alpha.7", |     "@nocobase/plugin-users": "0.14.0-alpha.7", | ||||||
|     "@nocobase/plugin-verification": "0.14.0-alpha.7", |     "@nocobase/plugin-verification": "0.14.0-alpha.7", | ||||||
|     "@nocobase/plugin-workflow": "0.14.0-alpha.7", |     "@nocobase/plugin-workflow": "0.14.0-alpha.7", | ||||||
|  |     "@nocobase/plugin-custom-request": "0.14.0-alpha.7", | ||||||
|     "@nocobase/server": "0.14.0-alpha.7", |     "@nocobase/server": "0.14.0-alpha.7", | ||||||
|     "cronstrue": "^2.11.0" |     "cronstrue": "^2.11.0" | ||||||
|   }, |   }, | ||||||
|  | |||||||
| @ -27,6 +27,7 @@ export class PresetNocoBase extends Plugin { | |||||||
|     'data-visualization', |     'data-visualization', | ||||||
|     'auth', |     'auth', | ||||||
|     'sms-auth', |     'sms-auth', | ||||||
|  |     'custom-request', | ||||||
|   ]; |   ]; | ||||||
| 
 | 
 | ||||||
|   localPlugins = [ |   localPlugins = [ | ||||||
|  | |||||||
| @ -15818,7 +15818,7 @@ lodash.union@^4.6.0: | |||||||
|   version "4.6.0" |   version "4.6.0" | ||||||
|   resolved "https://registry.npmmirror.com/lodash.union/-/lodash.union-4.6.0.tgz#48bb5088409f16f1821666641c44dd1aaae3cd88" |   resolved "https://registry.npmmirror.com/lodash.union/-/lodash.union-4.6.0.tgz#48bb5088409f16f1821666641c44dd1aaae3cd88" | ||||||
| 
 | 
 | ||||||
| lodash@4.17.21, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.7.0: | lodash@4.17.21, lodash@4.x, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.7.0: | ||||||
|   version "4.17.21" |   version "4.17.21" | ||||||
|   resolved "https://registry.npmmirror.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" |   resolved "https://registry.npmmirror.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" | ||||||
| 
 | 
 | ||||||
|  | |||||||
		Loading…
	
		Reference in New Issue
	
	Block a user