feat(custom-request): support selected table records in custom request (#6647)

* feat: support selected table records in custom request

* fix: test

* fix: bug

* fix: bug

* fix: bug

* fix: bug

* fix: merge bug

* fix: e2e test

* fix: e2e test
This commit is contained in:
Katherine 2025-04-17 12:35:03 +08:00 committed by GitHub
parent 1c343cad66
commit f5fb2844da
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 96 additions and 48 deletions

View File

@ -57,6 +57,7 @@ test.describe('add blocks to the popup', () => {
await page.getByRole('menuitem', { name: 'Details right' }).hover();
await page.getByRole('menuitem', { name: 'Associated records' }).last().hover();
await page.getByRole('menuitem', { name: 'Roles' }).click();
await page.mouse.move(300, 0);
await page.getByLabel('schema-initializer-Grid-details:configureFields-roles').hover();
await page.getByRole('menuitem', { name: 'Role UID' }).click();
await page.mouse.move(300, 0);

View File

@ -71,7 +71,7 @@ test.describe('sub page', () => {
expect(page.url()).not.toContain('/popups/');
// 确认是否回到了主页面
await page.getByText('Users单层子页面Configure').hover();
// await page.getByText('Users单层子页面Configure').hover();
await expect(
page.getByRole('button', { name: 'designer-schema-settings-CardItem-blockSettings:table-users' }),
).toBeVisible();

View File

@ -57,7 +57,7 @@ import { useAllDataBlocks } from '../page/AllDataBlocksProvider';
const useA = () => {
return {
async run() { },
async run() {},
};
};
@ -139,23 +139,26 @@ export const Action: ComposedAction = withDynamicSchemaProps(
);
const handleClick = useMemo(() => {
return onClick && (async (e, callback) => {
await onClick?.(e, callback);
return (
onClick &&
(async (e, callback) => {
await onClick?.(e, callback);
// 执行完 onClick 之后,刷新数据区块
const blocksToRefresh = fieldSchema['x-action-settings']?.onSuccess?.blocksToRefresh || []
if (blocksToRefresh.length > 0) {
getAllDataBlocks().forEach((block) => {
if (blocksToRefresh.includes(block.uid)) {
try {
block.service?.refresh();
} catch (error) {
console.error('Failed to refresh block:', block.uid, error);
// 执行完 onClick 之后,刷新数据区块
const blocksToRefresh = fieldSchema['x-action-settings']?.onSuccess?.blocksToRefresh || [];
if (blocksToRefresh.length > 0) {
getAllDataBlocks().forEach((block) => {
if (blocksToRefresh.includes(block.uid)) {
try {
block.service?.refresh();
} catch (error) {
console.error('Failed to refresh block:', block.uid, error);
}
}
}
});
}
});
});
}
})
);
}, [onClick, fieldSchema, getAllDataBlocks]);
return (

View File

@ -365,8 +365,8 @@ export const SchemaSettingsFormItemTemplate = function FormItemTemplate(props) {
required: true,
default: collection
? `${compile(collection?.title || collection?.name)}_${t(
componentTitle[componentName] || componentName,
)}`
componentTitle[componentName] || componentName,
)}`
: t(componentTitle[componentName] || componentName),
'x-decorator': 'FormItem',
'x-component': 'Input',
@ -567,7 +567,7 @@ export const SchemaSettingsRemove: FC<SchemaSettingsRemoveProps> = (props) => {
export interface SchemaSettingsSelectItemProps
extends Omit<SchemaSettingsItemProps, 'onChange' | 'onClick'>,
Omit<SelectWithTitleProps, 'title' | 'defaultValue'> {
Omit<SelectWithTitleProps, 'title' | 'defaultValue'> {
value?: SelectWithTitleProps['defaultValue'];
optionRender?: (option: any, info: { index: number }) => React.ReactNode;
}
@ -900,26 +900,32 @@ export const SchemaSettingsModalItem: FC<SchemaSettingsModalItemProps> = (props)
>
<LocationSearchContext.Provider value={locationSearch}>
<BlockRequestContext_deprecated.Provider value={ctx}>
<DataSourceApplicationProvider dataSourceManager={dm} dataSource={dataSourceKey}>
<DataSourceApplicationProvider
dataSourceManager={dm}
dataSource={dataSourceKey}
>
<AssociationOrCollectionProvider
allowNull
collection={collection?.name}
association={association}
>
<SchemaComponentOptions scope={options.scope} components={options.components}>
<SchemaComponentOptions
scope={options.scope}
components={options.components}
>
<FormLayout
layout={'vertical'}
className={css`
// screen > 576px
@media (min-width: 576px) {
min-width: 520px;
}
// screen > 576px
@media (min-width: 576px) {
min-width: 520px;
}
// screen <= 576px
@media (max-width: 576px) {
min-width: 320px;
}
`}
// screen <= 576px
@media (max-width: 576px) {
min-width: 320px;
}
`}
>
<ApplicationContext.Provider value={app}>
<APIClientProvider apiClient={apiClient}>
@ -984,13 +990,13 @@ export const SchemaSettingsDefaultSortingRules = function DefaultSortingRules(pr
const sort = defaultSort?.map((item: string) => {
return item.startsWith('-')
? {
field: item.substring(1),
direction: 'desc',
}
field: item.substring(1),
direction: 'desc',
}
: {
field: item,
direction: 'asc',
};
field: item,
direction: 'asc',
};
});
const sortFields = useSortFields(props.name || collection?.name);

View File

@ -15,3 +15,4 @@ export * from './useURLSearchParamsVariable';
export * from './useUserVariable';
export * from './useVariableOptions';
export * from './usePopupVariable';
export * from './useContextAssociationFields';

View File

@ -46,7 +46,9 @@ const getChildren = (
return {
key: option.name,
value: option.name,
name: option.name,
label: compile(option.title),
title: compile(option.title),
disabled: disabled,
isLeaf: true,
depth,
@ -59,7 +61,9 @@ const getChildren = (
return {
key: option.name,
value: option.name,
name: option.name,
label: compile(option.title),
title: compile(option.title),
disabled: disabled,
isLeaf: true,
field: option,
@ -77,10 +81,10 @@ export const useContextAssociationFields = ({
contextCollectionName,
collectionField,
}: {
schema: any;
schema?: any;
maxDepth?: number;
contextCollectionName: string;
collectionField: CollectionFieldOptions_deprecated;
collectionField?: CollectionFieldOptions_deprecated;
}) => {
const { t } = useTranslation();
const compile = useCompile();
@ -101,10 +105,17 @@ export const useContextAssociationFields = ({
const children =
getChildren(
getFilterOptions(collectionName).filter((v) => {
const isAssociationField = ['hasOne', 'hasMany', 'belongsTo', 'belongsToMany', 'belongsToArray'].includes(
v.type,
);
return isAssociationField;
if (collectionField) {
const isAssociationField = [
'hasOne',
'hasMany',
'belongsTo',
'belongsToMany',
'belongsToArray',
].includes(v.type);
return isAssociationField;
}
return true;
}),
{
schema,

View File

@ -78,7 +78,7 @@ function AfterSuccess() {
return (
<SchemaSettingsModalItem
dialogRootClassName='dialog-after-successful-submission'
dialogRootClassName="dialog-after-successful-submission"
width={700}
title={t('After successful submission')}
initialValues={fieldSchema?.['x-action-settings']?.['onSuccess']}

View File

@ -15,11 +15,28 @@ import {
useCollectionRecordData,
useCompile,
useGlobalVariable,
useContextAssociationFields,
useTableBlockContext,
useCurrentPopupContext,
getStoredPopupContext,
useFormBlockContext,
} from '@nocobase/client';
import { useMemo } from 'react';
import { isEmpty } from 'lodash';
import { useTranslation } from '../locale';
const useIsShowTableSelectRecord = () => {
const { params } = useCurrentPopupContext();
const recordData = useCollectionRecordData();
const tableBlockContextBasicValue = useTableBlockContext();
if (recordData) {
return false;
}
const popupTableBlockContext = getStoredPopupContext(params?.popupuid)?.tableBlockContext;
return !isEmpty(popupTableBlockContext) || !isEmpty(tableBlockContextBasicValue);
};
export const useCustomRequestVariableOptions = () => {
const collection = useCollection_deprecated();
const { t } = useTranslation();
@ -33,6 +50,8 @@ export const useCustomRequestVariableOptions = () => {
return [compile(fieldsOptions), compile(userFieldOptions)];
}, [fieldsOptions, userFieldOptions]);
const environmentVariables = useGlobalVariable('$env');
const contextVariable = useContextAssociationFields({ maxDepth: 2, contextCollectionName: collection.name });
const shouldShowTableSelectVariable = useIsShowTableSelectRecord();
return useMemo(() => {
return [
environmentVariables,
@ -61,6 +80,7 @@ export const useCustomRequestVariableOptions = () => {
title: 'API token',
children: null,
},
shouldShowTableSelectVariable && { ...contextVariable, name: '$nSelectedRecord', title: contextVariable.label },
].filter(Boolean);
}, [recordData, t, fields, blockType, userFields]);
}, [recordData, t, fields, blockType, userFields, shouldShowTableSelectVariable]);
};

View File

@ -16,6 +16,8 @@ import {
useCompile,
useDataSourceKey,
useNavigateNoUpdate,
useBlockRequestContext,
useContextVariable,
} from '@nocobase/client';
import { isURL } from '@nocobase/utils/client';
import { App } from 'antd';
@ -25,18 +27,21 @@ export const useCustomizeRequestActionProps = () => {
const apiClient = useAPIClient();
const navigate = useNavigateNoUpdate();
const actionSchema = useFieldSchema();
const { field } = useBlockRequestContext();
const compile = useCompile();
const form = useForm();
const { name: blockType } = useBlockContext() || {};
// const { getPrimaryKey } = useCollection_deprecated();
const recordData = useCollectionRecordData();
const fieldSchema = useFieldSchema();
const actionField = useField();
const { setVisible } = useActionContext();
const { modal, message } = App.useApp();
const dataSourceKey = useDataSourceKey();
const { ctx } = useContextVariable();
return {
async onClick(e?, callBack?) {
const selectedRecord = field.data?.selectedRowData ? field.data?.selectedRowData : ctx;
const { skipValidator, onSuccess } = actionSchema?.['x-action-settings'] ?? {};
const { manualClose, redirecting, redirectTo, successMessage, actionAfterSuccess } = onSuccess || {};
const xAction = actionSchema?.['x-action'];
@ -58,12 +63,11 @@ export const useCustomizeRequestActionProps = () => {
method: 'POST',
data: {
currentRecord: {
// id: record[getPrimaryKey()],
// appends: result.params[0]?.appends,
dataSourceKey,
data: currentRecordData,
},
$nForm: blockType === 'form' ? form.values : undefined,
$nSelectedRecord: selectedRecord,
},
responseType: fieldSchema['x-response-type'] === 'stream' ? 'blob' : 'json',
});

View File

@ -73,6 +73,7 @@ export async function send(this: CustomRequestPlugin, ctx: Context, next: Next)
data: {},
},
$nForm,
$nSelectedRecord,
} = values;
// root role has all permissions
@ -154,6 +155,7 @@ export async function send(this: CustomRequestPlugin, ctx: Context, next: Next)
$nToken: ctx.getBearerToken(),
$nForm,
$env: ctx.app.environment.getVariables(),
$nSelectedRecord,
};
const axiosRequestConfig = {