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: 'Details right' }).hover();
await page.getByRole('menuitem', { name: 'Associated records' }).last().hover(); await page.getByRole('menuitem', { name: 'Associated records' }).last().hover();
await page.getByRole('menuitem', { name: 'Roles' }).click(); 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.getByLabel('schema-initializer-Grid-details:configureFields-roles').hover();
await page.getByRole('menuitem', { name: 'Role UID' }).click(); await page.getByRole('menuitem', { name: 'Role UID' }).click();
await page.mouse.move(300, 0); await page.mouse.move(300, 0);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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