{
diff --git a/packages/core/client/src/schema-component/antd/variable/Input.tsx b/packages/core/client/src/schema-component/antd/variable/Input.tsx
index 58bb3ab12..176e35a4f 100644
--- a/packages/core/client/src/schema-component/antd/variable/Input.tsx
+++ b/packages/core/client/src/schema-component/antd/variable/Input.tsx
@@ -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 样式,防止样式缺失
diff --git a/packages/core/client/src/schema-component/antd/variable/RawTextArea.tsx b/packages/core/client/src/schema-component/antd/variable/RawTextArea.tsx
index 83f41fc5a..6af0771b6 100644
--- a/packages/core/client/src/schema-component/antd/variable/RawTextArea.tsx
+++ b/packages/core/client/src/schema-component/antd/variable/RawTextArea.tsx
@@ -18,7 +18,8 @@ function setNativeInputValue(input, value) {
export function RawTextArea(props): JSX.Element {
const inputRef = useRef
(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 {
}
`}
>
-
+
);
diff --git a/packages/core/client/src/schema-component/antd/variable/TextArea.tsx b/packages/core/client/src/schema-component/antd/variable/TextArea.tsx
index 1f03a8007..22aa0f53c 100644
--- a/packages/core/client/src/schema-component/antd/variable/TextArea.tsx
+++ b/packages/core/client/src/schema-component/antd/variable/TextArea.tsx
@@ -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]);
diff --git a/packages/core/client/src/schema-component/antd/variable/VariableSelect.tsx b/packages/core/client/src/schema-component/antd/variable/VariableSelect.tsx
index 11854790f..536337942 100644
--- a/packages/core/client/src/schema-component/antd/variable/VariableSelect.tsx
+++ b/packages/core/client/src/schema-component/antd/variable/VariableSelect.tsx
@@ -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
([]);
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[]);
diff --git a/packages/core/client/src/schema-initializer/buttons/CalendarFormActionInitializers.tsx b/packages/core/client/src/schema-initializer/buttons/CalendarFormActionInitializers.tsx
index 311e620cc..b19bee240 100644
--- a/packages/core/client/src/schema-initializer/buttons/CalendarFormActionInitializers.tsx
+++ b/packages/core/client/src/schema-initializer/buttons/CalendarFormActionInitializers.tsx
@@ -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';
diff --git a/packages/core/client/src/schema-initializer/buttons/FormActionInitializers.tsx b/packages/core/client/src/schema-initializer/buttons/FormActionInitializers.tsx
index 86a17865f..48f18f846 100644
--- a/packages/core/client/src/schema-initializer/buttons/FormActionInitializers.tsx
+++ b/packages/core/client/src/schema-initializer/buttons/FormActionInitializers.tsx
@@ -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',
},
],
},
diff --git a/packages/core/client/src/schema-initializer/buttons/GridCardActionInitializers.tsx b/packages/core/client/src/schema-initializer/buttons/GridCardActionInitializers.tsx
index 2c868d2c2..706535c75 100644
--- a/packages/core/client/src/schema-initializer/buttons/GridCardActionInitializers.tsx
+++ b/packages/core/client/src/schema-initializer/buttons/GridCardActionInitializers.tsx
@@ -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();
diff --git a/packages/core/client/src/schema-initializer/buttons/ListActionInitializers.tsx b/packages/core/client/src/schema-initializer/buttons/ListActionInitializers.tsx
index 4ad7c4bf8..b4eb84a72 100644
--- a/packages/core/client/src/schema-initializer/buttons/ListActionInitializers.tsx
+++ b/packages/core/client/src/schema-initializer/buttons/ListActionInitializers.tsx
@@ -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();
diff --git a/packages/core/client/src/schema-initializer/buttons/ReadPrettyFormActionInitializers.tsx b/packages/core/client/src/schema-initializer/buttons/ReadPrettyFormActionInitializers.tsx
index fb346016f..b581c8da3 100644
--- a/packages/core/client/src/schema-initializer/buttons/ReadPrettyFormActionInitializers.tsx
+++ b/packages/core/client/src/schema-initializer/buttons/ReadPrettyFormActionInitializers.tsx
@@ -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,
},
],
diff --git a/packages/core/client/src/schema-initializer/buttons/TableActionColumnInitializers.tsx b/packages/core/client/src/schema-initializer/buttons/TableActionColumnInitializers.tsx
index ec040fe53..40e6770b3 100644
--- a/packages/core/client/src/schema-initializer/buttons/TableActionColumnInitializers.tsx
+++ b/packages/core/client/src/schema-initializer/buttons/TableActionColumnInitializers.tsx
@@ -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';
diff --git a/packages/core/client/src/schema-settings/SchemaSettings.tsx b/packages/core/client/src/schema-settings/SchemaSettings.tsx
index 78cb5dd41..7bddbb484 100644
--- a/packages/core/client/src/schema-settings/SchemaSettings.tsx
+++ b/packages/core/client/src/schema-settings/SchemaSettings.tsx
@@ -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(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) => {
{
{modalTip && }
{modalTip &&
}
- {visible && schemaUid && }
- {visible && schema && }
+ {visible && schemaUid && (
+
+ )}
+ {visible && schema && }
@@ -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(
() => ({
diff --git a/packages/core/database/src/collection.ts b/packages/core/database/src/collection.ts
index 0da717e76..03c2ace95 100644
--- a/packages/core/database/src/collection.ts
+++ b/packages/core/database/src/collection.ts
@@ -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;
diff --git a/packages/core/utils/src/json-templates.ts b/packages/core/utils/src/json-templates.ts
index 0d3e422cc..9a38a29f3 100644
--- a/packages/core/utils/src/json-templates.ts
+++ b/packages/core/utils/src/json-templates.ts
@@ -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;
diff --git a/packages/plugins/@nocobase/plugin-custom-request/.npmignore b/packages/plugins/@nocobase/plugin-custom-request/.npmignore
new file mode 100644
index 000000000..65f5e8779
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-custom-request/.npmignore
@@ -0,0 +1,2 @@
+/node_modules
+/src
diff --git a/packages/plugins/@nocobase/plugin-custom-request/client.d.ts b/packages/plugins/@nocobase/plugin-custom-request/client.d.ts
new file mode 100755
index 000000000..6c459cbac
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-custom-request/client.d.ts
@@ -0,0 +1,2 @@
+export * from './dist/client';
+export { default } from './dist/client';
diff --git a/packages/plugins/@nocobase/plugin-custom-request/client.js b/packages/plugins/@nocobase/plugin-custom-request/client.js
new file mode 100755
index 000000000..b6e3be70e
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-custom-request/client.js
@@ -0,0 +1 @@
+module.exports = require('./dist/client/index.js');
diff --git a/packages/plugins/@nocobase/plugin-custom-request/package.json b/packages/plugins/@nocobase/plugin-custom-request/package.json
new file mode 100644
index 000000000..5a9e60f7d
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-custom-request/package.json
@@ -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"
+ }
+}
diff --git a/packages/plugins/@nocobase/plugin-custom-request/server.d.ts b/packages/plugins/@nocobase/plugin-custom-request/server.d.ts
new file mode 100755
index 000000000..c41081ddc
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-custom-request/server.d.ts
@@ -0,0 +1,2 @@
+export * from './dist/server';
+export { default } from './dist/server';
diff --git a/packages/plugins/@nocobase/plugin-custom-request/server.js b/packages/plugins/@nocobase/plugin-custom-request/server.js
new file mode 100755
index 000000000..972842039
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-custom-request/server.js
@@ -0,0 +1 @@
+module.exports = require('./dist/server/index.js');
diff --git a/packages/plugins/@nocobase/plugin-custom-request/src/client/components/CustomRequestAction.tsx b/packages/plugins/@nocobase/plugin-custom-request/src/client/components/CustomRequestAction.tsx
new file mode 100644
index 000000000..0f8ac9068
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-custom-request/src/client/components/CustomRequestAction.tsx
@@ -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 ;
+};
+
+CustomRequestAction.Designer = CustomRequestActionDesigner;
+CustomRequestAction.Decorator = CustomRequestActionACLDecorator;
diff --git a/packages/plugins/@nocobase/plugin-custom-request/src/client/components/CustomRequestActionDesigner.tsx b/packages/plugins/@nocobase/plugin-custom-request/src/client/components/CustomRequestActionDesigner.tsx
new file mode 100644
index 000000000..a92b31c10
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-custom-request/src/client/components/CustomRequestActionDesigner.tsx
@@ -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 (
+ <>
+ !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 (
+ <>
+ !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 (
+
+
+
+
+ );
+};
diff --git a/packages/plugins/@nocobase/plugin-custom-request/src/client/components/index.ts b/packages/plugins/@nocobase/plugin-custom-request/src/client/components/index.ts
new file mode 100644
index 000000000..ab246698f
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-custom-request/src/client/components/index.ts
@@ -0,0 +1 @@
+export * from './CustomRequestAction';
diff --git a/packages/plugins/@nocobase/plugin-custom-request/src/client/constants.ts b/packages/plugins/@nocobase/plugin-custom-request/src/client/constants.ts
new file mode 100644
index 000000000..ad59e2724
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-custom-request/src/client/constants.ts
@@ -0,0 +1 @@
+export const listByCurrentRoleUrl = `customRequests:listByCurrentRole`;
diff --git a/packages/plugins/@nocobase/plugin-custom-request/src/client/hooks/index.ts b/packages/plugins/@nocobase/plugin-custom-request/src/client/hooks/index.ts
new file mode 100644
index 000000000..a11f46dba
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-custom-request/src/client/hooks/index.ts
@@ -0,0 +1,3 @@
+export * from './useCustomizeRequestActionProps';
+export * from './useGetCustomRequest';
+export * from './useCustomRequestVariableOptions';
diff --git a/packages/plugins/@nocobase/plugin-custom-request/src/client/hooks/useCustomRequestVariableOptions.ts b/packages/plugins/@nocobase/plugin-custom-request/src/client/hooks/useCustomRequestVariableOptions.ts
new file mode 100644
index 000000000..5415667e1
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-custom-request/src/client/hooks/useCustomRequestVariableOptions.ts
@@ -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]);
+};
diff --git a/packages/plugins/@nocobase/plugin-custom-request/src/client/hooks/useCustomRequestsResource.ts b/packages/plugins/@nocobase/plugin-custom-request/src/client/hooks/useCustomRequestsResource.ts
new file mode 100644
index 000000000..0560ee34b
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-custom-request/src/client/hooks/useCustomRequestsResource.ts
@@ -0,0 +1,6 @@
+import { useAPIClient } from '@nocobase/client';
+
+export const useCustomRequestsResource = () => {
+ const apiClient = useAPIClient();
+ return apiClient.resource('customRequests');
+};
diff --git a/packages/plugins/@nocobase/plugin-custom-request/src/client/hooks/useCustomizeRequestActionProps.ts b/packages/plugins/@nocobase/plugin-custom-request/src/client/hooks/useCustomizeRequestActionProps.ts
new file mode 100644
index 000000000..18b145bde
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-custom-request/src/client/hooks/useCustomizeRequestActionProps.ts
@@ -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;
+ }
+ },
+ };
+};
diff --git a/packages/plugins/@nocobase/plugin-custom-request/src/client/hooks/useGetCustomRequest.ts b/packages/plugins/@nocobase/plugin-custom-request/src/client/hooks/useGetCustomRequest.ts
new file mode 100644
index 000000000..c68a05f9c
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-custom-request/src/client/hooks/useGetCustomRequest.ts
@@ -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,
+ },
+ );
+};
diff --git a/packages/plugins/@nocobase/plugin-custom-request/src/client/index.tsx b/packages/plugins/@nocobase/plugin-custom-request/src/client/index.tsx
new file mode 100644
index 000000000..7a8144198
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-custom-request/src/client/index.tsx
@@ -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 (
+
+ {props.children}
+
+ );
+};
+
+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;
diff --git a/packages/plugins/@nocobase/plugin-custom-request/src/client/initializer/CustomRequestInitializer.tsx b/packages/plugins/@nocobase/plugin-custom-request/src/client/initializer/CustomRequestInitializer.tsx
new file mode 100644
index 000000000..8565c5a3d
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-custom-request/src/client/initializer/CustomRequestInitializer.tsx
@@ -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 = (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 (
+ {
+ await customRequestsResource.updateOrCreate({
+ values: {
+ key: s['x-uid'],
+ },
+ filterKeys: ['key'],
+ });
+ await props?.insert(s);
+ }}
+ schema={schema}
+ />
+ );
+};
diff --git a/packages/plugins/@nocobase/plugin-custom-request/src/client/initializer/index.ts b/packages/plugins/@nocobase/plugin-custom-request/src/client/initializer/index.ts
new file mode 100644
index 000000000..c46bcdd84
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-custom-request/src/client/initializer/index.ts
@@ -0,0 +1 @@
+export * from './CustomRequestInitializer';
diff --git a/packages/plugins/@nocobase/plugin-custom-request/src/client/locale.ts b/packages/plugins/@nocobase/plugin-custom-request/src/client/locale.ts
new file mode 100644
index 000000000..f33f10c4e
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-custom-request/src/client/locale.ts
@@ -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',
+ });
+}
diff --git a/packages/plugins/@nocobase/plugin-custom-request/src/client/schemas/CustomRequestACL.ts b/packages/plugins/@nocobase/plugin-custom-request/src/client/schemas/CustomRequestACL.ts
new file mode 100644
index 000000000..5d1b8cba9
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-custom-request/src/client/schemas/CustomRequestACL.ts
@@ -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 }}',
+ },
+ },
+ },
+};
diff --git a/packages/plugins/@nocobase/plugin-custom-request/src/client/schemas/CustomRequestConfigurationFields.ts b/packages/plugins/@nocobase/plugin-custom-request/src/client/schemas/CustomRequestConfigurationFields.ts
new file mode 100644
index 000000000..14937fa14
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-custom-request/src/client/schemas/CustomRequestConfigurationFields.ts
@@ -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,
+ },
+ },
+ },
+};
diff --git a/packages/plugins/@nocobase/plugin-custom-request/src/client/schemas/index.ts b/packages/plugins/@nocobase/plugin-custom-request/src/client/schemas/index.ts
new file mode 100644
index 000000000..19061ed2b
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-custom-request/src/client/schemas/index.ts
@@ -0,0 +1,2 @@
+export * from './CustomRequestConfigurationFields';
+export * from './CustomRequestACL';
diff --git a/packages/plugins/@nocobase/plugin-custom-request/src/index.ts b/packages/plugins/@nocobase/plugin-custom-request/src/index.ts
new file mode 100644
index 000000000..7e74612df
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-custom-request/src/index.ts
@@ -0,0 +1,2 @@
+export * from './server';
+export { default } from './server';
diff --git a/packages/plugins/@nocobase/plugin-custom-request/src/locale/zh-CN.ts b/packages/plugins/@nocobase/plugin-custom-request/src/locale/zh-CN.ts
new file mode 100644
index 000000000..23de800d3
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-custom-request/src/locale/zh-CN.ts
@@ -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': '请先配置请求设置',
+}
diff --git a/packages/plugins/@nocobase/plugin-custom-request/src/server/__tests__/actions.test.ts b/packages/plugins/@nocobase/plugin-custom-request/src/server/__tests__/actions.test.ts
new file mode 100644
index 000000000..ae3ec1993
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-custom-request/src/server/__tests__/actions.test.ts
@@ -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;
+ let resource: ReturnType['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);
+ });
+ });
+});
diff --git a/packages/plugins/@nocobase/plugin-custom-request/src/server/actions/listByCurrentRole.ts b/packages/plugins/@nocobase/plugin-custom-request/src/server/actions/listByCurrentRole.ts
new file mode 100644
index 000000000..82b67d7bf
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-custom-request/src/server/actions/listByCurrentRole.ts
@@ -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),
+ );
+}
diff --git a/packages/plugins/@nocobase/plugin-custom-request/src/server/actions/send.ts b/packages/plugins/@nocobase/plugin-custom-request/src/server/actions/send.ts
new file mode 100644
index 000000000..4b08213cd
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-custom-request/src/server/actions/send.ts
@@ -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) => {
+ 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();
+}
diff --git a/packages/plugins/@nocobase/plugin-custom-request/src/server/collections/.gitkeep b/packages/plugins/@nocobase/plugin-custom-request/src/server/collections/.gitkeep
new file mode 100644
index 000000000..e69de29bb
diff --git a/packages/plugins/@nocobase/plugin-custom-request/src/server/collections/customRequest.ts b/packages/plugins/@nocobase/plugin-custom-request/src/server/collections/customRequest.ts
new file mode 100644
index 000000000..a487a2fac
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-custom-request/src/server/collections/customRequest.ts
@@ -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;
diff --git a/packages/plugins/@nocobase/plugin-custom-request/src/server/collections/customRequestsRoles.ts b/packages/plugins/@nocobase/plugin-custom-request/src/server/collections/customRequestsRoles.ts
new file mode 100644
index 000000000..a02c68012
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-custom-request/src/server/collections/customRequestsRoles.ts
@@ -0,0 +1,8 @@
+import { CollectionOptions } from '@nocobase/client';
+
+export default {
+ namespace: 'custom-requests',
+ duplicator: 'optional',
+ name: 'customRequestsRoles',
+ title: '{{t("Custom request")}}',
+} as CollectionOptions;
diff --git a/packages/plugins/@nocobase/plugin-custom-request/src/server/index.ts b/packages/plugins/@nocobase/plugin-custom-request/src/server/index.ts
new file mode 100644
index 000000000..b68aea57f
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-custom-request/src/server/index.ts
@@ -0,0 +1 @@
+export { default } from './plugin';
diff --git a/packages/plugins/@nocobase/plugin-custom-request/src/server/plugin.ts b/packages/plugins/@nocobase/plugin-custom-request/src/server/plugin.ts
new file mode 100644
index 000000000..68ae8cff8
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-custom-request/src/server/plugin.ts
@@ -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;
diff --git a/packages/presets/nocobase/package.json b/packages/presets/nocobase/package.json
index 618e6ee6d..70d4ba42b 100644
--- a/packages/presets/nocobase/package.json
+++ b/packages/presets/nocobase/package.json
@@ -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"
},
diff --git a/packages/presets/nocobase/src/server/index.ts b/packages/presets/nocobase/src/server/index.ts
index ebcdf22e8..cd48e0952 100644
--- a/packages/presets/nocobase/src/server/index.ts
+++ b/packages/presets/nocobase/src/server/index.ts
@@ -27,6 +27,7 @@ export class PresetNocoBase extends Plugin {
'data-visualization',
'auth',
'sms-auth',
+ 'custom-request',
];
localPlugins = [
diff --git a/yarn.lock b/yarn.lock
index ea9f86eb8..d8b22f177 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -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"