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`
|
||||
.ant-table-cell {
|
||||
> .ant-space-horizontal {
|
||||
.ant-space-item:empty:not(:last-child) + .ant-space-item-split {
|
||||
display: none;
|
||||
}
|
||||
.ant-space-item-split:has(+ .ant-space-item:last-child:empty) {
|
||||
.ant-space-item-split:has(+ .ant-space-item:empty) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
@ -66,7 +66,7 @@ const filterValue = (value) => {
|
||||
return obj;
|
||||
};
|
||||
|
||||
function getFormValues({
|
||||
export function getFormValues({
|
||||
filterByTk,
|
||||
field,
|
||||
form,
|
||||
|
@ -7,9 +7,8 @@ import { CollectionManagerContext } from '../context';
|
||||
import { CollectionFieldOptions, CollectionOptions } from '../types';
|
||||
|
||||
export const useCollectionManager = () => {
|
||||
const { refreshCM, updateCollection, service, interfaces, collections, templates } = useContext(
|
||||
CollectionManagerContext,
|
||||
);
|
||||
const { refreshCM, updateCollection, service, interfaces, collections, templates } =
|
||||
useContext(CollectionManagerContext);
|
||||
const compile = useCompile();
|
||||
const getInheritedFields = (name) => {
|
||||
const inheritKeys = getInheritCollections(name);
|
||||
|
@ -2,7 +2,7 @@ import { ArrayTable } from '@formily/antd-v5';
|
||||
import { onFieldValueChange, onFieldInputValueChange } from '@formily/core';
|
||||
import { connect, ISchema, mapProps, useField, useFieldSchema, useForm, useFormEffects } from '@formily/react';
|
||||
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 React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@ -68,12 +68,12 @@ const MenuGroup = (props) => {
|
||||
);
|
||||
};
|
||||
|
||||
function ButtonEditor() {
|
||||
function ButtonEditor(props) {
|
||||
const field = useField();
|
||||
const fieldSchema = useFieldSchema();
|
||||
const { dn } = useDesignable();
|
||||
const { t } = useTranslation();
|
||||
const isLink = fieldSchema['x-component'] === 'Action.Link';
|
||||
const isLink = props?.isLink || fieldSchema['x-component'] === 'Action.Link';
|
||||
|
||||
return (
|
||||
<SchemaSettings.ModalItem
|
||||
@ -689,7 +689,11 @@ function AfterSuccess() {
|
||||
);
|
||||
}
|
||||
|
||||
function RemoveButton() {
|
||||
function RemoveButton(
|
||||
props: {
|
||||
onConfirmOk?: ModalProps['onOk'];
|
||||
} = {},
|
||||
) {
|
||||
const { t } = useTranslation();
|
||||
const fieldSchema = useFieldSchema();
|
||||
const isDeletable = fieldSchema?.parent['x-component'] === 'CollectionField';
|
||||
@ -704,6 +708,7 @@ function RemoveButton() {
|
||||
}}
|
||||
confirm={{
|
||||
title: t('Delete action'),
|
||||
onOk: props.onConfirmOk,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
@ -890,7 +895,7 @@ function WorkflowConfig() {
|
||||
}
|
||||
|
||||
export const ActionDesigner = (props) => {
|
||||
const { modalTip, linkageAction, ...restProps } = props;
|
||||
const { modalTip, linkageAction, removeButtonProps, buttonEditorProps, linkageRulesProps, ...restProps } = props;
|
||||
const fieldSchema = useFieldSchema();
|
||||
const { name } = useCollection();
|
||||
const { getChildrenCollections } = useCollectionManager();
|
||||
@ -907,10 +912,10 @@ export const ActionDesigner = (props) => {
|
||||
return (
|
||||
<GeneralSchemaDesigner {...restProps} disableInitializer draggable={isDraggable}>
|
||||
<MenuGroup>
|
||||
<ButtonEditor />
|
||||
<ButtonEditor {...buttonEditorProps} />
|
||||
{fieldSchema['x-action'] === 'submit' &&
|
||||
fieldSchema.parent?.['x-initializer'] === 'CreateFormActionInitializers' && <SaveMode />}
|
||||
{isLinkageAction && <SchemaSettings.LinkageRules collectionName={name} />}
|
||||
{isLinkageAction && <SchemaSettings.LinkageRules {...linkageRulesProps} collectionName={name} />}
|
||||
{isDuplicateAction && <DuplicationMode />}
|
||||
<OpenModeSchemaItems openMode={isPopupAction} openSize={isPopupAction} />
|
||||
{isUpdateModePopupAction && <UpdateMode />}
|
||||
@ -919,10 +924,10 @@ export const ActionDesigner = (props) => {
|
||||
{isValid(fieldSchema?.['x-action-settings']?.skipValidator) && <SkipValidation />}
|
||||
{isValid(fieldSchema?.['x-action-settings']?.['onSuccess']) && <AfterSuccess />}
|
||||
{isValid(fieldSchema?.['x-action-settings']?.triggerWorkflows) && <WorkflowConfig />}
|
||||
|
||||
{restProps.children}
|
||||
{isChildCollectionAction && <SchemaSettings.EnableChildCollections collectionName={name} />}
|
||||
|
||||
{<RemoveButton />}
|
||||
{<RemoveButton {...removeButtonProps} />}
|
||||
</MenuGroup>
|
||||
</GeneralSchemaDesigner>
|
||||
);
|
||||
|
@ -23,6 +23,7 @@ import { ErrorFallback } from '../error-fallback';
|
||||
import FixedBlock from './FixedBlock';
|
||||
import { PageDesigner, PageTabDesigner } from './PageTabDesigner';
|
||||
import { useStyles } from './style';
|
||||
import { useStyles as useAClStyles } from '../../../acl/style';
|
||||
|
||||
export const Page = (props) => {
|
||||
const { children, ...others } = props;
|
||||
@ -58,6 +59,7 @@ export const Page = (props) => {
|
||||
);
|
||||
const [height, setHeight] = useState(0);
|
||||
const { wrapSSR, hashId, componentCls } = useStyles();
|
||||
const aclStyles = useAClStyles();
|
||||
|
||||
const handleErrors = (error) => {
|
||||
console.error(error);
|
||||
@ -66,7 +68,7 @@ export const Page = (props) => {
|
||||
const pageHeaderTitle = hidePageTitle ? undefined : fieldSchema.title || compile(title);
|
||||
return wrapSSR(
|
||||
<FilterBlockProvider>
|
||||
<div className={`${componentCls} ${hashId}`}>
|
||||
<div className={`${componentCls} ${hashId} ${aclStyles.styles}`}>
|
||||
<PageDesigner title={fieldSchema.title || title} />
|
||||
<div
|
||||
ref={(ref) => {
|
||||
|
@ -131,7 +131,6 @@ function getTypedConstantOption(type: string, types: true | string[], fieldNames
|
||||
export function Input(props) {
|
||||
const {
|
||||
value = '',
|
||||
scope,
|
||||
onChange,
|
||||
children,
|
||||
button,
|
||||
@ -141,6 +140,7 @@ export function Input(props) {
|
||||
changeOnSelect,
|
||||
fieldNames,
|
||||
} = props;
|
||||
const scope = typeof props.scope === 'function' ? props.scope() : props.scope;
|
||||
const { wrapSSR, hashId, componentCls, rootPrefixCls } = useStyles();
|
||||
|
||||
// 添加 antd input 样式,防止样式缺失
|
||||
|
@ -18,7 +18,8 @@ function setNativeInputValue(input, value) {
|
||||
|
||||
export function RawTextArea(props): JSX.Element {
|
||||
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) : []);
|
||||
|
||||
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>
|
||||
</div>
|
||||
);
|
||||
|
@ -428,7 +428,9 @@ async function preloadOptions(scope, value) {
|
||||
}
|
||||
|
||||
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 keyLabelMap = useMemo(() => createOptionsValueLabelMap(options), [options]);
|
||||
|
||||
|
@ -5,7 +5,13 @@ import { useTranslation } from 'react-i18next';
|
||||
import { useToken } from '../__builtins__';
|
||||
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 [selectedVar, setSelectedVar] = useState<string[]>([]);
|
||||
const { wrapSSR, componentCls, hashId } = useStyles();
|
||||
@ -26,6 +32,7 @@ export function VariableSelect({ options, setOptions, onInsert, changeOnSelect =
|
||||
placeholder={t('Select a variable')}
|
||||
value={[]}
|
||||
options={options}
|
||||
fieldNames={fieldNames}
|
||||
loadData={loadData}
|
||||
onChange={(keyPaths = [], selectedOptions = []) => {
|
||||
setSelectedVar(keyPaths as string[]);
|
||||
|
@ -153,25 +153,7 @@ export const CalendarFormActionInitializers = {
|
||||
{
|
||||
type: 'item',
|
||||
title: '{{t("Custom request")}}',
|
||||
component: 'CustomizeActionInitializer',
|
||||
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 }}',
|
||||
},
|
||||
},
|
||||
component: 'CustomRequestInitializer',
|
||||
visible: function useVisible() {
|
||||
const collection = useCollection();
|
||||
return (collection.template !== 'view' || collection?.writableView) && collection.template !== 'sql';
|
||||
|
@ -134,25 +134,7 @@ export const FormActionInitializers = {
|
||||
{
|
||||
type: 'item',
|
||||
title: '{{t("Custom request")}}',
|
||||
component: 'CustomizeActionInitializer',
|
||||
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 }}',
|
||||
},
|
||||
},
|
||||
component: 'CustomRequestInitializer',
|
||||
},
|
||||
],
|
||||
},
|
||||
@ -268,25 +250,7 @@ export const CreateFormActionInitializers = {
|
||||
{
|
||||
type: 'item',
|
||||
title: '{{t("Custom request")}}',
|
||||
component: 'CustomizeActionInitializer',
|
||||
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 }}',
|
||||
},
|
||||
},
|
||||
component: 'CustomRequestInitializer',
|
||||
},
|
||||
],
|
||||
},
|
||||
@ -401,25 +365,7 @@ export const UpdateFormActionInitializers = {
|
||||
{
|
||||
type: 'item',
|
||||
title: '{{t("Custom request")}}',
|
||||
component: 'CustomizeActionInitializer',
|
||||
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 }}',
|
||||
},
|
||||
},
|
||||
component: 'CustomRequestInitializer',
|
||||
},
|
||||
],
|
||||
},
|
||||
@ -532,25 +478,7 @@ export const BulkEditFormActionInitializers = {
|
||||
{
|
||||
type: 'item',
|
||||
title: '{{t("Custom request")}}',
|
||||
component: 'CustomizeActionInitializer',
|
||||
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 }}',
|
||||
},
|
||||
},
|
||||
component: 'CustomRequestInitializer',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
@ -279,23 +279,9 @@ export const GridCardItemActionInitializers = {
|
||||
{
|
||||
type: 'item',
|
||||
title: '{{t("Custom request")}}',
|
||||
component: 'CustomizeActionInitializer',
|
||||
component: 'CustomRequestInitializer',
|
||||
schema: {
|
||||
title: '{{ t("Custom request") }}',
|
||||
'x-component': 'Action.Link',
|
||||
'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() {
|
||||
const collection = useCollection();
|
||||
|
@ -283,23 +283,9 @@ export const ListItemActionInitializers = {
|
||||
{
|
||||
type: 'item',
|
||||
title: '{{t("Custom request")}}',
|
||||
component: 'CustomizeActionInitializer',
|
||||
component: 'CustomRequestInitializer',
|
||||
schema: {
|
||||
title: '{{ t("Custom request") }}',
|
||||
'x-component': 'Action.Link',
|
||||
'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() {
|
||||
const collection = useCollection();
|
||||
|
@ -150,25 +150,7 @@ export const ReadPrettyFormActionInitializers = {
|
||||
{
|
||||
type: 'item',
|
||||
title: '{{t("Custom request")}}',
|
||||
component: 'CustomizeActionInitializer',
|
||||
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 }}',
|
||||
},
|
||||
},
|
||||
component: 'CustomRequestInitializer',
|
||||
visible: useVisibleCollection,
|
||||
},
|
||||
],
|
||||
|
@ -231,23 +231,9 @@ export const TableActionColumnInitializers = (props: any) => {
|
||||
{
|
||||
type: 'item',
|
||||
title: '{{t("Custom request")}}',
|
||||
component: 'CustomizeActionInitializer',
|
||||
component: 'CustomRequestInitializer',
|
||||
schema: {
|
||||
title: '{{ t("Custom request") }}',
|
||||
'x-component': 'Action.Link',
|
||||
'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: () => {
|
||||
return (collection.template !== 'view' || collection?.writableView) && collection.template !== 'sql';
|
||||
|
@ -25,6 +25,7 @@ import React, {
|
||||
createContext,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
// @ts-ignore
|
||||
useTransition as useReactTransition,
|
||||
@ -559,7 +560,7 @@ SchemaSettings.Remove = function Remove(props: any) {
|
||||
title: t('Delete block'),
|
||||
content: t('Are you sure you want to delete it?'),
|
||||
...confirm,
|
||||
onOk() {
|
||||
async onOk() {
|
||||
const options = {
|
||||
removeParentsIfNoChildren,
|
||||
breakRemoveOn,
|
||||
@ -569,10 +570,11 @@ SchemaSettings.Remove = function Remove(props: any) {
|
||||
fieldSchema['required'] = false;
|
||||
}
|
||||
if (template && ctx?.dn) {
|
||||
ctx?.dn.remove(null, options);
|
||||
await ctx?.dn.remove(null, options);
|
||||
} else {
|
||||
dn.remove(null, options);
|
||||
await dn.remove(null, options);
|
||||
}
|
||||
await confirm?.onOk?.();
|
||||
delete form.values[fieldSchema.name];
|
||||
removeActiveFieldName?.(fieldSchema.name as string);
|
||||
if (field?.setInitialValue && field?.reset) {
|
||||
@ -834,7 +836,7 @@ SchemaSettings.PopupItem = function PopupItem(props) {
|
||||
};
|
||||
|
||||
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 [schemaUid, setSchemaUid] = useState<string>(props.uid);
|
||||
const { t } = useTranslation();
|
||||
@ -854,9 +856,14 @@ SchemaSettings.ActionModalItem = React.memo((props: any) => {
|
||||
[initialValues],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
form.setInitialValues(cloneDeep(initialValues));
|
||||
}, [JSON.stringify(initialValues || {})]);
|
||||
|
||||
const cancelHandler = useCallback(() => {
|
||||
setVisible(false);
|
||||
}, []);
|
||||
form.reset();
|
||||
}, [form]);
|
||||
|
||||
const submitHandler = useCallback(async () => {
|
||||
await form.submit();
|
||||
@ -871,6 +878,10 @@ SchemaSettings.ActionModalItem = React.memo((props: any) => {
|
||||
await api.resource('uiSchemas').insert({ values: initialSchema });
|
||||
setSchemaUid(initialSchema['x-uid']);
|
||||
}
|
||||
if (typeof others?.beforeOpen === 'function') {
|
||||
others?.beforeOpen?.();
|
||||
}
|
||||
ctx.setVisible(false);
|
||||
setVisible(true);
|
||||
}, [api, ctx, dn, fieldSchema, initialSchema, schemaUid]);
|
||||
|
||||
@ -884,6 +895,10 @@ SchemaSettings.ActionModalItem = React.memo((props: any) => {
|
||||
<Modal
|
||||
width={'50%'}
|
||||
title={compile(title)}
|
||||
bodyStyle={{
|
||||
maxHeight: '70vh',
|
||||
overflowY: 'scroll',
|
||||
}}
|
||||
{...others}
|
||||
destroyOnClose
|
||||
open={visible}
|
||||
@ -902,8 +917,10 @@ SchemaSettings.ActionModalItem = React.memo((props: any) => {
|
||||
<FormLayout layout={'vertical'}>
|
||||
{modalTip && <Alert message={modalTip} />}
|
||||
{modalTip && <br />}
|
||||
{visible && schemaUid && <RemoteSchemaComponent noForm components={components} uid={schemaUid} />}
|
||||
{visible && schema && <SchemaComponent components={components} schema={schema} />}
|
||||
{visible && schemaUid && (
|
||||
<RemoteSchemaComponent noForm components={components} scope={scope} uid={schemaUid} />
|
||||
)}
|
||||
{visible && schema && <SchemaComponent components={components} scope={scope} schema={schema} />}
|
||||
</FormLayout>
|
||||
</FormProvider>
|
||||
</FormActiveFieldsProvider>
|
||||
@ -1137,8 +1154,7 @@ SchemaSettings.LinkageRules = function LinkageRules(props) {
|
||||
const variables = useVariables();
|
||||
const localVariables = useLocalVariables();
|
||||
const record = useRecord();
|
||||
|
||||
const type = ['Action', 'Action.Link'].includes(fieldSchema['x-component']) ? 'button' : 'field';
|
||||
const type = props?.type || ['Action', 'Action.Link'].includes(fieldSchema['x-component']) ? 'button' : 'field';
|
||||
const gridSchema = findGridSchema(fieldSchema) || fieldSchema;
|
||||
const schema = useMemo<ISchema>(
|
||||
() => ({
|
||||
|
@ -108,7 +108,7 @@ export interface CollectionContext {
|
||||
|
||||
export class Collection<
|
||||
TModelAttributes extends {} = any,
|
||||
TCreationAttributes extends {} = TModelAttributes
|
||||
TCreationAttributes extends {} = TModelAttributes,
|
||||
> extends EventEmitter {
|
||||
options: CollectionOptions;
|
||||
context: CollectionContext;
|
||||
|
@ -3,7 +3,7 @@
|
||||
//
|
||||
// Created by Curran Kelleher and Chrostophe Serafin.
|
||||
// Contributions from Paul Brewer and Javier Blanco Martinez.
|
||||
import objectPath from 'object-path';
|
||||
import { get } from 'lodash';
|
||||
import dedupe from 'dedupe';
|
||||
|
||||
// An enhanced version of `typeof` that handles arrays and dates as well.
|
||||
@ -91,7 +91,7 @@ const parseString = (() => {
|
||||
context = context || {};
|
||||
return matches.reduce((result, match, i) => {
|
||||
const parameter = parameters[i];
|
||||
let value = objectPath.get(context, parameter.key);
|
||||
let value = get(context, parameter.key);
|
||||
|
||||
if (typeof value === 'undefined') {
|
||||
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-verification": "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",
|
||||
"cronstrue": "^2.11.0"
|
||||
},
|
||||
|
@ -27,6 +27,7 @@ export class PresetNocoBase extends Plugin {
|
||||
'data-visualization',
|
||||
'auth',
|
||||
'sms-auth',
|
||||
'custom-request',
|
||||
];
|
||||
|
||||
localPlugins = [
|
||||
|
@ -15818,7 +15818,7 @@ lodash.union@^4.6.0:
|
||||
version "4.6.0"
|
||||
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"
|
||||
resolved "https://registry.npmmirror.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user