mirror of
https://github.com/nocobase/nocobase.git
synced 2026-08-28 17:43:07 +08:00
Merge branch 'main' into next
This commit is contained in:
@@ -8,6 +8,7 @@
|
||||
*/
|
||||
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { APIClient } from '@nocobase/sdk';
|
||||
import { FlowContext, FlowRunJSContext } from '../flowContext';
|
||||
import { JSItemRunJSContext } from '../runjs-context/contexts/JSItemRunJSContext';
|
||||
|
||||
@@ -42,4 +43,96 @@ describe('FlowRunJSContext form submission', () => {
|
||||
ctx.form.submit();
|
||||
expect(nativeSubmit).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('preserves nested form values and adds association paths to matching resource create calls', async () => {
|
||||
const api = new APIClient();
|
||||
const request = vi.spyOn(api.axios, 'request').mockResolvedValue({ data: { data: { id: 1 } } });
|
||||
const values = { name: 'Alice', children: [{ name: 'Bob' }] };
|
||||
const getFieldsValue = vi.fn(() => values);
|
||||
const delegate = new FlowContext();
|
||||
delegate.defineProperty('api', { value: api });
|
||||
delegate.defineProperty('resource', {
|
||||
value: {
|
||||
getResourceName: () => 't1_user',
|
||||
getDataSourceKey: () => 'main',
|
||||
getUpdateAssociationValues: () => ['children'],
|
||||
},
|
||||
});
|
||||
delegate.defineProperty('form', { value: { submit: vi.fn(), getFieldsValue } });
|
||||
delegate.defineProperty('blockModel', { value: { submitFromRunJs: vi.fn() } });
|
||||
const ctx = new FlowRunJSContext(delegate);
|
||||
|
||||
await ctx.api.resource('t1_user').create({ values: ctx.form.getFieldsValue(true) });
|
||||
|
||||
expect(getFieldsValue).toHaveBeenCalledWith(true);
|
||||
expect(request).toHaveBeenCalledWith({
|
||||
url: 't1_user:create',
|
||||
method: 'post',
|
||||
params: { updateAssociationValues: ['children'] },
|
||||
data: values,
|
||||
});
|
||||
});
|
||||
|
||||
it('keeps explicit association params and unrelated resource calls unchanged', async () => {
|
||||
const api = new APIClient();
|
||||
const request = vi.spyOn(api.axios, 'request').mockResolvedValue({ data: { data: { id: 1 } } });
|
||||
const delegate = new FlowContext();
|
||||
delegate.defineProperty('api', { value: api });
|
||||
delegate.defineProperty('resource', {
|
||||
value: {
|
||||
getResourceName: () => 't1_user',
|
||||
getDataSourceKey: () => 'main',
|
||||
getUpdateAssociationValues: () => ['children'],
|
||||
},
|
||||
});
|
||||
delegate.defineProperty('form', { value: { submit: vi.fn() } });
|
||||
delegate.defineProperty('blockModel', { value: { submitFromRunJs: vi.fn() } });
|
||||
const ctx = new FlowRunJSContext(delegate);
|
||||
|
||||
await ctx.api.resource('t1_user').create({ values: {}, updateAssociationValues: [] });
|
||||
await ctx.api.resource('t1_user').create({ values: {}, updateAssociationValues: null });
|
||||
await ctx.api.resource('t1_user').create({ values: {}, updateAssociationValues: undefined });
|
||||
await ctx.api.resource('posts').create({ values: { title: 'Post' } });
|
||||
await ctx.api.resource('t1_user', undefined, { 'X-Data-Source': 'external' }).create({ values: { name: 'Bob' } });
|
||||
|
||||
expect(request.mock.calls.map(([config]) => config.params)).toEqual([
|
||||
{ updateAssociationValues: [] },
|
||||
{ updateAssociationValues: null },
|
||||
{ updateAssociationValues: undefined },
|
||||
{},
|
||||
{},
|
||||
]);
|
||||
});
|
||||
|
||||
it('adds association paths only for the matching association source record', async () => {
|
||||
const api = new APIClient();
|
||||
const request = vi.spyOn(api.axios, 'request').mockResolvedValue({ data: { data: { id: 1 } } });
|
||||
const delegate = new FlowContext();
|
||||
delegate.defineProperty('api', { value: api });
|
||||
delegate.defineProperty('resource', {
|
||||
value: {
|
||||
getResourceName: () => 'users.children',
|
||||
getDataSourceKey: () => 'main',
|
||||
getSourceId: () => 1,
|
||||
getUpdateAssociationValues: () => ['toys'],
|
||||
},
|
||||
});
|
||||
delegate.defineProperty('form', { value: { submit: vi.fn() } });
|
||||
delegate.defineProperty('blockModel', { value: { submitFromRunJs: vi.fn() } });
|
||||
const ctx = new FlowRunJSContext(delegate);
|
||||
|
||||
await ctx.api.resource('users.children', 1).create({ values: { name: 'same source' } });
|
||||
await ctx.api.resource('users.children', 2).create({ values: { name: 'different source' } });
|
||||
|
||||
expect(request.mock.calls.map(([config]) => ({ url: config.url, params: config.params }))).toEqual([
|
||||
{
|
||||
url: 'users/1/children:create',
|
||||
params: { updateAssociationValues: ['toys'] },
|
||||
},
|
||||
{
|
||||
url: 'users/2/children:create',
|
||||
params: {},
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -53,7 +53,7 @@ import { FlowExitAllException } from './utils/exceptions';
|
||||
import { buildFlowModelResolveDescriptor, enqueueVariablesResolve, JSONValue } from './utils/params-resolvers';
|
||||
import type { RecordRef } from './utils/serverContextParams';
|
||||
import { buildServerContextParams as _buildServerContextParams } from './utils/serverContextParams';
|
||||
import { getDirtyAwareApiClient } from './utils/dirtyAwareApiClient';
|
||||
import { getDirtyAwareApiClient, PREPARE_CONTEXT_RESOURCE_ACTION_PARAMS } from './utils/dirtyAwareApiClient';
|
||||
import { inferRecordRef, inferViewRecordRef } from './utils/variablesParams';
|
||||
import { FlowView, FlowViewer } from './views/FlowView';
|
||||
import { RunJSContextRegistry, getModelClassName, type RunJSVersion } from './runjs-context/registry';
|
||||
@@ -4608,6 +4608,49 @@ function __mergeRunJSDocMeta(base: any, patch: any): RunJSDocMeta {
|
||||
return out as RunJSDocMeta;
|
||||
}
|
||||
export class FlowRunJSContext extends FlowContext {
|
||||
[PREPARE_CONTEXT_RESOURCE_ACTION_PARAMS](
|
||||
action: { actionName: string; dataSourceKey?: string; resourceName: string; resourceOf?: unknown },
|
||||
params: Record<string, unknown> | undefined,
|
||||
) {
|
||||
if (
|
||||
action.actionName.toLowerCase() !== 'create' ||
|
||||
!params ||
|
||||
Array.isArray(params) ||
|
||||
Object.prototype.hasOwnProperty.call(params, 'updateAssociationValues') ||
|
||||
!this.form ||
|
||||
typeof this.blockModel?.submitFromRunJs !== 'function'
|
||||
) {
|
||||
return params;
|
||||
}
|
||||
|
||||
const resource = this.resource;
|
||||
const currentResourceName = resource?.getResourceName?.();
|
||||
const currentDataSourceKey = resource?.getDataSourceKey?.() || 'main';
|
||||
if (action.resourceName !== currentResourceName || (action.dataSourceKey || 'main') !== currentDataSourceKey) {
|
||||
return params;
|
||||
}
|
||||
|
||||
const currentSourceId = resource?.getSourceId?.();
|
||||
if (
|
||||
currentResourceName?.includes('.') &&
|
||||
currentSourceId !== null &&
|
||||
typeof currentSourceId !== 'undefined' &&
|
||||
String(action.resourceOf ?? '') !== String(currentSourceId)
|
||||
) {
|
||||
return params;
|
||||
}
|
||||
|
||||
const updateAssociationValues = resource?.getUpdateAssociationValues?.();
|
||||
if (!Array.isArray(updateAssociationValues) || updateAssociationValues.length === 0) {
|
||||
return params;
|
||||
}
|
||||
|
||||
return {
|
||||
...params,
|
||||
updateAssociationValues: [...updateAssociationValues],
|
||||
};
|
||||
}
|
||||
|
||||
constructor(delegate: FlowContext) {
|
||||
super();
|
||||
this.addDelegate(delegate);
|
||||
|
||||
@@ -42,9 +42,19 @@ type ResourceRequestOptions = RequestOptions & {
|
||||
type DirtyResourceAction = {
|
||||
dataSourceKey?: string;
|
||||
resourceName: string;
|
||||
resourceOf?: unknown;
|
||||
actionName: string;
|
||||
};
|
||||
|
||||
export const PREPARE_CONTEXT_RESOURCE_ACTION_PARAMS = Symbol('prepareContextResourceActionParams');
|
||||
|
||||
type ContextResourceActionParamsPreparer = {
|
||||
[PREPARE_CONTEXT_RESOURCE_ACTION_PARAMS]?: (
|
||||
action: DirtyResourceAction,
|
||||
params: ActionParams | undefined,
|
||||
) => ActionParams | undefined;
|
||||
};
|
||||
|
||||
type ApiUrlProvider = {
|
||||
getApiUrl?: (pathname?: string) => string;
|
||||
};
|
||||
@@ -328,6 +338,7 @@ function resolveDirtyResourceActionFromResource(
|
||||
|
||||
return {
|
||||
resourceName: normalizedResourceName,
|
||||
resourceOf,
|
||||
actionName: normalizedActionName,
|
||||
};
|
||||
}
|
||||
@@ -442,6 +453,18 @@ function createDirtyAwareResource(
|
||||
return async (...args: Parameters<ResourceActionFn>) => {
|
||||
const actionOptions = isObjectRecord(args[1]) ? args[1] : undefined;
|
||||
const dirtyResourceAction = resolveDirtyResourceActionFromResource(resourceName, resourceOf, prop, context);
|
||||
const prepareParams = (context as ContextResourceActionParamsPreparer)[PREPARE_CONTEXT_RESOURCE_ACTION_PARAMS];
|
||||
const actionParams =
|
||||
dirtyResourceAction && typeof prepareParams === 'function'
|
||||
? prepareParams.call(
|
||||
context,
|
||||
{
|
||||
...dirtyResourceAction,
|
||||
dataSourceKey: dirtyResourceAction.dataSourceKey || getDataSourceKeyFromHeaders(headers),
|
||||
},
|
||||
args[0],
|
||||
)
|
||||
: args[0];
|
||||
const requestKey = getDirtyResourceActionDispatchKey(dirtyResourceAction, headers);
|
||||
const resourceKey = getResourceDispatchKey(resourceName, resourceOf, headers);
|
||||
const inheritedToken =
|
||||
@@ -459,13 +482,13 @@ function createDirtyAwareResource(
|
||||
const forwardedArgs: Parameters<ResourceActionFn> =
|
||||
actionOptions || args[1] == null
|
||||
? [
|
||||
args[0],
|
||||
actionParams,
|
||||
{
|
||||
...actionOptions,
|
||||
[DIRTY_DISPATCH_TOKEN]: token,
|
||||
},
|
||||
]
|
||||
: args;
|
||||
: [actionParams, args[1]];
|
||||
let actionResult: Promise<unknown>;
|
||||
requestTokenStack.push({ key: requestKey, token });
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user