feat(editor): Add create new button in resource locator for data table node (#19788)

This commit is contained in:
Svetoslav Dekov
2025-09-22 11:49:40 +03:00
committed by GitHub
parent 517b936152
commit 0c82007360
7 changed files with 111 additions and 8 deletions
@@ -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}",
@@ -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) => {
@@ -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',
},
},
},
{
+4 -3
View File
@@ -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 {