mirror of
https://github.com/n8n-io/n8n.git
synced 2026-09-01 15:47:41 +08:00
feat(editor): Add create new button in resource locator for data table node (#19788)
This commit is contained in:
@@ -1834,6 +1834,7 @@
|
||||
"resourceLocator.mode.list.placeholder": "Choose...",
|
||||
"resourceLocator.mode.list.searchRequired": "Enter a search term to show results",
|
||||
"resourceLocator.mode.list.addNewResource.vectorStoreInMemory": "Create key '{resourceName}'",
|
||||
"resourceLocator.dataTable.createNew": "Create new data table",
|
||||
"resourceLocator.modeSelector.placeholder": "Mode...",
|
||||
"resourceLocator.openSpecificResource": "Open {entity} in {appName}",
|
||||
"resourceLocator.openResource": "Open in {appName}",
|
||||
|
||||
+32
@@ -134,3 +134,35 @@ export const TEST_NODE_NO_CREDENTIALS: INode = {
|
||||
},
|
||||
credentials: undefined,
|
||||
};
|
||||
|
||||
export const TEST_PARAMETER_URL_REDIRECT: INodeProperties = {
|
||||
...TEST_PARAMETER_MULTI_MODE,
|
||||
name: 'testParameterUrlRedirect',
|
||||
modes: [
|
||||
{
|
||||
displayName: 'From List',
|
||||
name: 'list',
|
||||
type: 'list',
|
||||
typeOptions: {
|
||||
searchListMethod: 'testSearch',
|
||||
searchable: true,
|
||||
allowNewResource: {
|
||||
label: 'resourceLocator.dataTable.createNew',
|
||||
url: '/projects/{{$projectId}}/datatables/new',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const TEST_NODE_URL_REDIRECT: INode = {
|
||||
...TEST_NODE_MULTI_MODE,
|
||||
name: 'Test Node - URL Redirect',
|
||||
parameters: {
|
||||
resource: 'test',
|
||||
operation: 'get',
|
||||
testParameterUrlRedirect: TEST_MODEL_VALUE,
|
||||
id: '',
|
||||
options: {},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { createComponentRenderer } from '@/__tests__/render';
|
||||
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
||||
import { useProjectsStore } from '@/stores/projects.store';
|
||||
import ResourceLocator from './ResourceLocator.vue';
|
||||
import { createTestingPinia } from '@pinia/testing';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
@@ -10,10 +11,12 @@ import {
|
||||
TEST_NODE_MULTI_MODE,
|
||||
TEST_NODE_SINGLE_MODE,
|
||||
TEST_NODE_NO_CREDENTIALS,
|
||||
TEST_NODE_URL_REDIRECT,
|
||||
TEST_PARAMETER_ADD_RESOURCE,
|
||||
TEST_PARAMETER_MULTI_MODE,
|
||||
TEST_PARAMETER_SINGLE_MODE,
|
||||
TEST_PARAMETER_SKIP_CREDENTIALS_CHECK,
|
||||
TEST_PARAMETER_URL_REDIRECT,
|
||||
} from './ResourceLocator.test.constants';
|
||||
|
||||
vi.mock('vue-router', async () => {
|
||||
@@ -43,7 +46,11 @@ vi.mock('@/composables/useTelemetry', () => ({
|
||||
useTelemetry: () => ({ track: vi.fn() }),
|
||||
}));
|
||||
|
||||
// Mock window.open
|
||||
vi.spyOn(window, 'open').mockImplementation(() => null);
|
||||
|
||||
let nodeTypesStore: ReturnType<typeof mockedStore<typeof useNodeTypesStore>>;
|
||||
let projectsStore: ReturnType<typeof mockedStore<typeof useProjectsStore>>;
|
||||
|
||||
const renderComponent = createComponentRenderer(ResourceLocator, {
|
||||
props: {
|
||||
@@ -74,6 +81,8 @@ describe('ResourceLocator', () => {
|
||||
createTestingPinia();
|
||||
nodeTypesStore = mockedStore(useNodeTypesStore);
|
||||
nodeTypesStore.getNodeType = vi.fn().mockReturnValue({ displayName: 'Test Node' });
|
||||
projectsStore = mockedStore(useProjectsStore);
|
||||
projectsStore.currentProjectId = 'test-project-123';
|
||||
});
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
@@ -433,4 +442,34 @@ describe('ResourceLocator', () => {
|
||||
],
|
||||
]);
|
||||
});
|
||||
|
||||
it('opens URL in new tab when add resource button is clicked with URL configuration', async () => {
|
||||
const windowOpenSpy = vi.spyOn(window, 'open');
|
||||
nodeTypesStore.getResourceLocatorResults.mockResolvedValue({
|
||||
results: [],
|
||||
paginationToken: null,
|
||||
});
|
||||
|
||||
const { getByTestId } = renderComponent({
|
||||
props: {
|
||||
modelValue: TEST_MODEL_VALUE,
|
||||
parameter: TEST_PARAMETER_URL_REDIRECT,
|
||||
path: `parameters.${TEST_PARAMETER_URL_REDIRECT.name}`,
|
||||
node: TEST_NODE_URL_REDIRECT,
|
||||
displayTitle: 'Test Resource Locator',
|
||||
expressionComputedValue: '',
|
||||
},
|
||||
});
|
||||
|
||||
await userEvent.click(getByTestId('rlc-input'));
|
||||
|
||||
await userEvent.click(getByTestId('rlc-item-add-resource'));
|
||||
|
||||
expect(windowOpenSpy).toHaveBeenCalledWith(
|
||||
'/projects/test-project-123/datatables/new',
|
||||
'_blank',
|
||||
);
|
||||
|
||||
expect(nodeTypesStore.getNodeParameterActionResult).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -367,10 +367,13 @@ const allowNewResources = computed(() => {
|
||||
return {
|
||||
label: i18n.baseText(addNewResourceOptions.label as BaseTextKey, {
|
||||
interpolate: {
|
||||
resourceName: searchFilter.value ? searchFilter.value : addNewResourceOptions.defaultName,
|
||||
resourceName: searchFilter.value
|
||||
? searchFilter.value
|
||||
: (addNewResourceOptions.defaultName ?? ''),
|
||||
},
|
||||
}),
|
||||
method: addNewResourceOptions.method,
|
||||
url: addNewResourceOptions.url,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -379,7 +382,23 @@ const handleAddResourceClick = async () => {
|
||||
return;
|
||||
}
|
||||
|
||||
const { method: addNewResourceMethodName } = allowNewResources.value;
|
||||
const { method: addNewResourceMethodName, url: redirectUrl } = allowNewResources.value;
|
||||
|
||||
if (redirectUrl) {
|
||||
let resolvedUrl = redirectUrl;
|
||||
|
||||
if (resolvedUrl.includes('{{$projectId}}')) {
|
||||
resolvedUrl = resolvedUrl.replace(
|
||||
/\{\{\$projectId\}\}/g,
|
||||
projectsStore.currentProjectId ?? '',
|
||||
);
|
||||
}
|
||||
|
||||
hideResourceDropdown();
|
||||
openResource(resolvedUrl);
|
||||
return;
|
||||
}
|
||||
|
||||
const resolvedNodeParameters = workflowHelpers.resolveRequiredParameters(
|
||||
props.parameter,
|
||||
currentRequestParams.value.parameters,
|
||||
@@ -417,7 +436,9 @@ const handleAddResourceClick = async () => {
|
||||
};
|
||||
|
||||
const onAddResourceClicked = computed(() =>
|
||||
allowNewResources.value ? handleAddResourceClick : undefined,
|
||||
allowNewResources.value && (allowNewResources.value.method || allowNewResources.value.url)
|
||||
? handleAddResourceClick
|
||||
: undefined,
|
||||
);
|
||||
|
||||
watch(currentQueryError, (curr, prev) => {
|
||||
|
||||
+7
-2
@@ -439,11 +439,16 @@ defineExpose({ isWithinDropdown });
|
||||
}
|
||||
|
||||
.resourceNameContainer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: var(--font-size-2xs);
|
||||
min-width: 0;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.resourceNameContainer > :first-child {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: inline-block;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.searchIcon {
|
||||
|
||||
@@ -69,6 +69,10 @@ export const description: INodeProperties[] = [
|
||||
typeOptions: {
|
||||
searchListMethod: 'tableSearch',
|
||||
searchable: true,
|
||||
allowNewResource: {
|
||||
label: 'resourceLocator.dataTable.createNew',
|
||||
url: '/projects/{{$projectId}}/datatables/new',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1561,9 +1561,10 @@ export interface INodePropertyModeTypeOptions {
|
||||
skipCredentialsCheckInRLC?: boolean;
|
||||
allowNewResource?: {
|
||||
label: string;
|
||||
defaultName: string;
|
||||
method: string;
|
||||
};
|
||||
} & (
|
||||
| { method: string; url?: never; defaultName: string }
|
||||
| { method?: never; url: string; defaultName?: never }
|
||||
);
|
||||
}
|
||||
|
||||
export interface INodePropertyMode {
|
||||
|
||||
Reference in New Issue
Block a user