From d55d066bf39fadb37592ed821fdec6d9dcec494b Mon Sep 17 00:00:00 2001 From: Shireen Missi <94372015+ShireenMissi@users.noreply.github.com> Date: Fri, 8 Nov 2024 11:32:27 +0000 Subject: [PATCH 1/8] chore: Update PR template with examples to automatically close Github Issues (no-changelog) (#11650) --- .github/pull_request_template.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 0df76d2700..0788c7c480 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -11,6 +11,8 @@ Photos and videos are recommended. Include links to **Linear ticket** or Github issue or Community forum post. Important in order to close *automatically* and provide context to reviewers. --> + + ## Review / Merge checklist From aec372793bae0d08434c14bb71e88e353f243b1e Mon Sep 17 00:00:00 2001 From: Ricardo Espinoza Date: Fri, 8 Nov 2024 07:46:03 -0500 Subject: [PATCH 2/8] refactor(editor): Migrate `templates.store` to composition API (#11641) --- .../components/Node/NodeCreator/viewsData.ts | 6 +- .../editor-ui/src/stores/templates.store.ts | 793 +++++++++--------- .../src/views/TemplatesCollectionView.vue | 4 +- 3 files changed, 413 insertions(+), 390 deletions(-) diff --git a/packages/editor-ui/src/components/Node/NodeCreator/viewsData.ts b/packages/editor-ui/src/components/Node/NodeCreator/viewsData.ts index 33d3626d7e..5f36a5bf19 100644 --- a/packages/editor-ui/src/components/Node/NodeCreator/viewsData.ts +++ b/packages/editor-ui/src/components/Node/NodeCreator/viewsData.ts @@ -141,6 +141,10 @@ export function AIView(_nodes: SimplifiedNodeType[]): NodeView { const chainNodes = getAiNodesBySubcategory(nodeTypesStore.allLatestNodeTypes, AI_CATEGORY_CHAINS); const agentNodes = getAiNodesBySubcategory(nodeTypesStore.allLatestNodeTypes, AI_CATEGORY_AGENTS); + const websiteCategoryURL = templatesStore.websiteTemplateRepositoryParameters; + + websiteCategoryURL.append('utm_user_role', 'AdvancedAI'); + return { value: AI_NODE_CREATOR_VIEW, title: i18n.baseText('nodeCreator.aiPanel.aiNodes'), @@ -154,7 +158,7 @@ export function AIView(_nodes: SimplifiedNodeType[]): NodeView { icon: 'box-open', description: i18n.baseText('nodeCreator.aiPanel.linkItem.description'), name: 'ai_templates_root', - url: templatesStore.getWebsiteCategoryURL(undefined, 'AdvancedAI'), + url: websiteCategoryURL.toString(), tag: { type: 'info', text: i18n.baseText('nodeCreator.triggerHelperPanel.manualTriggerTag'), diff --git a/packages/editor-ui/src/stores/templates.store.ts b/packages/editor-ui/src/stores/templates.store.ts index 36eceb120a..f96202c273 100644 --- a/packages/editor-ui/src/stores/templates.store.ts +++ b/packages/editor-ui/src/stores/templates.store.ts @@ -6,24 +6,17 @@ import type { ITemplatesCollection, ITemplatesCollectionFull, ITemplatesQuery, - ITemplateState, ITemplatesWorkflow, ITemplatesWorkflowFull, IWorkflowTemplate, } from '@/Interface'; import { useSettingsStore } from './settings.store'; -import { - getCategories, - getCollectionById, - getCollections, - getTemplateById, - getWorkflows, - getWorkflowTemplate, -} from '@/api/templates'; +import * as templatesApi from '@/api/templates'; import { getFixedNodesList } from '@/utils/nodeViewUtils'; import { useRootStore } from '@/stores/root.store'; import { useUsersStore } from './users.store'; import { useWorkflowsStore } from './workflows.store'; +import { computed, ref } from 'vue'; const TEMPLATES_PAGE_SIZE = 20; @@ -33,398 +26,424 @@ function getSearchKey(query: ITemplatesQuery): string { export type TemplatesStore = ReturnType; -export const useTemplatesStore = defineStore(STORES.TEMPLATES, { - state: (): ITemplateState => ({ - categories: [], - collections: {}, - workflows: {}, - collectionSearches: {}, - workflowSearches: {}, - currentSessionId: '', - previousSessionId: '', - currentN8nPath: `${window.location.protocol}//${window.location.host}${window.BASE_PATH}`, - }), - getters: { - allCategories(): ITemplatesCategory[] { - return Object.values(this.categories).sort((a: ITemplatesCategory, b: ITemplatesCategory) => - a.name > b.name ? 1 : -1, - ); - }, - getTemplateById() { - return (id: string): null | ITemplatesWorkflow => this.workflows[id]; - }, - getFullTemplateById() { - return (id: string): null | ITemplatesWorkflowFull => { - const template = this.workflows[id]; - return template && 'full' in template && template.full ? template : null; - }; - }, - getCollectionById() { - return (id: string): null | ITemplatesCollection => this.collections[id]; - }, - getCategoryById() { - return (id: string): null | ITemplatesCategory => this.categories[id as unknown as number]; - }, - getSearchedCollections() { - return (query: ITemplatesQuery) => { - const searchKey = getSearchKey(query); - const search = this.collectionSearches[searchKey]; - if (!search) { - return null; - } - - return search.collectionIds.map((collectionId: string) => this.collections[collectionId]); - }; - }, - getSearchedWorkflows() { - return (query: ITemplatesQuery) => { - const searchKey = getSearchKey(query); - const search = this.workflowSearches[searchKey]; - if (!search) { - return null; - } - - return search.workflowIds.map((workflowId: string) => this.workflows[workflowId]); - }; - }, - getSearchedWorkflowsTotal() { - return (query: ITemplatesQuery) => { - const searchKey = getSearchKey(query); - const search = this.workflowSearches[searchKey]; - - return search ? search.totalWorkflows : 0; - }; - }, - isSearchLoadingMore() { - return (query: ITemplatesQuery) => { - const searchKey = getSearchKey(query); - const search = this.workflowSearches[searchKey]; - - return Boolean(search && search.loadingMore); - }; - }, - isSearchFinished() { - return (query: ITemplatesQuery) => { - const searchKey = getSearchKey(query); - const search = this.workflowSearches[searchKey]; - - return Boolean( - search && !search.loadingMore && search.totalWorkflows === search.workflowIds.length, - ); - }; - }, - hasCustomTemplatesHost(): boolean { - const settingsStore = useSettingsStore(); - return settingsStore.templatesHost !== TEMPLATES_URLS.DEFAULT_API_HOST; - }, - /** - * Constructs URLSearchParams object based on the default parameters for the template repository - * and provided additional parameters - */ - websiteTemplateRepositoryParameters(_roleOverride?: string) { - const rootStore = useRootStore(); - const userStore = useUsersStore(); - const workflowsStore = useWorkflowsStore(); - const defaultParameters: Record = { - ...TEMPLATES_URLS.UTM_QUERY, - utm_instance: this.currentN8nPath, - utm_n8n_version: rootStore.versionCli, - utm_awc: String(workflowsStore.activeWorkflows.length), - }; - const userRole: string | null | undefined = - userStore.currentUserCloudInfo?.role ?? - (userStore.currentUser?.personalizationAnswers && - 'role' in userStore.currentUser.personalizationAnswers - ? userStore.currentUser.personalizationAnswers.role - : undefined); - - if (userRole) { - defaultParameters.utm_user_role = userRole; +export const useTemplatesStore = defineStore(STORES.TEMPLATES, () => { + const categories = ref([]); + const collections = ref>({}); + const workflows = ref>({}); + const workflowSearches = ref< + Record< + string, + { + workflowIds: string[]; + totalWorkflows: number; + loadingMore?: boolean; + categories?: ITemplatesCategory[]; } - return (additionalParameters: Record = {}) => { - return new URLSearchParams({ - ...defaultParameters, - ...additionalParameters, - }); - }; - }, - /** - * Construct the URL for the template repository on the website - * @returns {string} - */ - websiteTemplateRepositoryURL(): string { - return `${ - TEMPLATES_URLS.BASE_WEBSITE_URL - }?${this.websiteTemplateRepositoryParameters().toString()}`; - }, - /** - * Construct the URL for the template category page on the website for a given category id - */ - getWebsiteCategoryURL() { - return (id?: string, roleOverride?: string) => { - const payload: Record = {}; - if (id) { - payload.categories = id; - } - if (roleOverride) { - payload.utm_user_role = roleOverride; - } - return `${TEMPLATES_URLS.BASE_WEBSITE_URL}/?${this.websiteTemplateRepositoryParameters(payload).toString()}`; - }; - }, - }, - actions: { - addCategories(categories: ITemplatesCategory[]): void { - categories.forEach((category: ITemplatesCategory) => { - this.categories = { - ...this.categories, - [category.id]: category, - }; - }); - }, - addCollections(collections: Array): void { - collections.forEach((collection) => { - const workflows = (collection.workflows || []).map((workflow) => ({ id: workflow.id })); - const cachedCollection = this.collections[collection.id] || {}; - - this.collections = { - ...this.collections, - [collection.id]: { - ...cachedCollection, - ...collection, - workflows, - }, - }; - }); - }, - addWorkflows(workflows: Array): void { - workflows.forEach((workflow: ITemplatesWorkflow) => { - const cachedWorkflow = this.workflows[workflow.id] || {}; - - this.workflows = { - ...this.workflows, - [workflow.id]: { - ...cachedWorkflow, - ...workflow, - }, - }; - }); - }, - addCollectionSearch(data: { - collections: ITemplatesCollection[]; - query: ITemplatesQuery; - }): void { - const collectionIds = data.collections.map((collection) => String(collection.id)); - const searchKey = getSearchKey(data.query); - - this.collectionSearches = { - ...this.collectionSearches, - [searchKey]: { - collectionIds, - }, - }; - }, - addWorkflowsSearch(data: { - totalWorkflows: number; - workflows: ITemplatesWorkflow[]; - query: ITemplatesQuery; - }): void { - const workflowIds = data.workflows.map((workflow) => workflow.id); - const searchKey = getSearchKey(data.query); - const cachedResults = this.workflowSearches[searchKey]; - if (!cachedResults) { - this.workflowSearches = { - ...this.workflowSearches, - [searchKey]: { - workflowIds: workflowIds as unknown as string[], - totalWorkflows: data.totalWorkflows, - categories: this.categories, - }, - }; - - return; + > + >({}); + const collectionSearches = ref< + Record< + string, + { + collectionIds: string[]; } + > + >({}); + const currentSessionId = ref(''); + const previousSessionId = ref(''); + const currentN8nPath = ref( + `${window.location.protocol}//${window.location.host}${window.BASE_PATH}`, + ); - this.workflowSearches = { - ...this.workflowSearches, - [searchKey]: { - workflowIds: [...cachedResults.workflowIds, ...workflowIds] as string[], - totalWorkflows: data.totalWorkflows, - categories: this.categories, - }, - }; - }, - setWorkflowSearchLoading(query: ITemplatesQuery): void { + const settingsStore = useSettingsStore(); + const rootStore = useRootStore(); + const userStore = useUsersStore(); + const workflowsStore = useWorkflowsStore(); + + const allCategories = computed(() => { + return categories.value.sort((a: ITemplatesCategory, b: ITemplatesCategory) => + a.name > b.name ? 1 : -1, + ); + }); + + const getTemplatesById = computed(() => { + return (id: string): null | ITemplatesWorkflow => workflows.value[id]; + }); + + const getFullTemplateById = computed(() => { + return (id: string): null | ITemplatesWorkflowFull => { + const template = workflows.value[id]; + return template && 'full' in template && template.full ? template : null; + }; + }); + + const getCollectionById = computed(() => collections.value); + + const getCategoryById = computed(() => { + return (id: string): null | ITemplatesCategory => categories.value[id as unknown as number]; + }); + + const getSearchedCollections = computed(() => { + return (query: ITemplatesQuery) => { const searchKey = getSearchKey(query); - const cachedResults = this.workflowSearches[searchKey]; - if (!cachedResults) { - return; + const search = collectionSearches.value[searchKey]; + if (!search) { + return null; } - this.workflowSearches[searchKey] = { - ...this.workflowSearches[searchKey], - loadingMore: true, - }; - }, - setWorkflowSearchLoaded(query: ITemplatesQuery): void { + return search.collectionIds.map((collectionId: string) => collections.value[collectionId]); + }; + }); + + const getSearchedWorkflows = computed(() => { + return (query: ITemplatesQuery) => { const searchKey = getSearchKey(query); - const cachedResults = this.workflowSearches[searchKey]; - if (!cachedResults) { - return; + const search = workflowSearches.value[searchKey]; + if (!search) { + return null; } - this.workflowSearches[searchKey] = { - ...this.workflowSearches[searchKey], - loadingMore: false, + return search.workflowIds.map((workflowId: string) => workflows.value[workflowId]); + }; + }); + + const getSearchedWorkflowsTotal = computed(() => { + return (query: ITemplatesQuery) => { + const searchKey = getSearchKey(query); + const search = workflowSearches.value[searchKey]; + + return search ? search.totalWorkflows : 0; + }; + }); + + const isSearchLoadingMore = computed(() => { + return (query: ITemplatesQuery) => { + const searchKey = getSearchKey(query); + const search = workflowSearches.value[searchKey]; + + return Boolean(search && search.loadingMore); + }; + }); + + const isSearchFinished = computed(() => { + return (query: ITemplatesQuery) => { + const searchKey = getSearchKey(query); + const search = workflowSearches.value[searchKey]; + + return Boolean( + search && !search.loadingMore && search.totalWorkflows === search.workflowIds.length, + ); + }; + }); + + const hasCustomTemplatesHost = computed(() => { + return settingsStore.templatesHost !== TEMPLATES_URLS.DEFAULT_API_HOST; + }); + + const websiteTemplateRepositoryParameters = computed(() => { + const defaultParameters: Record = { + ...TEMPLATES_URLS.UTM_QUERY, + utm_instance: currentN8nPath.value, + utm_n8n_version: rootStore.versionCli, + utm_awc: String(workflowsStore.activeWorkflows.length), + }; + const userRole: string | null | undefined = + userStore.currentUserCloudInfo?.role ?? + (userStore.currentUser?.personalizationAnswers && + 'role' in userStore.currentUser.personalizationAnswers + ? userStore.currentUser.personalizationAnswers.role + : undefined); + + if (userRole) { + defaultParameters.utm_user_role = userRole; + } + return new URLSearchParams({ + ...defaultParameters, + }); + }); + + const websiteTemplateRepositoryURL = computed( + () => + `${TEMPLATES_URLS.BASE_WEBSITE_URL}?${websiteTemplateRepositoryParameters.value.toString()}`, + ); + + const addCategories = (_categories: ITemplatesCategory[]): void => { + categories.value = _categories; + }; + + const addCollections = ( + _collections: Array, + ): void => { + _collections.forEach((collection) => { + const workflows = (collection.workflows || []).map((workflow) => ({ id: workflow.id })); + const cachedCollection = collections.value[collection.id] || {}; + + collections.value[collection.id] = { + ...cachedCollection, + ...collection, + workflows, }; - }, - resetSessionId(): void { - this.previousSessionId = this.currentSessionId; - this.currentSessionId = ''; - }, - setSessionId(): void { - if (!this.currentSessionId) { - this.currentSessionId = `templates-${Date.now()}`; - } - }, - async fetchTemplateById(templateId: string): Promise { - const settingsStore = useSettingsStore(); - const rootStore = useRootStore(); - const apiEndpoint: string = settingsStore.templatesHost; - const versionCli: string = rootStore.versionCli; - const response = await getTemplateById(apiEndpoint, templateId, { - 'n8n-version': versionCli, + }); + }; + + const addWorkflows = (_workflows: Array): void => { + _workflows.forEach((workflow) => { + const cachedWorkflow = workflows.value[workflow.id] || {}; + workflows.value[workflow.id.toString()] = { ...cachedWorkflow, ...workflow }; + }); + }; + + const addCollectionsSearch = (data: { + _collections: ITemplatesCollection[]; + query: ITemplatesQuery; + }) => { + const collectionIds = data._collections.map((collection) => String(collection.id)); + const searchKey = getSearchKey(data.query); + + collectionSearches.value[searchKey] = { + collectionIds, + }; + }; + + const addWorkflowsSearch = (data: { + totalWorkflows: number; + workflows: ITemplatesWorkflow[]; + query: ITemplatesQuery; + }) => { + const workflowIds = data.workflows.map((workflow) => workflow.id); + const searchKey = getSearchKey(data.query); + const cachedResults = workflowSearches.value[searchKey]; + if (!cachedResults) { + workflowSearches.value[searchKey] = { + workflowIds: workflowIds as unknown as string[], + totalWorkflows: data.totalWorkflows, + categories: categories.value, + }; + return; + } + + workflowSearches.value[searchKey] = { + workflowIds: [...cachedResults.workflowIds, ...workflowIds] as string[], + totalWorkflows: data.totalWorkflows, + categories: categories.value, + }; + }; + + const setWorkflowSearchLoading = (query: ITemplatesQuery): void => { + const searchKey = getSearchKey(query); + const cachedResults = workflowSearches.value[searchKey]; + if (!cachedResults) { + return; + } + + workflowSearches.value[searchKey] = { + ...workflowSearches.value[searchKey], + loadingMore: true, + }; + }; + + const setWorkflowSearchLoaded = (query: ITemplatesQuery): void => { + const searchKey = getSearchKey(query); + const cachedResults = workflowSearches.value[searchKey]; + if (!cachedResults) { + return; + } + + workflowSearches.value[searchKey] = { + ...workflowSearches.value[searchKey], + loadingMore: false, + }; + }; + + const resetSessionId = (): void => { + previousSessionId.value = currentSessionId.value; + currentSessionId.value = ''; + }; + + const setSessionId = (): void => { + if (!currentSessionId.value) { + currentSessionId.value = `templates-${Date.now()}`; + } + }; + + const fetchTemplateById = async (templateId: string): Promise => { + const apiEndpoint: string = settingsStore.templatesHost; + const versionCli: string = rootStore.versionCli; + const response = await templatesApi.getTemplateById(apiEndpoint, templateId, { + 'n8n-version': versionCli, + }); + + const template: ITemplatesWorkflowFull = { + ...response.workflow, + full: true, + }; + addWorkflows([template]); + + return template; + }; + + const fetchCollectionById = async ( + collectionId: string, + ): Promise => { + const apiEndpoint: string = settingsStore.templatesHost; + const versionCli: string = rootStore.versionCli; + const response = await templatesApi.getCollectionById(apiEndpoint, collectionId, { + 'n8n-version': versionCli, + }); + const collection: ITemplatesCollectionFull = { + ...response.collection, + full: true, + }; + + addCollections([collection]); + addWorkflows(response.collection.workflows); + return getCollectionById.value[collectionId]; + }; + + const getCategories = async (): Promise => { + const cachedCategories = allCategories.value; + if (cachedCategories.length) { + return cachedCategories; + } + const apiEndpoint: string = settingsStore.templatesHost; + const versionCli: string = rootStore.versionCli; + const response = await templatesApi.getCategories(apiEndpoint, { + 'n8n-version': versionCli, + }); + const categories = response.categories; + + addCategories(categories); + return categories; + }; + + const getCollections = async (query: ITemplatesQuery): Promise => { + const cachedResults = getSearchedCollections.value(query); + if (cachedResults) { + return cachedResults; + } + + const apiEndpoint: string = settingsStore.templatesHost; + const versionCli: string = rootStore.versionCli; + const response = await templatesApi.getCollections(apiEndpoint, query, { + 'n8n-version': versionCli, + }); + const collections = response.collections; + + addCollections(collections); + addCollectionsSearch({ query, _collections: collections }); + collections.forEach((collection) => addWorkflows(collection.workflows as ITemplatesWorkflow[])); + + return collections; + }; + + const getWorkflows = async (query: ITemplatesQuery): Promise => { + const cachedResults = getSearchedWorkflows.value(query); + if (cachedResults) { + categories.value = workflowSearches.value[getSearchKey(query)].categories ?? []; + return cachedResults; + } + + const apiEndpoint: string = settingsStore.templatesHost; + const versionCli: string = rootStore.versionCli; + const payload = await templatesApi.getWorkflows( + apiEndpoint, + { ...query, page: 1, limit: TEMPLATES_PAGE_SIZE }, + { 'n8n-version': versionCli }, + ); + + addWorkflows(payload.workflows); + addWorkflowsSearch({ ...payload, query }); + return getSearchedWorkflows.value(query) || []; + }; + + const getMoreWorkflows = async (query: ITemplatesQuery): Promise => { + if (isSearchLoadingMore.value(query) && !isSearchFinished.value(query)) { + return []; + } + const cachedResults = getSearchedWorkflows.value(query) || []; + const apiEndpoint: string = settingsStore.templatesHost; + + setWorkflowSearchLoading(query); + try { + const payload = await templatesApi.getWorkflows(apiEndpoint, { + ...query, + page: cachedResults.length / TEMPLATES_PAGE_SIZE + 1, + limit: TEMPLATES_PAGE_SIZE, }); - const template: ITemplatesWorkflowFull = { - ...response.workflow, - full: true, - }; - this.addWorkflows([template]); + setWorkflowSearchLoaded(query); + addWorkflows(payload.workflows); + addWorkflowsSearch({ ...payload, query }); - return template; - }, - async fetchCollectionById(collectionId: string): Promise { - const settingsStore = useSettingsStore(); - const rootStore = useRootStore(); - const apiEndpoint: string = settingsStore.templatesHost; - const versionCli: string = rootStore.versionCli; - const response = await getCollectionById(apiEndpoint, collectionId, { - 'n8n-version': versionCli, + return getSearchedWorkflows.value(query) || []; + } catch (e) { + setWorkflowSearchLoaded(query); + throw e; + } + }; + + const getWorkflowTemplate = async (templateId: string): Promise => { + const apiEndpoint: string = settingsStore.templatesHost; + const versionCli: string = rootStore.versionCli; + return await templatesApi.getWorkflowTemplate(apiEndpoint, templateId, { + 'n8n-version': versionCli, + }); + }; + + const getFixedWorkflowTemplate = async ( + templateId: string, + ): Promise => { + const template = await getWorkflowTemplate(templateId); + if (template?.workflow?.nodes) { + template.workflow.nodes = getFixedNodesList(template.workflow.nodes) as INodeUi[]; + template.workflow.nodes?.forEach((node) => { + if (node.credentials) { + delete node.credentials; + } }); - const collection: ITemplatesCollectionFull = { - ...response.collection, - full: true, - }; + } - this.addCollections([collection]); - this.addWorkflows(response.collection.workflows); - return this.getCollectionById(collectionId); - }, - async getCategories(): Promise { - const cachedCategories = this.allCategories; - if (cachedCategories.length) { - return cachedCategories; - } - const settingsStore = useSettingsStore(); - const rootStore = useRootStore(); - const apiEndpoint: string = settingsStore.templatesHost; - const versionCli: string = rootStore.versionCli; - const response = await getCategories(apiEndpoint, { 'n8n-version': versionCli }); - const categories = response.categories; + return template; + }; - this.addCategories(categories); - return categories; - }, - async getCollections(query: ITemplatesQuery): Promise { - const cachedResults = this.getSearchedCollections(query); - if (cachedResults) { - return cachedResults; - } - - const settingsStore = useSettingsStore(); - const rootStore = useRootStore(); - const apiEndpoint: string = settingsStore.templatesHost; - const versionCli: string = rootStore.versionCli; - const response = await getCollections(apiEndpoint, query, { 'n8n-version': versionCli }); - const collections = response.collections; - - this.addCollections(collections); - this.addCollectionSearch({ query, collections }); - collections.forEach((collection) => - this.addWorkflows(collection.workflows as ITemplatesWorkflowFull[]), - ); - - return collections; - }, - async getWorkflows(query: ITemplatesQuery): Promise { - const cachedResults = this.getSearchedWorkflows(query); - if (cachedResults) { - this.categories = this.workflowSearches[getSearchKey(query)].categories ?? []; - return cachedResults; - } - - const settingsStore = useSettingsStore(); - const rootStore = useRootStore(); - const apiEndpoint: string = settingsStore.templatesHost; - const versionCli: string = rootStore.versionCli; - - const payload = await getWorkflows( - apiEndpoint, - { ...query, page: 1, limit: TEMPLATES_PAGE_SIZE }, - { 'n8n-version': versionCli }, - ); - - this.addWorkflows(payload.workflows); - this.addWorkflowsSearch({ ...payload, query }); - return this.getSearchedWorkflows(query) || []; - }, - async getMoreWorkflows(query: ITemplatesQuery): Promise { - if (this.isSearchLoadingMore(query) && !this.isSearchFinished(query)) { - return []; - } - const cachedResults = this.getSearchedWorkflows(query) || []; - const settingsStore = useSettingsStore(); - const apiEndpoint: string = settingsStore.templatesHost; - - this.setWorkflowSearchLoading(query); - try { - const payload = await getWorkflows(apiEndpoint, { - ...query, - page: cachedResults.length / TEMPLATES_PAGE_SIZE + 1, - limit: TEMPLATES_PAGE_SIZE, - }); - - this.setWorkflowSearchLoaded(query); - this.addWorkflows(payload.workflows); - this.addWorkflowsSearch({ ...payload, query }); - - return this.getSearchedWorkflows(query) || []; - } catch (e) { - this.setWorkflowSearchLoaded(query); - throw e; - } - }, - async getWorkflowTemplate(templateId: string): Promise { - const settingsStore = useSettingsStore(); - const rootStore = useRootStore(); - const apiEndpoint: string = settingsStore.templatesHost; - const versionCli: string = rootStore.versionCli; - return await getWorkflowTemplate(apiEndpoint, templateId, { 'n8n-version': versionCli }); - }, - - async getFixedWorkflowTemplate(templateId: string): Promise { - const template = await this.getWorkflowTemplate(templateId); - if (template?.workflow?.nodes) { - template.workflow.nodes = getFixedNodesList(template.workflow.nodes) as INodeUi[]; - template.workflow.nodes?.forEach((node) => { - if (node.credentials) { - delete node.credentials; - } - }); - } - - return template; - }, - }, + return { + categories, + collections, + workflows, + workflowSearches, + collectionSearches, + currentSessionId, + previousSessionId, + currentN8nPath, + allCategories, + getTemplatesById, + getFullTemplateById, + getCollectionById, + getCategoryById, + getSearchedCollections, + getSearchedWorkflows, + getSearchedWorkflowsTotal, + isSearchLoadingMore, + isSearchFinished, + hasCustomTemplatesHost, + websiteTemplateRepositoryURL, + websiteTemplateRepositoryParameters, + addCategories, + addCollections, + addWorkflows, + addCollectionsSearch, + addWorkflowsSearch, + setWorkflowSearchLoading, + setWorkflowSearchLoaded, + resetSessionId, + setSessionId, + fetchTemplateById, + fetchCollectionById, + getCategories, + getCollections, + getWorkflows, + getMoreWorkflows, + getWorkflowTemplate, + getFixedWorkflowTemplate, + }; }); diff --git a/packages/editor-ui/src/views/TemplatesCollectionView.vue b/packages/editor-ui/src/views/TemplatesCollectionView.vue index d5ddf2ade3..647c7e6eee 100644 --- a/packages/editor-ui/src/views/TemplatesCollectionView.vue +++ b/packages/editor-ui/src/views/TemplatesCollectionView.vue @@ -35,14 +35,14 @@ const collectionId = computed(() => { return Array.isArray(id) ? id[0] : id; }); -const collection = computed(() => templatesStore.getCollectionById(collectionId.value)); +const collection = computed(() => templatesStore.getCollectionById[collectionId.value]); const collectionWorkflows = computed(() => { if (!collection.value || loading.value) { return []; } return collection.value.workflows - .map(({ id }) => templatesStore.getTemplateById(id.toString())) + .map(({ id }) => templatesStore.getTemplatesById(id.toString())) .filter((workflow): workflow is ITemplatesWorkflow => !!workflow); }); From 38fefff3488e1fb2ad65f9c4f20c6c55cc0c2388 Mon Sep 17 00:00:00 2001 From: Ricardo Espinoza Date: Fri, 8 Nov 2024 08:11:36 -0500 Subject: [PATCH 3/8] refactor(editor): Migrate `ndv.store` to composition API (#11574) --- packages/editor-ui/src/Interface.ts | 41 ++ .../InlineExpressionTip.test.ts | 15 +- .../InlineExpressionTip.vue | 2 +- .../src/components/NDVDraggablePanels.vue | 18 +- .../src/components/NodeExecuteButton.vue | 2 +- .../editor-ui/src/components/OutputPanel.vue | 2 +- .../src/components/ParameterInput.test.ts | 3 +- .../src/components/ParameterInput.vue | 2 +- packages/editor-ui/src/components/RunData.vue | 8 +- packages/editor-ui/src/stores/ndv.store.ts | 680 ++++++++++-------- 10 files changed, 458 insertions(+), 315 deletions(-) diff --git a/packages/editor-ui/src/Interface.ts b/packages/editor-ui/src/Interface.ts index b4e8f7cc4b..11c30c67c3 100644 --- a/packages/editor-ui/src/Interface.ts +++ b/packages/editor-ui/src/Interface.ts @@ -1585,3 +1585,44 @@ export type ApiKey = { createdAt: string; updatedAt: string; }; + +export type InputPanel = { + displayMode: IRunDataDisplayMode; + nodeName?: string; + run?: number; + branch?: number; + data: { + isEmpty: boolean; + }; +}; + +export type OutputPanel = { + branch?: number; + displayMode: IRunDataDisplayMode; + data: { + isEmpty: boolean; + }; + editMode: { + enabled: boolean; + value: string; + }; +}; + +export type Draggable = { + isDragging: boolean; + type: string; + data: string; + dimensions: DOMRect | null; + activeTarget: { id: string; stickyPosition: null | XYPosition } | null; +}; + +export type MainPanelType = 'regular' | 'dragless' | 'inputless' | 'unknown' | 'wide'; + +export type MainPanelDimensions = Record< + MainPanelType, + { + relativeLeft: number; + relativeRight: number; + relativeWidth: number; + } +>; diff --git a/packages/editor-ui/src/components/InlineExpressionEditor/InlineExpressionTip.test.ts b/packages/editor-ui/src/components/InlineExpressionEditor/InlineExpressionTip.test.ts index 604152c29d..5242a53290 100644 --- a/packages/editor-ui/src/components/InlineExpressionEditor/InlineExpressionTip.test.ts +++ b/packages/editor-ui/src/components/InlineExpressionEditor/InlineExpressionTip.test.ts @@ -26,7 +26,8 @@ describe('InlineExpressionTip.vue', () => { beforeEach(() => { mockNdvState = { hasInputData: true, - isNDVDataEmpty: vi.fn(() => true), + isInputPanelEmpty: true, + isOutputPanelEmpty: true, setHighlightDraggables: vi.fn(), }; }); @@ -42,7 +43,8 @@ describe('InlineExpressionTip.vue', () => { test('should show the drag-n-drop tip', async () => { mockNdvState = { hasInputData: true, - isNDVDataEmpty: vi.fn(() => false), + isInputPanelEmpty: false, + isOutputPanelEmpty: false, focusedMappableInput: 'Some Input', setHighlightDraggables: vi.fn(), }; @@ -62,7 +64,8 @@ describe('InlineExpressionTip.vue', () => { mockNdvState = { hasInputData: false, isInputParentOfActiveNode: true, - isNDVDataEmpty: vi.fn(() => false), + isInputPanelEmpty: false, + isOutputPanelEmpty: false, focusedMappableInput: 'Some Input', setHighlightDraggables: vi.fn(), }; @@ -77,7 +80,8 @@ describe('InlineExpressionTip.vue', () => { test('should show the correct tip for objects', async () => { mockNdvState = { hasInputData: true, - isNDVDataEmpty: vi.fn(() => false), + isInputPanelEmpty: false, + isOutputPanelEmpty: false, focusedMappableInput: 'Some Input', setHighlightDraggables: vi.fn(), }; @@ -106,7 +110,8 @@ describe('InlineExpressionTip.vue', () => { test('should show the correct tip for primitives', async () => { mockNdvState = { hasInputData: true, - isNDVDataEmpty: vi.fn(() => false), + isInputPanelEmpty: false, + isOutputPanelEmpty: false, focusedMappableInput: 'Some Input', setHighlightDraggables: vi.fn(), }; diff --git a/packages/editor-ui/src/components/InlineExpressionEditor/InlineExpressionTip.vue b/packages/editor-ui/src/components/InlineExpressionEditor/InlineExpressionTip.vue index 19258d2df9..9abd13dda6 100644 --- a/packages/editor-ui/src/components/InlineExpressionEditor/InlineExpressionTip.vue +++ b/packages/editor-ui/src/components/InlineExpressionEditor/InlineExpressionTip.vue @@ -30,7 +30,7 @@ const canAddDotToExpression = ref(false); const resolvedExpressionHasFields = ref(false); const canDragToFocusedInput = computed( - () => !ndvStore.isNDVDataEmpty('input') && ndvStore.focusedMappableInput, + () => !ndvStore.isInputPanelEmpty && ndvStore.focusedMappableInput, ); const emptyExpression = computed(() => props.unresolvedExpression.trim().length === 0); diff --git a/packages/editor-ui/src/components/NDVDraggablePanels.vue b/packages/editor-ui/src/components/NDVDraggablePanels.vue index 537c0dceac..6ece740eb4 100644 --- a/packages/editor-ui/src/components/NDVDraggablePanels.vue +++ b/packages/editor-ui/src/components/NDVDraggablePanels.vue @@ -9,7 +9,7 @@ import { useNDVStore } from '@/stores/ndv.store'; import { ndvEventBus } from '@/event-bus'; import NDVFloatingNodes from '@/components/NDVFloatingNodes.vue'; import { useDebounce } from '@/composables/useDebounce'; -import type { XYPosition } from '@/Interface'; +import type { MainPanelType, XYPosition } from '@/Interface'; import { ref, onMounted, onBeforeUnmount, computed, watch } from 'vue'; import { useUIStore } from '@/stores/ui.store'; @@ -20,7 +20,7 @@ const PANEL_WIDTH = 350; const PANEL_WIDTH_LARGE = 420; const MIN_WINDOW_WIDTH = 2 * (SIDE_MARGIN + SIDE_PANELS_MARGIN) + MIN_PANEL_WIDTH; -const initialMainPanelWidth: { [key: string]: number } = { +const initialMainPanelWidth: Record = { regular: MAIN_NODE_PANEL_WIDTH, dragless: MAIN_NODE_PANEL_WIDTH, unknown: MAIN_NODE_PANEL_WIDTH, @@ -106,22 +106,16 @@ watch(containerWidth, (width) => { setPositions(mainPanelDimensions.value.relativeLeft); }); -const currentNodePaneType = computed((): string => { +const currentNodePaneType = computed((): MainPanelType => { if (!hasInputSlot.value) return 'inputless'; if (!props.isDraggable) return 'dragless'; if (props.nodeType === null) return 'unknown'; return props.nodeType.parameterPane ?? 'regular'; }); -const mainPanelDimensions = computed( - (): { - relativeWidth: number; - relativeLeft: number; - relativeRight: number; - } => { - return ndvStore.getMainPanelDimensions(currentNodePaneType.value); - }, -); +const mainPanelDimensions = computed(() => { + return ndvStore.mainPanelDimensions[currentNodePaneType.value]; +}); const calculatedPositions = computed( (): { inputPanelRelativeRight: number; outputPanelRelativeLeft: number } => { diff --git a/packages/editor-ui/src/components/NodeExecuteButton.vue b/packages/editor-ui/src/components/NodeExecuteButton.vue index e130f400ea..43b4dfa7dc 100644 --- a/packages/editor-ui/src/components/NodeExecuteButton.vue +++ b/packages/editor-ui/src/components/NodeExecuteButton.vue @@ -316,7 +316,7 @@ async function onClick() { codeGenerationInProgress.value = false; } - if (isChatNode.value || (isChatChild.value && ndvStore.isNDVDataEmpty('input'))) { + if (isChatNode.value || (isChatChild.value && ndvStore.isInputPanelEmpty)) { ndvStore.setActiveNodeName(null); nodeViewEventBus.emit('openChat'); } else if (isListeningForEvents.value) { diff --git a/packages/editor-ui/src/components/OutputPanel.vue b/packages/editor-ui/src/components/OutputPanel.vue index 10d87eb810..0503ad5c94 100644 --- a/packages/editor-ui/src/components/OutputPanel.vue +++ b/packages/editor-ui/src/components/OutputPanel.vue @@ -78,7 +78,7 @@ const { isSubNodeType } = useNodeType({ }); const pinnedData = usePinnedData(activeNode, { runIndex: props.runIndex, - displayMode: ndvStore.getPanelDisplayMode('output'), + displayMode: ndvStore.outputPanelDisplayMode, }); // Data diff --git a/packages/editor-ui/src/components/ParameterInput.test.ts b/packages/editor-ui/src/components/ParameterInput.test.ts index 05637e3903..121c17c0d0 100644 --- a/packages/editor-ui/src/components/ParameterInput.test.ts +++ b/packages/editor-ui/src/components/ParameterInput.test.ts @@ -54,7 +54,8 @@ describe('ParameterInput.vue', () => { type: 'test', typeVersion: 1, }, - isNDVDataEmpty: vi.fn(() => false), + isInputPanelEmpty: false, + isOutputPanelEmpty: false, }; mockNodeTypesState = { allNodeTypes: [], diff --git a/packages/editor-ui/src/components/ParameterInput.vue b/packages/editor-ui/src/components/ParameterInput.vue index 8162e854a2..0339c561db 100644 --- a/packages/editor-ui/src/components/ParameterInput.vue +++ b/packages/editor-ui/src/components/ParameterInput.vue @@ -523,7 +523,7 @@ const isHtmlNode = computed(() => !!node.value && node.value.type === HTML_NODE_ const isInputTypeString = computed(() => props.parameter.type === 'string'); const isInputTypeNumber = computed(() => props.parameter.type === 'number'); -const isInputDataEmpty = computed(() => ndvStore.isNDVDataEmpty('input')); +const isInputDataEmpty = computed(() => ndvStore.isInputPanelEmpty); const isDropDisabled = computed( () => props.parameter.noDataExpression || diff --git a/packages/editor-ui/src/components/RunData.vue b/packages/editor-ui/src/components/RunData.vue index 351fdd8890..e1f74f215b 100644 --- a/packages/editor-ui/src/components/RunData.vue +++ b/packages/editor-ui/src/components/RunData.vue @@ -185,12 +185,17 @@ const node = toRef(props, 'node'); const pinnedData = usePinnedData(node, { runIndex: props.runIndex, - displayMode: ndvStore.getPanelDisplayMode(props.paneType), + displayMode: + props.paneType === 'input' ? ndvStore.inputPanelDisplayMode : ndvStore.outputPanelDisplayMode, }); const { isSubNodeType } = useNodeType({ node, }); +const displayMode = computed(() => + props.paneType === 'input' ? ndvStore.inputPanelDisplayMode : ndvStore.outputPanelDisplayMode, +); + const isReadOnlyRoute = computed(() => route.meta.readOnlyCanvas === true); const isWaitNodeWaiting = computed( () => @@ -200,7 +205,6 @@ const isWaitNodeWaiting = computed( ); const { activeNode } = storeToRefs(ndvStore); -const displayMode = computed(() => ndvStore.getPanelDisplayMode(props.paneType)); const nodeType = computed(() => { if (!node.value) return null; diff --git a/packages/editor-ui/src/stores/ndv.store.ts b/packages/editor-ui/src/stores/ndv.store.ts index e3be5db5fe..137c8ac828 100644 --- a/packages/editor-ui/src/stores/ndv.store.ts +++ b/packages/editor-ui/src/stores/ndv.store.ts @@ -1,10 +1,13 @@ import type { - INodeUi, + Draggable, + InputPanel, IRunDataDisplayMode, + MainPanelDimensions, + MainPanelType, NDVState, NodePanelType, + OutputPanel, TargetItem, - XYPosition, } from '@/Interface'; import { useStorage } from '@/composables/useStorage'; import { @@ -13,316 +16,411 @@ import { LOCAL_STORAGE_TABLE_HOVER_IS_ONBOARDED, STORES, } from '@/constants'; -import type { INodeExecutionData, INodeIssues } from 'n8n-workflow'; +import type { INodeIssues } from 'n8n-workflow'; import { NodeConnectionType } from 'n8n-workflow'; import { defineStore } from 'pinia'; import { v4 as uuid } from 'uuid'; import { useWorkflowsStore } from './workflows.store'; +import { computed, ref } from 'vue'; -export const useNDVStore = defineStore(STORES.NDV, { - state: (): NDVState => ({ - activeNodeName: null, - mainPanelDimensions: {}, - pushRef: '', - input: { - displayMode: 'schema', - nodeName: undefined, - run: undefined, - branch: undefined, - data: { - isEmpty: true, - }, +const DEFAULT_MAIN_PANEL_DIMENSIONS = { + relativeLeft: 1, + relativeRight: 1, + relativeWidth: 1, +}; + +export const useNDVStore = defineStore(STORES.NDV, () => { + const localStorageMappingIsOnboarded = useStorage(LOCAL_STORAGE_MAPPING_IS_ONBOARDED); + const localStorageTableHoverIsOnboarded = useStorage(LOCAL_STORAGE_TABLE_HOVER_IS_ONBOARDED); + const localStorageAutoCompleteIsOnboarded = useStorage(LOCAL_STORAGE_AUTOCOMPLETE_IS_ONBOARDED); + + const activeNodeName = ref(null); + const mainPanelDimensions = ref({ + unknown: { ...DEFAULT_MAIN_PANEL_DIMENSIONS }, + regular: { ...DEFAULT_MAIN_PANEL_DIMENSIONS }, + dragless: { ...DEFAULT_MAIN_PANEL_DIMENSIONS }, + inputless: { ...DEFAULT_MAIN_PANEL_DIMENSIONS }, + wide: { ...DEFAULT_MAIN_PANEL_DIMENSIONS }, + }); + const pushRef = ref(''); + const input = ref({ + displayMode: 'schema', + nodeName: undefined, + run: undefined, + branch: undefined, + data: { + isEmpty: true, }, - output: { - displayMode: 'table', - branch: undefined, - data: { - isEmpty: true, - }, - editMode: { - enabled: false, - value: '', - }, + }); + const output = ref({ + displayMode: 'table', + branch: undefined, + data: { + isEmpty: true, }, - focusedMappableInput: '', - focusedInputPath: '', - mappingTelemetry: {}, - hoveringItem: null, - expressionOutputItemIndex: 0, - draggable: { + editMode: { + enabled: false, + value: '', + }, + }); + const focusedMappableInput = ref(''); + const focusedInputPath = ref(''); + const mappingTelemetry = ref>({}); + const hoveringItem = ref(null); + const expressionOutputItemIndex = ref(0); + const draggable = ref({ + isDragging: false, + type: '', + data: '', + dimensions: null, + activeTarget: null, + }); + const isMappingOnboarded = ref(localStorageMappingIsOnboarded.value === 'true'); + const isTableHoverOnboarded = ref(localStorageTableHoverIsOnboarded.value === 'true'); + + const isAutocompleteOnboarded = ref(localStorageAutoCompleteIsOnboarded.value === 'true'); + + const highlightDraggables = ref(false); + + const workflowsStore = useWorkflowsStore(); + + const activeNode = computed(() => { + return workflowsStore.getNodeByName(activeNodeName.value || ''); + }); + + const ndvInputData = computed(() => { + const executionData = workflowsStore.getWorkflowExecution; + const inputNodeName: string | undefined = input.value.nodeName; + const inputRunIndex: number = input.value.run ?? 0; + const inputBranchIndex: number = input.value.branch ?? 0; + + if ( + !executionData || + !inputNodeName || + inputRunIndex === undefined || + inputBranchIndex === undefined + ) { + return []; + } + + return ( + executionData.data?.resultData?.runData?.[inputNodeName]?.[inputRunIndex]?.data?.main?.[ + inputBranchIndex + ] ?? [] + ); + }); + + const ndvInputNodeName = computed(() => { + return input.value.nodeName; + }); + + const ndvInputDataWithPinnedData = computed(() => { + const data = ndvInputData.value; + return ndvInputNodeName.value + ? (workflowsStore.pinDataByNodeName(ndvInputNodeName.value) ?? data) + : data; + }); + + const hasInputData = computed(() => { + return ndvInputDataWithPinnedData.value.length > 0; + }); + + const inputPanelDisplayMode = computed(() => input.value.displayMode); + + const outputPanelDisplayMode = computed(() => output.value.displayMode); + + const isDraggableDragging = computed(() => draggable.value.isDragging); + + const draggableType = computed(() => draggable.value.type); + + const draggableData = computed(() => draggable.value.data); + + const canDraggableDrop = computed(() => draggable.value.activeTarget !== null); + + const outputPanelEditMode = computed(() => output.value.editMode); + + const draggableStickyPos = computed(() => draggable.value.activeTarget?.stickyPosition ?? null); + + const ndvNodeInputNumber = computed(() => { + const returnData: { [nodeName: string]: number[] } = {}; + const workflow = workflowsStore.getCurrentWorkflow(); + const activeNodeConections = ( + workflow.connectionsByDestinationNode[activeNode.value?.name || ''] ?? {} + ).main; + + if (!activeNodeConections || activeNodeConections.length < 2) return returnData; + + for (const [index, connection] of activeNodeConections.entries()) { + for (const node of connection) { + if (!returnData[node.node]) { + returnData[node.node] = []; + } + returnData[node.node].push(index + 1); + } + } + + return returnData; + }); + + const ndvInputRunIndex = computed(() => input.value.run); + + const ndvInputBranchIndex = computed(() => input.value.branch); + + const isInputPanelEmpty = computed(() => input.value.data.isEmpty); + + const isOutputPanelEmpty = computed(() => output.value.data.isEmpty); + + const isInputParentOfActiveNode = computed(() => { + const inputNodeName = ndvInputNodeName.value; + if (!activeNode.value || !inputNodeName) { + return false; + } + const workflow = workflowsStore.getCurrentWorkflow(); + const parentNodes = workflow.getParentNodes(activeNode.value.name, NodeConnectionType.Main, 1); + return parentNodes.includes(inputNodeName); + }); + + const getHoveringItem = computed(() => { + if (isInputParentOfActiveNode.value) { + return hoveringItem.value; + } + + return null; + }); + + const expressionTargetItem = computed(() => { + if (getHoveringItem.value) { + return getHoveringItem.value; + } + + if (expressionOutputItemIndex.value && ndvInputNodeName.value) { + return { + nodeName: ndvInputNodeName.value, + runIndex: ndvInputRunIndex.value ?? 0, + outputIndex: ndvInputBranchIndex.value ?? 0, + itemIndex: expressionOutputItemIndex.value, + }; + } + + return null; + }); + + const isNDVOpen = computed(() => activeNodeName.value !== null); + + const setActiveNodeName = (nodeName: string | null): void => { + activeNodeName.value = nodeName; + }; + + const setInputNodeName = (nodeName: string | undefined): void => { + input.value.nodeName = nodeName; + }; + + const setInputRunIndex = (run?: number): void => { + input.value.run = run; + }; + + const setMainPanelDimensions = (params: { + panelType: MainPanelType; + dimensions: { relativeLeft?: number; relativeRight?: number; relativeWidth?: number }; + }): void => { + mainPanelDimensions.value[params.panelType] = { + ...mainPanelDimensions.value[params.panelType], + ...params.dimensions, + }; + }; + + const setNDVPushRef = (): void => { + pushRef.value = `ndv-${uuid()}`; + }; + + const resetNDVPushRef = (): void => { + pushRef.value = ''; + }; + + const setPanelDisplayMode = (params: { + pane: NodePanelType; + mode: IRunDataDisplayMode; + }): void => { + if (params.pane === 'input') { + input.value.displayMode = params.mode; + } else { + output.value.displayMode = params.mode; + } + }; + + const setOutputPanelEditModeEnabled = (isEnabled: boolean): void => { + output.value.editMode.enabled = isEnabled; + }; + + const setOutputPanelEditModeValue = (payload: string): void => { + output.value.editMode.value = payload; + }; + + const setMappableNDVInputFocus = (paramName: string): void => { + focusedMappableInput.value = paramName; + }; + + const draggableStartDragging = ({ + type, + data, + dimensions, + }: { type: string; data: string; dimensions: DOMRect | null }): void => { + draggable.value = { + isDragging: true, + type, + data, + dimensions, + activeTarget: null, + }; + }; + + const draggableStopDragging = (): void => { + draggable.value = { isDragging: false, type: '', data: '', dimensions: null, activeTarget: null, - }, - isMappingOnboarded: useStorage(LOCAL_STORAGE_MAPPING_IS_ONBOARDED).value === 'true', - isTableHoverOnboarded: useStorage(LOCAL_STORAGE_TABLE_HOVER_IS_ONBOARDED).value === 'true', - isAutocompleteOnboarded: useStorage(LOCAL_STORAGE_AUTOCOMPLETE_IS_ONBOARDED).value === 'true', - highlightDraggables: false, - }), - getters: { - activeNode(): INodeUi | null { - const workflowsStore = useWorkflowsStore(); - return workflowsStore.getNodeByName(this.activeNodeName || ''); - }, - ndvInputData(): INodeExecutionData[] { - const workflowsStore = useWorkflowsStore(); - const executionData = workflowsStore.getWorkflowExecution; - const inputNodeName: string | undefined = this.input.nodeName; - const inputRunIndex: number = this.input.run ?? 0; - const inputBranchIndex: number = this.input.branch ?? 0; + }; + }; - if ( - !executionData || - !inputNodeName || - inputRunIndex === undefined || - inputBranchIndex === undefined - ) { - return []; - } + const setDraggableTarget = (target: NDVState['draggable']['activeTarget']): void => { + draggable.value.activeTarget = target; + }; - return ( - executionData.data?.resultData?.runData?.[inputNodeName]?.[inputRunIndex]?.data?.main?.[ - inputBranchIndex - ] ?? [] - ); - }, - ndvInputDataWithPinnedData(): INodeExecutionData[] { - const data = this.ndvInputData; - return this.ndvInputNodeName - ? (useWorkflowsStore().pinDataByNodeName(this.ndvInputNodeName) ?? data) - : data; - }, - hasInputData(): boolean { - return this.ndvInputDataWithPinnedData.length > 0; - }, - getPanelDisplayMode() { - return (panel: NodePanelType) => this[panel].displayMode; - }, - inputPanelDisplayMode(): IRunDataDisplayMode { - return this.input.displayMode; - }, - outputPanelDisplayMode(): IRunDataDisplayMode { - return this.output.displayMode; - }, - isDraggableDragging(): boolean { - return this.draggable.isDragging; - }, - draggableType(): string { - return this.draggable.type; - }, - draggableData(): string { - return this.draggable.data; - }, - canDraggableDrop(): boolean { - return this.draggable.activeTarget !== null; - }, - outputPanelEditMode(): NDVState['output']['editMode'] { - return this.output.editMode; - }, - getMainPanelDimensions() { - return (panelType: string) => { - const defaults = { relativeRight: 1, relativeLeft: 1, relativeWidth: 1 }; - return { ...defaults, ...this.mainPanelDimensions[panelType] }; - }; - }, - draggableStickyPos(): XYPosition | null { - return this.draggable.activeTarget?.stickyPosition ?? null; - }, - ndvInputNodeName(): string | undefined { - return this.input.nodeName; - }, - ndvInputRunIndex(): number | undefined { - return this.input.run; - }, - ndvInputBranchIndex(): number | undefined { - return this.input.branch; - }, - isNDVDataEmpty() { - return (panel: 'input' | 'output'): boolean => this[panel].data.isEmpty; - }, - isInputParentOfActiveNode(): boolean { - const inputNodeName = this.ndvInputNodeName; - if (!this.activeNode || !inputNodeName) { - return false; - } - const workflow = useWorkflowsStore().getCurrentWorkflow(); - const parentNodes = workflow.getParentNodes(this.activeNode.name, NodeConnectionType.Main, 1); - return parentNodes.includes(inputNodeName); - }, - getHoveringItem(): TargetItem | null { - if (this.isInputParentOfActiveNode) { - return this.hoveringItem; - } + const setMappingTelemetry = (telemetry: { [key: string]: string | number | boolean }): void => { + mappingTelemetry.value = { ...mappingTelemetry.value, ...telemetry }; + }; - return null; - }, - expressionTargetItem(): TargetItem | null { - if (this.getHoveringItem) { - return this.getHoveringItem; - } + const resetMappingTelemetry = (): void => { + mappingTelemetry.value = {}; + }; - if (this.expressionOutputItemIndex && this.ndvInputNodeName) { - return { - nodeName: this.ndvInputNodeName, - runIndex: this.ndvInputRunIndex ?? 0, - outputIndex: this.ndvInputBranchIndex ?? 0, - itemIndex: this.expressionOutputItemIndex, - }; - } + const setHoveringItem = (item: TargetItem | null): void => { + if (item) setTableHoverOnboarded(); + hoveringItem.value = item; + }; - return null; - }, - isNDVOpen(): boolean { - return this.activeNodeName !== null; - }, - ndvNodeInputNumber() { - const returnData: { [nodeName: string]: number[] } = {}; - const workflow = useWorkflowsStore().getCurrentWorkflow(); - const activeNodeConections = ( - workflow.connectionsByDestinationNode[this.activeNode?.name || ''] ?? {} - ).main; + const setNDVBranchIndex = (e: { pane: NodePanelType; branchIndex: number }): void => { + if (e.pane === 'input') { + input.value.branch = e.branchIndex; + } else { + output.value.branch = e.branchIndex; + } + }; - if (!activeNodeConections || activeNodeConections.length < 2) return returnData; + const setNDVPanelDataIsEmpty = (params: { + panel: NodePanelType; + isEmpty: boolean; + }): void => { + if (params.panel === 'input') { + input.value.data.isEmpty = params.isEmpty; + } else { + output.value.data.isEmpty = params.isEmpty; + } + }; - for (const [index, connection] of activeNodeConections.entries()) { - for (const node of connection) { - if (!returnData[node.node]) { - returnData[node.node] = []; - } - returnData[node.node].push(index + 1); - } - } + const setMappingOnboarded = () => { + isMappingOnboarded.value = true; + localStorageMappingIsOnboarded.value = 'true'; + }; - return returnData; - }, - }, - actions: { - setActiveNodeName(nodeName: string | null): void { - this.activeNodeName = nodeName; - }, - setInputNodeName(nodeName: string | undefined): void { - this.input = { - ...this.input, - nodeName, - }; - }, - setInputRunIndex(run?: number): void { - this.input = { - ...this.input, - run, - }; - }, - setMainPanelDimensions(params: { - panelType: string; - dimensions: { relativeLeft?: number; relativeRight?: number; relativeWidth?: number }; - }): void { - this.mainPanelDimensions = { - ...this.mainPanelDimensions, - [params.panelType]: { - ...this.mainPanelDimensions[params.panelType], - ...params.dimensions, + const setTableHoverOnboarded = () => { + isTableHoverOnboarded.value = true; + localStorageTableHoverIsOnboarded.value = 'true'; + }; + + const setAutocompleteOnboarded = () => { + isAutocompleteOnboarded.value = true; + localStorageAutoCompleteIsOnboarded.value = 'true'; + }; + + const setHighlightDraggables = (highlight: boolean) => { + highlightDraggables.value = highlight; + }; + + const updateNodeParameterIssues = (issues: INodeIssues): void => { + const activeNode = workflowsStore.getNodeByName(activeNodeName.value || ''); + + if (activeNode) { + const nodeIndex = workflowsStore.workflow.nodes.findIndex((node) => { + return node.name === activeNode.name; + }); + + workflowsStore.updateNodeAtIndex(nodeIndex, { + issues: { + ...activeNode.issues, + ...issues, }, - }; - }, - setNDVPushRef(): void { - this.pushRef = `ndv-${uuid()}`; - }, - resetNDVPushRef(): void { - this.pushRef = ''; - }, - setPanelDisplayMode(params: { pane: NodePanelType; mode: IRunDataDisplayMode }): void { - this[params.pane].displayMode = params.mode; - }, - setOutputPanelEditModeEnabled(isEnabled: boolean): void { - this.output.editMode.enabled = isEnabled; - }, - setOutputPanelEditModeValue(payload: string): void { - this.output.editMode.value = payload; - }, - setMappableNDVInputFocus(paramName: string): void { - this.focusedMappableInput = paramName; - }, - draggableStartDragging({ - type, - data, - dimensions, - }: { - type: string; - data: string; - dimensions: DOMRect | null; - }): void { - this.draggable = { - isDragging: true, - type, - data, - dimensions, - activeTarget: null, - }; - }, - draggableStopDragging(): void { - this.draggable = { - isDragging: false, - type: '', - data: '', - dimensions: null, - activeTarget: null, - }; - }, - setDraggableTarget(target: NDVState['draggable']['activeTarget']): void { - this.draggable.activeTarget = target; - }, - setMappingTelemetry(telemetry: { [key: string]: string | number | boolean }): void { - this.mappingTelemetry = { ...this.mappingTelemetry, ...telemetry }; - }, - resetMappingTelemetry(): void { - this.mappingTelemetry = {}; - }, - setHoveringItem(item: null | NDVState['hoveringItem']): void { - if (item) this.setTableHoverOnboarded(); - this.hoveringItem = item; - }, - setNDVBranchIndex(e: { pane: 'input' | 'output'; branchIndex: number }): void { - this[e.pane].branch = e.branchIndex; - }, - setNDVPanelDataIsEmpty(payload: { panel: 'input' | 'output'; isEmpty: boolean }): void { - this[payload.panel].data.isEmpty = payload.isEmpty; - }, - setMappingOnboarded() { - this.isMappingOnboarded = true; - useStorage(LOCAL_STORAGE_MAPPING_IS_ONBOARDED).value = 'true'; - }, - setTableHoverOnboarded() { - this.isTableHoverOnboarded = true; - useStorage(LOCAL_STORAGE_TABLE_HOVER_IS_ONBOARDED).value = 'true'; - }, - setAutocompleteOnboarded() { - this.isAutocompleteOnboarded = true; - useStorage(LOCAL_STORAGE_AUTOCOMPLETE_IS_ONBOARDED).value = 'true'; - }, - setHighlightDraggables(highlight: boolean) { - this.highlightDraggables = highlight; - }, - updateNodeParameterIssues(issues: INodeIssues): void { - const workflowsStore = useWorkflowsStore(); - const activeNode = workflowsStore.getNodeByName(this.activeNodeName || ''); + }); + } + }; - if (activeNode) { - const nodeIndex = workflowsStore.workflow.nodes.findIndex((node) => { - return node.name === activeNode.name; - }); + const setFocusedInputPath = (path: string) => { + focusedInputPath.value = path; + }; - workflowsStore.updateNodeAtIndex(nodeIndex, { - issues: { - ...activeNode.issues, - ...issues, - }, - }); - } - }, - setFocusedInputPath(path: string) { - this.focusedInputPath = path; - }, - }, + return { + activeNode, + ndvInputData, + ndvInputNodeName, + ndvInputDataWithPinnedData, + hasInputData, + inputPanelDisplayMode, + outputPanelDisplayMode, + isDraggableDragging, + draggableType, + draggableData, + canDraggableDrop, + outputPanelEditMode, + draggableStickyPos, + ndvNodeInputNumber, + ndvInputRunIndex, + ndvInputBranchIndex, + isInputParentOfActiveNode, + getHoveringItem, + expressionTargetItem, + isNDVOpen, + isInputPanelEmpty, + isOutputPanelEmpty, + focusedMappableInput, + isMappingOnboarded, + pushRef, + activeNodeName, + focusedInputPath, + input, + output, + hoveringItem, + highlightDraggables, + mappingTelemetry, + draggable, + isAutocompleteOnboarded, + expressionOutputItemIndex, + isTableHoverOnboarded, + mainPanelDimensions, + setActiveNodeName, + setInputNodeName, + setInputRunIndex, + setMainPanelDimensions, + setNDVPushRef, + resetNDVPushRef, + setPanelDisplayMode, + setOutputPanelEditModeEnabled, + setOutputPanelEditModeValue, + setMappableNDVInputFocus, + draggableStartDragging, + draggableStopDragging, + setDraggableTarget, + setMappingTelemetry, + resetMappingTelemetry, + setHoveringItem, + setNDVBranchIndex, + setNDVPanelDataIsEmpty, + setMappingOnboarded, + setTableHoverOnboarded, + setAutocompleteOnboarded, + setHighlightDraggables, + updateNodeParameterIssues, + setFocusedInputPath, + }; }); From 74582290c04d2dd32300b1a6c7715862ae837d34 Mon Sep 17 00:00:00 2001 From: Michael Kret <88898367+michael-radency@users.noreply.github.com> Date: Fri, 8 Nov 2024 16:27:56 +0200 Subject: [PATCH 4/8] fix(Supabase Node): Reset query parameters in get many operation (#11630) --- packages/nodes-base/nodes/Supabase/Supabase.node.ts | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/packages/nodes-base/nodes/Supabase/Supabase.node.ts b/packages/nodes-base/nodes/Supabase/Supabase.node.ts index 929da0ad30..06b9d03337 100644 --- a/packages/nodes-base/nodes/Supabase/Supabase.node.ts +++ b/packages/nodes-base/nodes/Supabase/Supabase.node.ts @@ -124,14 +124,16 @@ export class Supabase implements INodeType { const items = this.getInputData(); const returnData: INodeExecutionData[] = []; const length = items.length; - const qs: IDataObject = {}; + let qs: IDataObject = {}; const resource = this.getNodeParameter('resource', 0); const operation = this.getNodeParameter('operation', 0); if (resource === 'row') { + const tableId = this.getNodeParameter('tableId', 0) as string; + if (operation === 'create') { const records: IDataObject[] = []; - const tableId = this.getNodeParameter('tableId', 0) as string; + for (let i = 0; i < length; i++) { const record: IDataObject = {}; const dataToSend = this.getNodeParameter('dataToSend', 0) as @@ -185,7 +187,6 @@ export class Supabase implements INodeType { } if (operation === 'delete') { - const tableId = this.getNodeParameter('tableId', 0) as string; const filterType = this.getNodeParameter('filterType', 0) as string; for (let i = 0; i < length; i++) { let endpoint = `/${tableId}`; @@ -241,7 +242,6 @@ export class Supabase implements INodeType { } if (operation === 'get') { - const tableId = this.getNodeParameter('tableId', 0) as string; const endpoint = `/${tableId}`; for (let i = 0; i < length; i++) { @@ -281,11 +281,13 @@ export class Supabase implements INodeType { } if (operation === 'getAll') { - const tableId = this.getNodeParameter('tableId', 0) as string; const returnAll = this.getNodeParameter('returnAll', 0); const filterType = this.getNodeParameter('filterType', 0) as string; + let endpoint = `/${tableId}`; for (let i = 0; i < length; i++) { + qs = {}; // reset qs + if (filterType === 'manual') { const matchType = this.getNodeParameter('matchType', 0) as string; const keys = this.getNodeParameter('filters.conditions', i, []) as IDataObject[]; @@ -342,7 +344,6 @@ export class Supabase implements INodeType { } if (operation === 'update') { - const tableId = this.getNodeParameter('tableId', 0) as string; const filterType = this.getNodeParameter('filterType', 0) as string; let endpoint = `/${tableId}`; for (let i = 0; i < length; i++) { From 658568e2700bfd5b61da53f3052403d0098c2d90 Mon Sep 17 00:00:00 2001 From: Csaba Tuncsik Date: Fri, 8 Nov 2024 15:48:23 +0100 Subject: [PATCH 5/8] fix(editor): Fix default workflow settings (#11632) --- packages/editor-ui/src/Interface.ts | 1 - packages/editor-ui/src/__tests__/defaults.ts | 4 +- .../src/components/WorkflowSettings.test.ts | 46 ++++++++++++++ .../src/components/WorkflowSettings.vue | 63 +++++++++---------- 4 files changed, 79 insertions(+), 35 deletions(-) diff --git a/packages/editor-ui/src/Interface.ts b/packages/editor-ui/src/Interface.ts index 11c30c67c3..2a140eb76c 100644 --- a/packages/editor-ui/src/Interface.ts +++ b/packages/editor-ui/src/Interface.ts @@ -626,7 +626,6 @@ export type WorkflowCallerPolicyDefaultOption = 'any' | 'none' | 'workflowsFromA export interface IWorkflowSettings extends IWorkflowSettingsWorkflow { errorWorkflow?: string; - saveManualExecutions?: boolean; timezone?: string; executionTimeout?: number; maxExecutionTimeout?: number; diff --git a/packages/editor-ui/src/__tests__/defaults.ts b/packages/editor-ui/src/__tests__/defaults.ts index d16678b17c..d2181db151 100644 --- a/packages/editor-ui/src/__tests__/defaults.ts +++ b/packages/editor-ui/src/__tests__/defaults.ts @@ -63,8 +63,8 @@ export const defaultSettings: FrontendSettings = { }, publicApi: { enabled: false, latestVersion: 0, path: '', swaggerUi: { enabled: false } }, pushBackend: 'websocket', - saveDataErrorExecution: 'DEFAULT', - saveDataSuccessExecution: 'DEFAULT', + saveDataErrorExecution: 'all', + saveDataSuccessExecution: 'all', saveManualExecutions: false, saveExecutionProgress: false, sso: { diff --git a/packages/editor-ui/src/components/WorkflowSettings.test.ts b/packages/editor-ui/src/components/WorkflowSettings.test.ts index 2c1a5e2c9c..fdca2d5f33 100644 --- a/packages/editor-ui/src/components/WorkflowSettings.test.ts +++ b/packages/editor-ui/src/components/WorkflowSettings.test.ts @@ -145,4 +145,50 @@ describe('WorkflowSettingsVue', () => { expect(getByTestId('workflow-caller-policy-workflow-ids')).toHaveValue(cleanedUpWorkflowList); }); + + test.each([ + ['workflow-settings-save-failed-executions', 'Default - Save', () => {}], + [ + 'workflow-settings-save-failed-executions', + 'Default - Do not save', + () => { + settingsStore.saveDataErrorExecution = 'none'; + }, + ], + ['workflow-settings-save-success-executions', 'Default - Save', () => {}], + [ + 'workflow-settings-save-success-executions', + 'Default - Do not save', + () => { + settingsStore.saveDataSuccessExecution = 'none'; + }, + ], + [ + 'workflow-settings-save-manual-executions', + 'Default - Save', + () => { + settingsStore.saveManualExecutions = true; + }, + ], + ['workflow-settings-save-manual-executions', 'Default - Do not save', () => {}], + [ + 'workflow-settings-save-execution-progress', + 'Default - Save', + () => { + settingsStore.saveDataProgressExecution = true; + }, + ], + ['workflow-settings-save-execution-progress', 'Default - Do not save', () => {}], + ])( + 'should show %s dropdown correct default value as %s', + async (testId, optionText, storeSetter) => { + storeSetter(); + const { getByTestId } = createComponent({ pinia }); + await nextTick(); + + const dropdownItems = await getDropdownItems(getByTestId(testId)); + + expect(dropdownItems[0]).toHaveTextContent(optionText); + }, + ); }); diff --git a/packages/editor-ui/src/components/WorkflowSettings.vue b/packages/editor-ui/src/components/WorkflowSettings.vue index 3146e9a7e6..d6d306eacd 100644 --- a/packages/editor-ui/src/components/WorkflowSettings.vue +++ b/packages/editor-ui/src/components/WorkflowSettings.vue @@ -70,14 +70,14 @@ const helpTexts = computed(() => ({ workflowCallerPolicy: i18n.baseText('workflowSettings.helpTexts.workflowCallerPolicy'), workflowCallerIds: i18n.baseText('workflowSettings.helpTexts.workflowCallerIds'), })); -const defaultValues = computed(() => ({ +const defaultValues = ref({ timezone: 'America/New_York', saveDataErrorExecution: 'all', saveDataSuccessExecution: 'all', saveExecutionProgress: false, saveManualExecutions: false, workflowCallerPolicy: 'workflowsFromSameOwner', -})); +}); const readOnlyEnv = computed(() => sourceControlStore.preferences.branchReadOnly); const workflowName = computed(() => workflowsStore.workflowName); const workflowId = computed(() => workflowsStore.workflowId); @@ -145,8 +145,7 @@ const loadWorkflowCallerPolicyOptions = async () => { }; const loadSaveDataErrorExecutionOptions = async () => { - saveDataErrorExecutionOptions.value.length = 0; - saveDataErrorExecutionOptions.value.push.apply(saveDataErrorExecutionOptions.value, [ + saveDataErrorExecutionOptions.value = [ { key: 'DEFAULT', value: i18n.baseText('workflowSettings.saveDataErrorExecutionOptions.defaultSave', { @@ -166,12 +165,11 @@ const loadSaveDataErrorExecutionOptions = async () => { key: 'none', value: i18n.baseText('workflowSettings.saveDataErrorExecutionOptions.doNotSave'), }, - ]); + ]; }; const loadSaveDataSuccessExecutionOptions = async () => { - saveDataSuccessExecutionOptions.value.length = 0; - saveDataSuccessExecutionOptions.value.push.apply(saveDataSuccessExecutionOptions.value, [ + saveDataSuccessExecutionOptions.value = [ { key: 'DEFAULT', value: i18n.baseText('workflowSettings.saveDataSuccessExecutionOptions.defaultSave', { @@ -191,12 +189,11 @@ const loadSaveDataSuccessExecutionOptions = async () => { key: 'none', value: i18n.baseText('workflowSettings.saveDataSuccessExecutionOptions.doNotSave'), }, - ]); + ]; }; const loadSaveExecutionProgressOptions = async () => { - saveExecutionProgressOptions.value.length = 0; - saveExecutionProgressOptions.value.push.apply(saveExecutionProgressOptions.value, [ + saveExecutionProgressOptions.value = [ { key: 'DEFAULT', value: i18n.baseText('workflowSettings.saveExecutionProgressOptions.defaultSave', { @@ -215,29 +212,30 @@ const loadSaveExecutionProgressOptions = async () => { key: false, value: i18n.baseText('workflowSettings.saveExecutionProgressOptions.doNotSave'), }, - ]); + ]; }; const loadSaveManualOptions = async () => { - saveManualOptions.value.length = 0; - saveManualOptions.value.push({ - key: 'DEFAULT', - value: i18n.baseText('workflowSettings.saveManualOptions.defaultSave', { - interpolate: { - defaultValue: defaultValues.value.saveManualExecutions - ? i18n.baseText('workflowSettings.saveManualOptions.save') - : i18n.baseText('workflowSettings.saveManualOptions.doNotSave'), - }, - }), - }); - saveManualOptions.value.push({ - key: true, - value: i18n.baseText('workflowSettings.saveManualOptions.save'), - }); - saveManualOptions.value.push({ - key: false, - value: i18n.baseText('workflowSettings.saveManualOptions.doNotSave'), - }); + saveManualOptions.value = [ + { + key: 'DEFAULT', + value: i18n.baseText('workflowSettings.saveManualOptions.defaultSave', { + interpolate: { + defaultValue: defaultValues.value.saveManualExecutions + ? i18n.baseText('workflowSettings.saveManualOptions.save') + : i18n.baseText('workflowSettings.saveManualOptions.doNotSave'), + }, + }), + }, + { + key: true, + value: i18n.baseText('workflowSettings.saveManualOptions.save'), + }, + { + key: false, + value: i18n.baseText('workflowSettings.saveManualOptions.doNotSave'), + }, + ]; }; const loadTimezones = async () => { @@ -400,6 +398,7 @@ onMounted(async () => { defaultValues.value.saveDataErrorExecution = settingsStore.saveDataErrorExecution; defaultValues.value.saveDataSuccessExecution = settingsStore.saveDataSuccessExecution; defaultValues.value.saveManualExecutions = settingsStore.saveManualExecutions; + defaultValues.value.saveExecutionProgress = settingsStore.saveDataProgressExecution; defaultValues.value.timezone = rootStore.timezone; defaultValues.value.workflowCallerPolicy = settingsStore.workflowCallerPolicyDefaultOption; @@ -423,7 +422,7 @@ onMounted(async () => { ); } - const workflowSettingsData = deepCopy(workflowsStore.workflowSettings) as IWorkflowSettings; + const workflowSettingsData = deepCopy(workflowsStore.workflowSettings); if (workflowSettingsData.timezone === undefined) { workflowSettingsData.timezone = 'DEFAULT'; @@ -438,7 +437,7 @@ onMounted(async () => { workflowSettingsData.saveExecutionProgress = 'DEFAULT'; } if (workflowSettingsData.saveManualExecutions === undefined) { - workflowSettingsData.saveManualExecutions = defaultValues.value.saveManualExecutions; + workflowSettingsData.saveManualExecutions = 'DEFAULT'; } if (workflowSettingsData.callerPolicy === undefined) { workflowSettingsData.callerPolicy = defaultValues.value From 9b6123dfb2648f880c7829211fa07666611ad0ea Mon Sep 17 00:00:00 2001 From: oleg Date: Fri, 8 Nov 2024 16:15:33 +0100 Subject: [PATCH 6/8] fix(AI Agent Node): Throw better errors for non-tool agents when using structured tools (#11582) --- .../agents/ConversationalAgent/execute.ts | 4 +- .../agents/PlanAndExecuteAgent/execute.ts | 3 +- .../agents/Agent/agents/ReActAgent/execute.ts | 4 +- .../nodes/agents/Agent/agents/utils.ts | 25 ++++- .../nodes/agents/Agent/test/utils.test.ts | 106 ++++++++++++++++++ packages/workflow/src/NodeHelpers.ts | 3 +- packages/workflow/src/WorkflowDataProxy.ts | 2 +- 7 files changed, 141 insertions(+), 6 deletions(-) create mode 100644 packages/@n8n/nodes-langchain/nodes/agents/Agent/test/utils.test.ts diff --git a/packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/ConversationalAgent/execute.ts b/packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/ConversationalAgent/execute.ts index 365fce3bf0..2ad4f1c075 100644 --- a/packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/ConversationalAgent/execute.ts +++ b/packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/ConversationalAgent/execute.ts @@ -14,7 +14,7 @@ import { import { getOptionalOutputParsers } from '../../../../../utils/output_parsers/N8nOutputParser'; import { throwIfToolSchema } from '../../../../../utils/schemaParsing'; import { getTracingConfig } from '../../../../../utils/tracing'; -import { extractParsedOutput } from '../utils'; +import { checkForStructuredTools, extractParsedOutput } from '../utils'; export async function conversationalAgentExecute( this: IExecuteFunctions, @@ -34,6 +34,8 @@ export async function conversationalAgentExecute( const tools = await getConnectedTools(this, nodeVersion >= 1.5); const outputParsers = await getOptionalOutputParsers(this); + await checkForStructuredTools(tools, this.getNode(), 'Conversational Agent'); + // TODO: Make it possible in the future to use values for other items than just 0 const options = this.getNodeParameter('options', 0, {}) as { systemMessage?: string; diff --git a/packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/PlanAndExecuteAgent/execute.ts b/packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/PlanAndExecuteAgent/execute.ts index 9425caaf84..a8f607f950 100644 --- a/packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/PlanAndExecuteAgent/execute.ts +++ b/packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/PlanAndExecuteAgent/execute.ts @@ -14,7 +14,7 @@ import { getConnectedTools, getPromptInputByType } from '../../../../../utils/he import { getOptionalOutputParsers } from '../../../../../utils/output_parsers/N8nOutputParser'; import { throwIfToolSchema } from '../../../../../utils/schemaParsing'; import { getTracingConfig } from '../../../../../utils/tracing'; -import { extractParsedOutput } from '../utils'; +import { checkForStructuredTools, extractParsedOutput } from '../utils'; export async function planAndExecuteAgentExecute( this: IExecuteFunctions, @@ -28,6 +28,7 @@ export async function planAndExecuteAgentExecute( const tools = await getConnectedTools(this, nodeVersion >= 1.5); + await checkForStructuredTools(tools, this.getNode(), 'Plan & Execute Agent'); const outputParsers = await getOptionalOutputParsers(this); const options = this.getNodeParameter('options', 0, {}) as { diff --git a/packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/ReActAgent/execute.ts b/packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/ReActAgent/execute.ts index 429adfad21..224e727102 100644 --- a/packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/ReActAgent/execute.ts +++ b/packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/ReActAgent/execute.ts @@ -19,7 +19,7 @@ import { import { getOptionalOutputParsers } from '../../../../../utils/output_parsers/N8nOutputParser'; import { throwIfToolSchema } from '../../../../../utils/schemaParsing'; import { getTracingConfig } from '../../../../../utils/tracing'; -import { extractParsedOutput } from '../utils'; +import { checkForStructuredTools, extractParsedOutput } from '../utils'; export async function reActAgentAgentExecute( this: IExecuteFunctions, @@ -33,6 +33,8 @@ export async function reActAgentAgentExecute( const tools = await getConnectedTools(this, nodeVersion >= 1.5); + await checkForStructuredTools(tools, this.getNode(), 'ReAct Agent'); + const outputParsers = await getOptionalOutputParsers(this); const options = this.getNodeParameter('options', 0, {}) as { diff --git a/packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/utils.ts b/packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/utils.ts index da2f666248..0d85806bf3 100644 --- a/packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/utils.ts +++ b/packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/utils.ts @@ -1,5 +1,7 @@ +import type { ZodObjectAny } from '@langchain/core/dist/types/zod'; import type { BaseOutputParser } from '@langchain/core/output_parsers'; -import type { IExecuteFunctions } from 'n8n-workflow'; +import type { DynamicStructuredTool, Tool } from 'langchain/tools'; +import { NodeOperationError, type IExecuteFunctions, type INode } from 'n8n-workflow'; export async function extractParsedOutput( ctx: IExecuteFunctions, @@ -17,3 +19,24 @@ export async function extractParsedOutput( // with fallback to the original output if it's not present return parsedOutput?.output ?? parsedOutput; } + +export async function checkForStructuredTools( + tools: Array>, + node: INode, + currentAgentType: string, +) { + const dynamicStructuredTools = tools.filter( + (tool) => tool.constructor.name === 'DynamicStructuredTool', + ); + if (dynamicStructuredTools.length > 0) { + const getToolName = (tool: Tool | DynamicStructuredTool) => `"${tool.name}"`; + throw new NodeOperationError( + node, + `The selected tools are not supported by "${currentAgentType}", please use "Tools Agent" instead`, + { + itemIndex: 0, + description: `Incompatible connected tools: ${dynamicStructuredTools.map(getToolName).join(', ')}`, + }, + ); + } +} diff --git a/packages/@n8n/nodes-langchain/nodes/agents/Agent/test/utils.test.ts b/packages/@n8n/nodes-langchain/nodes/agents/Agent/test/utils.test.ts new file mode 100644 index 0000000000..dc54ca4d45 --- /dev/null +++ b/packages/@n8n/nodes-langchain/nodes/agents/Agent/test/utils.test.ts @@ -0,0 +1,106 @@ +import type { Tool } from 'langchain/tools'; +import { DynamicStructuredTool } from 'langchain/tools'; +import { NodeOperationError } from 'n8n-workflow'; +import type { INode } from 'n8n-workflow'; +import { z } from 'zod'; + +import { checkForStructuredTools } from '../agents/utils'; + +describe('checkForStructuredTools', () => { + let mockNode: INode; + + beforeEach(() => { + mockNode = { + id: 'test-node', + name: 'Test Node', + type: 'test', + typeVersion: 1, + position: [0, 0], + parameters: {}, + }; + }); + + it('should not throw error when no DynamicStructuredTools are present', async () => { + const tools = [ + { + name: 'regular-tool', + constructor: { name: 'Tool' }, + } as Tool, + ]; + + await expect( + checkForStructuredTools(tools, mockNode, 'Conversation Agent'), + ).resolves.not.toThrow(); + }); + + it('should throw NodeOperationError when DynamicStructuredTools are present', async () => { + const dynamicTool = new DynamicStructuredTool({ + name: 'dynamic-tool', + description: 'test tool', + schema: z.object({}), + func: async () => 'result', + }); + + const tools: Array = [dynamicTool]; + + await expect(checkForStructuredTools(tools, mockNode, 'Conversation Agent')).rejects.toThrow( + NodeOperationError, + ); + + await expect( + checkForStructuredTools(tools, mockNode, 'Conversation Agent'), + ).rejects.toMatchObject({ + message: + 'The selected tools are not supported by "Conversation Agent", please use "Tools Agent" instead', + description: 'Incompatible connected tools: "dynamic-tool"', + }); + }); + + it('should list multiple dynamic tools in error message', async () => { + const dynamicTool1 = new DynamicStructuredTool({ + name: 'dynamic-tool-1', + description: 'test tool 1', + schema: z.object({}), + func: async () => 'result', + }); + + const dynamicTool2 = new DynamicStructuredTool({ + name: 'dynamic-tool-2', + description: 'test tool 2', + schema: z.object({}), + func: async () => 'result', + }); + + const tools = [dynamicTool1, dynamicTool2]; + + await expect( + checkForStructuredTools(tools, mockNode, 'Conversation Agent'), + ).rejects.toMatchObject({ + description: 'Incompatible connected tools: "dynamic-tool-1", "dynamic-tool-2"', + }); + }); + + it('should throw error with mixed tool types and list only dynamic tools in error message', async () => { + const regularTool = { + name: 'regular-tool', + constructor: { name: 'Tool' }, + } as Tool; + + const dynamicTool = new DynamicStructuredTool({ + name: 'dynamic-tool', + description: 'test tool', + schema: z.object({}), + func: async () => 'result', + }); + + const tools = [regularTool, dynamicTool]; + + await expect( + checkForStructuredTools(tools, mockNode, 'Conversation Agent'), + ).rejects.toMatchObject({ + message: + 'The selected tools are not supported by "Conversation Agent", please use "Tools Agent" instead', + description: 'Incompatible connected tools: "dynamic-tool"', + }); + }); +}); diff --git a/packages/workflow/src/NodeHelpers.ts b/packages/workflow/src/NodeHelpers.ts index e21f2b7e1a..9626e315a1 100644 --- a/packages/workflow/src/NodeHelpers.ts +++ b/packages/workflow/src/NodeHelpers.ts @@ -408,7 +408,8 @@ export function convertNodeToAiTool< }; const noticeProp: INodeProperties = { - displayName: 'Use the expression {{ $fromAI() }} for any data to be filled by the model', + displayName: + "Use the expression {{ $fromAI('placeholder_name') }} for any data to be filled by the model", name: 'notice', type: 'notice', default: '', diff --git a/packages/workflow/src/WorkflowDataProxy.ts b/packages/workflow/src/WorkflowDataProxy.ts index 2ad2beb3a1..ea167f50b3 100644 --- a/packages/workflow/src/WorkflowDataProxy.ts +++ b/packages/workflow/src/WorkflowDataProxy.ts @@ -946,7 +946,7 @@ export class WorkflowDataProxy { defaultValue?: unknown, ) => { if (!name || name === '') { - throw new ExpressionError('Please provide a key', { + throw new ExpressionError("Add a key, e.g. $fromAI('placeholder_name')", { runIndex: that.runIndex, itemIndex: that.itemIndex, }); From fb06b55211b5ebed9feceea28a75b1c51227ddbb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E0=A4=95=E0=A4=BE=E0=A4=B0=E0=A4=A4=E0=A5=8B=E0=A4=AB?= =?UTF-8?q?=E0=A5=8D=E0=A4=AB=E0=A5=87=E0=A4=B2=E0=A4=B8=E0=A5=8D=E0=A4=95?= =?UTF-8?q?=E0=A5=8D=E0=A4=B0=E0=A4=BF=E0=A4=AA=E0=A5=8D=E0=A4=9F=E2=84=A2?= Date: Fri, 8 Nov 2024 16:54:06 +0100 Subject: [PATCH 7/8] fix(core): Fix hot-reload that broke after chokidar upgrade (no-changelog) (#11658) --- packages/cli/src/load-nodes-and-credentials.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/cli/src/load-nodes-and-credentials.ts b/packages/cli/src/load-nodes-and-credentials.ts index a5cff3764f..22273fb894 100644 --- a/packages/cli/src/load-nodes-and-credentials.ts +++ b/packages/cli/src/load-nodes-and-credentials.ts @@ -390,7 +390,15 @@ export class LoadNodesAndCredentials { const toWatch = loader.isLazyLoaded ? ['**/nodes.json', '**/credentials.json'] : ['**/*.js', '**/*.json']; - watch(toWatch, { cwd: realModulePath }).on('change', reloader); + const files = await glob(toWatch, { + cwd: realModulePath, + ignore: ['node_modules/**'], + }); + const watcher = watch(files, { + cwd: realModulePath, + ignoreInitial: true, + }); + watcher.on('add', reloader).on('change', reloader).on('unlink', reloader); }); } } From 0fdb79a270b499c309eb289796ede04a89b01398 Mon Sep 17 00:00:00 2001 From: Tomi Turtiainen <10324676+tomi@users.noreply.github.com> Date: Fri, 8 Nov 2024 20:27:37 +0200 Subject: [PATCH 8/8] fix(core): Make task runner oom error message more user friendly (no-changelog) (#11646) --- .../src/runners/errors/task-runner-oom-error.ts | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/packages/cli/src/runners/errors/task-runner-oom-error.ts b/packages/cli/src/runners/errors/task-runner-oom-error.ts index e52b8b4bea..f846d98768 100644 --- a/packages/cli/src/runners/errors/task-runner-oom-error.ts +++ b/packages/cli/src/runners/errors/task-runner-oom-error.ts @@ -5,18 +5,22 @@ import type { TaskRunner } from '../task-broker.service'; export class TaskRunnerOomError extends ApplicationError { public description: string; - constructor(runnerId: TaskRunner['id'], isCloudDeployment: boolean) { - super(`Task runner (${runnerId}) ran out of memory.`, { level: 'error' }); + constructor( + public readonly runnerId: TaskRunner['id'], + isCloudDeployment: boolean, + ) { + super('Node ran out of memory.', { level: 'error' }); const fixSuggestions = { - reduceItems: 'Reduce the number of items processed at a time by batching the input.', + reduceItems: + 'Reduce the number of items processed at a time, by batching them using a loop node', increaseMemory: - "Increase the memory available to the task runner with 'N8N_RUNNERS_MAX_OLD_SPACE_SIZE' environment variable.", - upgradePlan: 'Upgrade your cloud plan to increase the available memory.', + "Increase the memory available to the task runner with 'N8N_RUNNERS_MAX_OLD_SPACE_SIZE' environment variable", + upgradePlan: 'Upgrade your cloud plan to increase the available memory', }; const subtitle = - 'The runner executing the code ran out of memory. This usually happens when there are too many items to process. You can try the following:'; + 'This usually happens when there are too many items to process. You can try the following:'; const suggestions = isCloudDeployment ? [fixSuggestions.reduceItems, fixSuggestions.upgradePlan] : [fixSuggestions.reduceItems, fixSuggestions.increaseMemory];