From bf099a436475344647900bdd0a2488e5359b7e77 Mon Sep 17 00:00:00 2001 From: Dunqing Date: Fri, 13 Oct 2023 02:16:50 -0500 Subject: [PATCH] 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 Co-authored-by: Rain <958414905@qq.com> --- packages/core/client/src/acl/style.ts | 5 +- .../client/src/block-provider/hooks/index.ts | 2 +- .../hooks/useCollectionManager.ts | 5 +- .../antd/action/Action.Designer.tsx | 23 ++- .../src/schema-component/antd/page/Page.tsx | 4 +- .../schema-component/antd/variable/Input.tsx | 2 +- .../antd/variable/RawTextArea.tsx | 11 +- .../antd/variable/TextArea.tsx | 4 +- .../antd/variable/VariableSelect.tsx | 9 +- .../CalendarFormActionInitializers.tsx | 20 +- .../buttons/FormActionInitializers.tsx | 80 +------- .../buttons/GridCardActionInitializers.tsx | 16 +- .../buttons/ListActionInitializers.tsx | 16 +- .../ReadPrettyFormActionInitializers.tsx | 20 +- .../buttons/TableActionColumnInitializers.tsx | 16 +- .../src/schema-settings/SchemaSettings.tsx | 34 +++- packages/core/database/src/collection.ts | 2 +- packages/core/utils/src/json-templates.ts | 4 +- .../plugin-custom-request/.npmignore | 2 + .../plugin-custom-request/client.d.ts | 2 + .../@nocobase/plugin-custom-request/client.js | 1 + .../plugin-custom-request/package.json | 22 +++ .../plugin-custom-request/server.d.ts | 2 + .../@nocobase/plugin-custom-request/server.js | 1 + .../client/components/CustomRequestAction.tsx | 41 ++++ .../CustomRequestActionDesigner.tsx | 122 ++++++++++++ .../src/client/components/index.ts | 1 + .../src/client/constants.ts | 1 + .../src/client/hooks/index.ts | 3 + .../hooks/useCustomRequestVariableOptions.ts | 34 ++++ .../client/hooks/useCustomRequestsResource.ts | 6 + .../hooks/useCustomizeRequestActionProps.ts | 112 +++++++++++ .../src/client/hooks/useGetCustomRequest.ts | 19 ++ .../src/client/index.tsx | 38 ++++ .../initializer/CustomRequestInitializer.tsx | 40 ++++ .../src/client/initializer/index.ts | 1 + .../src/client/locale.ts | 18 ++ .../src/client/schemas/CustomRequestACL.ts | 25 +++ .../CustomRequestConfigurationFields.ts | 182 ++++++++++++++++++ .../src/client/schemas/index.ts | 2 + .../plugin-custom-request/src/index.ts | 2 + .../plugin-custom-request/src/locale/zh-CN.ts | 26 +++ .../src/server/__tests__/actions.test.ts | 58 ++++++ .../src/server/actions/listByCurrentRole.ts | 23 +++ .../src/server/actions/send.ts | 115 +++++++++++ .../src/server/collections/.gitkeep | 0 .../src/server/collections/customRequest.ts | 30 +++ .../server/collections/customRequestsRoles.ts | 8 + .../plugin-custom-request/src/server/index.ts | 1 + .../src/server/plugin.ts | 41 ++++ packages/presets/nocobase/package.json | 1 + packages/presets/nocobase/src/server/index.ts | 1 + yarn.lock | 2 +- 53 files changed, 1061 insertions(+), 195 deletions(-) create mode 100644 packages/plugins/@nocobase/plugin-custom-request/.npmignore create mode 100755 packages/plugins/@nocobase/plugin-custom-request/client.d.ts create mode 100755 packages/plugins/@nocobase/plugin-custom-request/client.js create mode 100644 packages/plugins/@nocobase/plugin-custom-request/package.json create mode 100755 packages/plugins/@nocobase/plugin-custom-request/server.d.ts create mode 100755 packages/plugins/@nocobase/plugin-custom-request/server.js create mode 100644 packages/plugins/@nocobase/plugin-custom-request/src/client/components/CustomRequestAction.tsx create mode 100644 packages/plugins/@nocobase/plugin-custom-request/src/client/components/CustomRequestActionDesigner.tsx create mode 100644 packages/plugins/@nocobase/plugin-custom-request/src/client/components/index.ts create mode 100644 packages/plugins/@nocobase/plugin-custom-request/src/client/constants.ts create mode 100644 packages/plugins/@nocobase/plugin-custom-request/src/client/hooks/index.ts create mode 100644 packages/plugins/@nocobase/plugin-custom-request/src/client/hooks/useCustomRequestVariableOptions.ts create mode 100644 packages/plugins/@nocobase/plugin-custom-request/src/client/hooks/useCustomRequestsResource.ts create mode 100644 packages/plugins/@nocobase/plugin-custom-request/src/client/hooks/useCustomizeRequestActionProps.ts create mode 100644 packages/plugins/@nocobase/plugin-custom-request/src/client/hooks/useGetCustomRequest.ts create mode 100644 packages/plugins/@nocobase/plugin-custom-request/src/client/index.tsx create mode 100644 packages/plugins/@nocobase/plugin-custom-request/src/client/initializer/CustomRequestInitializer.tsx create mode 100644 packages/plugins/@nocobase/plugin-custom-request/src/client/initializer/index.ts create mode 100644 packages/plugins/@nocobase/plugin-custom-request/src/client/locale.ts create mode 100644 packages/plugins/@nocobase/plugin-custom-request/src/client/schemas/CustomRequestACL.ts create mode 100644 packages/plugins/@nocobase/plugin-custom-request/src/client/schemas/CustomRequestConfigurationFields.ts create mode 100644 packages/plugins/@nocobase/plugin-custom-request/src/client/schemas/index.ts create mode 100644 packages/plugins/@nocobase/plugin-custom-request/src/index.ts create mode 100644 packages/plugins/@nocobase/plugin-custom-request/src/locale/zh-CN.ts create mode 100644 packages/plugins/@nocobase/plugin-custom-request/src/server/__tests__/actions.test.ts create mode 100644 packages/plugins/@nocobase/plugin-custom-request/src/server/actions/listByCurrentRole.ts create mode 100644 packages/plugins/@nocobase/plugin-custom-request/src/server/actions/send.ts create mode 100644 packages/plugins/@nocobase/plugin-custom-request/src/server/collections/.gitkeep create mode 100644 packages/plugins/@nocobase/plugin-custom-request/src/server/collections/customRequest.ts create mode 100644 packages/plugins/@nocobase/plugin-custom-request/src/server/collections/customRequestsRoles.ts create mode 100644 packages/plugins/@nocobase/plugin-custom-request/src/server/index.ts create mode 100644 packages/plugins/@nocobase/plugin-custom-request/src/server/plugin.ts diff --git a/packages/core/client/src/acl/style.ts b/packages/core/client/src/acl/style.ts index b9181fb93..c4e9179c3 100644 --- a/packages/core/client/src/acl/style.ts +++ b/packages/core/client/src/acl/style.ts @@ -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; } } diff --git a/packages/core/client/src/block-provider/hooks/index.ts b/packages/core/client/src/block-provider/hooks/index.ts index dadd215ec..7c0c35462 100644 --- a/packages/core/client/src/block-provider/hooks/index.ts +++ b/packages/core/client/src/block-provider/hooks/index.ts @@ -66,7 +66,7 @@ const filterValue = (value) => { return obj; }; -function getFormValues({ +export function getFormValues({ filterByTk, field, form, diff --git a/packages/core/client/src/collection-manager/hooks/useCollectionManager.ts b/packages/core/client/src/collection-manager/hooks/useCollectionManager.ts index f7d0f2f17..f9a234523 100644 --- a/packages/core/client/src/collection-manager/hooks/useCollectionManager.ts +++ b/packages/core/client/src/collection-manager/hooks/useCollectionManager.ts @@ -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); diff --git a/packages/core/client/src/schema-component/antd/action/Action.Designer.tsx b/packages/core/client/src/schema-component/antd/action/Action.Designer.tsx index 03759f926..eab4803ba 100644 --- a/packages/core/client/src/schema-component/antd/action/Action.Designer.tsx +++ b/packages/core/client/src/schema-component/antd/action/Action.Designer.tsx @@ -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 ( @@ -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 ( - + {fieldSchema['x-action'] === 'submit' && fieldSchema.parent?.['x-initializer'] === 'CreateFormActionInitializers' && } - {isLinkageAction && } + {isLinkageAction && } {isDuplicateAction && } {isUpdateModePopupAction && } @@ -919,10 +924,10 @@ export const ActionDesigner = (props) => { {isValid(fieldSchema?.['x-action-settings']?.skipValidator) && } {isValid(fieldSchema?.['x-action-settings']?.['onSuccess']) && } {isValid(fieldSchema?.['x-action-settings']?.triggerWorkflows) && } - + {restProps.children} {isChildCollectionAction && } - {} + {} ); diff --git a/packages/core/client/src/schema-component/antd/page/Page.tsx b/packages/core/client/src/schema-component/antd/page/Page.tsx index 0fcf3e010..1e150c414 100644 --- a/packages/core/client/src/schema-component/antd/page/Page.tsx +++ b/packages/core/client/src/schema-component/antd/page/Page.tsx @@ -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( -
+
{ 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"