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