feat(editor): Add option to disable credentials check in RLC (#17381)

This commit is contained in:
RomanDavydchuk
2025-07-17 12:02:34 +03:00
committed by GitHub
parent 436ec864d8
commit d466d9d373
4 changed files with 145 additions and 5 deletions

View File

@@ -103,3 +103,34 @@ export const TEST_NODE_SINGLE_MODE: INode = {
options: {}, options: {},
}, },
}; };
export const TEST_PARAMETER_SKIP_CREDENTIALS_CHECK: INodeProperties = {
...TEST_PARAMETER_MULTI_MODE,
name: 'testParameterSkipCredentialsCheck',
modes: [
{
displayName: 'From List',
name: 'list',
type: 'list',
typeOptions: {
searchListMethod: 'testSearch',
searchable: true,
skipCredentialsCheckInRLC: true,
},
},
],
};
export const TEST_NODE_NO_CREDENTIALS: INode = {
...TEST_NODE_MULTI_MODE,
name: 'Test Node - No Credentials',
parameters: {
authentication: undefined,
resource: 'test',
operation: 'get',
testParameterSkipCredentialsCheck: TEST_MODEL_VALUE,
id: '',
options: {},
},
credentials: undefined,
};

View File

@@ -9,9 +9,11 @@ import {
TEST_MODEL_VALUE, TEST_MODEL_VALUE,
TEST_NODE_MULTI_MODE, TEST_NODE_MULTI_MODE,
TEST_NODE_SINGLE_MODE, TEST_NODE_SINGLE_MODE,
TEST_NODE_NO_CREDENTIALS,
TEST_PARAMETER_ADD_RESOURCE, TEST_PARAMETER_ADD_RESOURCE,
TEST_PARAMETER_MULTI_MODE, TEST_PARAMETER_MULTI_MODE,
TEST_PARAMETER_SINGLE_MODE, TEST_PARAMETER_SINGLE_MODE,
TEST_PARAMETER_SKIP_CREDENTIALS_CHECK,
} from './ResourceLocator.test.constants'; } from './ResourceLocator.test.constants';
vi.mock('vue-router', async () => { vi.mock('vue-router', async () => {
@@ -197,6 +199,101 @@ describe('ResourceLocator', () => {
}); });
}); });
it('renders error when credentials are required and skipCredentialsCheckInRLC is false', async () => {
nodeTypesStore.getResourceLocatorResults.mockResolvedValue({
results: [
{
name: 'Test Resource',
value: 'test-resource',
url: 'https://test.com/test-resource',
},
],
paginationToken: null,
});
nodeTypesStore.getNodeType = vi.fn().mockReturnValue({
displayName: 'Test Node',
credentials: [
{
name: 'testAuth',
required: true,
},
],
});
const { getByTestId, queryByTestId } = renderComponent({
props: {
modelValue: TEST_MODEL_VALUE,
parameter: TEST_PARAMETER_MULTI_MODE,
path: `parameters.${TEST_PARAMETER_MULTI_MODE.name}`,
node: TEST_NODE_NO_CREDENTIALS,
displayTitle: 'Test Resource Locator',
expressionComputedValue: '',
isValueExpression: false,
},
});
expect(getByTestId(`resource-locator-${TEST_PARAMETER_MULTI_MODE.name}`)).toBeInTheDocument();
await userEvent.click(getByTestId('rlc-input'));
expect(getByTestId('rlc-error-container')).toBeInTheDocument();
expect(getByTestId('permission-error-link')).toBeInTheDocument();
expect(queryByTestId('rlc-item')).not.toBeInTheDocument();
});
it('renders list items when skipCredentialsCheckInRLC is true even without credentials', async () => {
const TEST_ITEMS = [
{ name: 'Test Resource', value: 'test-resource', url: 'https://test.com/test-resource' },
{
name: 'Test Resource 2',
value: 'test-resource-2',
url: 'https://test.com/test-resource-2',
},
];
nodeTypesStore.getResourceLocatorResults.mockResolvedValue({
results: TEST_ITEMS,
paginationToken: null,
});
nodeTypesStore.getNodeType = vi.fn().mockReturnValue({
displayName: 'Test Node',
credentials: [
{
name: 'testAuth',
required: true,
},
],
});
const { getByTestId, getByText, getAllByTestId, queryByTestId } = renderComponent({
props: {
modelValue: TEST_MODEL_VALUE,
parameter: TEST_PARAMETER_SKIP_CREDENTIALS_CHECK,
path: `parameters.${TEST_PARAMETER_SKIP_CREDENTIALS_CHECK.name}`,
node: TEST_NODE_NO_CREDENTIALS,
displayTitle: 'Test Resource Locator',
expressionComputedValue: '',
isValueExpression: false,
},
});
expect(
getByTestId(`resource-locator-${TEST_PARAMETER_SKIP_CREDENTIALS_CHECK.name}`),
).toBeInTheDocument();
await userEvent.click(getByTestId('rlc-input'));
await waitFor(() => {
expect(nodeTypesStore.getResourceLocatorResults).toHaveBeenCalled();
});
expect(getAllByTestId('rlc-item')).toHaveLength(TEST_ITEMS.length);
TEST_ITEMS.forEach((item) => {
expect(getByText(item.name)).toBeInTheDocument();
});
expect(queryByTestId('rlc-error-container')).not.toBeInTheDocument();
expect(queryByTestId('permission-error-link')).not.toBeInTheDocument();
});
// Testing error message deduplication // Testing error message deduplication
describe('ResourceLocator credentials error handling', () => { describe('ResourceLocator credentials error handling', () => {
it.each([ it.each([

View File

@@ -186,8 +186,9 @@ const hasCredentialError = computed(() => {
); );
}); });
const credentialsNotSet = computed(() => { const credentialsRequiredAndNotSet = computed(() => {
if (!props.node) return false; if (!props.node) return false;
if (skipCredentialsCheckInRLC.value) return false;
const nodeType = nodeTypesStore.getNodeType(props.node.type); const nodeType = nodeTypesStore.getNodeType(props.node.type);
if (nodeType) { if (nodeType) {
const usesCredentials = nodeType.credentials !== undefined && nodeType.credentials.length > 0; const usesCredentials = nodeType.credentials !== undefined && nodeType.credentials.length > 0;
@@ -315,6 +316,10 @@ const currentQueryError = computed(() => {
const isSearchable = computed(() => !!getPropertyArgument(currentMode.value, 'searchable')); const isSearchable = computed(() => !!getPropertyArgument(currentMode.value, 'searchable'));
const skipCredentialsCheckInRLC = computed(
() => !!getPropertyArgument(currentMode.value, 'skipCredentialsCheckInRLC'),
);
const requiresSearchFilter = computed( const requiresSearchFilter = computed(
() => !!getPropertyArgument(currentMode.value, 'searchFilterRequired'), () => !!getPropertyArgument(currentMode.value, 'searchFilterRequired'),
); );
@@ -666,7 +671,7 @@ async function loadResources() {
const paramsKey = currentRequestKey.value; const paramsKey = currentRequestKey.value;
const cachedResponse = cachedResponses.value[paramsKey]; const cachedResponse = cachedResponses.value[paramsKey];
if (credentialsNotSet.value) { if (credentialsRequiredAndNotSet.value) {
setResponse(paramsKey, { error: true }); setResponse(paramsKey, { error: true });
return; return;
} }
@@ -922,7 +927,7 @@ function removeOverride() {
<template #error> <template #error>
<div :class="$style.errorContainer" data-test-id="rlc-error-container"> <div :class="$style.errorContainer" data-test-id="rlc-error-container">
<n8n-text <n8n-text
v-if="credentialsNotSet || currentResponse.errorDetails" v-if="credentialsRequiredAndNotSet || currentResponse.errorDetails"
color="text-dark" color="text-dark"
align="center" align="center"
tag="div" tag="div"
@@ -946,9 +951,12 @@ function removeOverride() {
{{ currentResponse.errorDetails.description }} {{ currentResponse.errorDetails.description }}
</N8nNotice> </N8nNotice>
</div> </div>
<div v-if="hasCredentialError || credentialsNotSet" data-test-id="permission-error-link"> <div
v-if="hasCredentialError || credentialsRequiredAndNotSet"
data-test-id="permission-error-link"
>
<a <a
v-if="credentialsNotSet" v-if="credentialsRequiredAndNotSet"
:class="$style['credential-link']" :class="$style['credential-link']"
@click="createNewCredential" @click="createNewCredential"
> >

View File

@@ -1474,6 +1474,10 @@ export interface INodePropertyModeTypeOptions {
searchListMethod?: string; // Supported by: options searchListMethod?: string; // Supported by: options
searchFilterRequired?: boolean; searchFilterRequired?: boolean;
searchable?: boolean; searchable?: boolean;
/**
* If true, the resource locator will not show an error if the credentials are not selected
*/
skipCredentialsCheckInRLC?: boolean;
allowNewResource?: { allowNewResource?: {
label: string; label: string;
defaultName: string; defaultName: string;