Skip to content

Commit

Permalink
feat(editor): Add new /templates/search endpoint (#8227)
Browse files Browse the repository at this point in the history
Updating n8n front-end to use the new search endpoint powered by TypeSense.

Endpoint is deployed on staging API so, in order to test it, use this env var:
```export N8N_TEMPLATES_HOST=https://api-staging.n8n.io/api```

**NOTE**: This PR should not be merged until [backend changes](n8n-io/creators-site#118) are merged and released.

## Related tickets and issues
https://linear.app/n8n/issue/ADO-1555/update-in-app-search-to-work-with-the-new-back-end


## Review / Merge checklist
- [x] PR title and summary are descriptive. **Remember, the title automatically goes into the changelog. Use `(no-changelog)` otherwise.** ([conventions](https://github.com/n8n-io/n8n/blob/master/.github/pull_request_title_conventions.md))
- [ ] Tests included.
   > A bug is not considered fixed, unless a test is added to prevent it from happening again.
   > A feature is not complete without tests.
  • Loading branch information
RicardoE105 authored Jan 15, 2024
1 parent bb2be8d commit 4277e92
Show file tree
Hide file tree
Showing 12 changed files with 2,786 additions and 83 deletions.
73 changes: 71 additions & 2 deletions cypress/e2e/29-templates.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,13 @@ const workflowPage = new WorkflowPage();
const templateWorkflowPage = new TemplateWorkflowPage();

describe('Templates', () => {
beforeEach(() => {
cy.intercept('GET', '**/api/templates/search?page=1&rows=20&category=&search=', { fixture: 'templates_search/all_templates_search_response.json' }).as('searchRequest');
cy.intercept('GET', '**/api/templates/search?page=1&rows=20&category=Sales*', { fixture: 'templates_search/sales_templates_search_response.json' }).as('categorySearchRequest');
cy.intercept('GET', '**/api/templates/workflows/*', { fixture: 'templates_search/test_template_preview.json' }).as('singleTemplateRequest');
cy.intercept('GET', '**/api/workflows/templates/*', { fixture: 'templates_search/test_template_import.json' }).as('singleTemplateRequest');
});

it('can open onboarding flow', () => {
templatesPage.actions.openOnboardingFlow(1234, OnboardingWorkflow.name, OnboardingWorkflow);
cy.url().then(($url) => {
Expand Down Expand Up @@ -37,10 +44,9 @@ describe('Templates', () => {

it('should save template id with the workflow', () => {
cy.visit(templatesPage.url);
cy.intercept('GET', '**/api/templates/**').as('loadApi');
cy.get('.el-skeleton.n8n-loading').should('not.exist');
templatesPage.getters.firstTemplateCard().should('exist');
cy.wait('@loadApi');
templatesPage.getters.templatesLoadingContainer().should('not.exist');
templatesPage.getters.firstTemplateCard().click();
cy.url().should('include', '/templates/');

Expand All @@ -67,4 +73,67 @@ describe('Templates', () => {

templateWorkflowPage.getters.description().find('img').should('have.length', 1);
});


it('renders search elements correctly', () => {
cy.visit(templatesPage.url);
templatesPage.getters.searchInput().should('exist');
templatesPage.getters.allCategoriesFilter().should('exist');
templatesPage.getters.categoryFilters().should('have.length.greaterThan', 1);
templatesPage.getters.templateCards().should('have.length.greaterThan', 0);
});

it('can filter templates by category', () => {
cy.visit(templatesPage.url);
templatesPage.getters.templatesLoadingContainer().should('not.exist');
templatesPage.getters.expandCategoriesButton().click();
templatesPage.getters.categoryFilter('sales').should('exist');
let initialTemplateCount = 0;
let initialCollectionCount = 0;

templatesPage.getters.templateCountLabel().then(($el) => {
initialTemplateCount = parseInt($el.text().replace(/\D/g, ''), 10);
templatesPage.getters.collectionCountLabel().then(($el) => {
initialCollectionCount = parseInt($el.text().replace(/\D/g, ''), 10);

templatesPage.getters.categoryFilter('sales').click();
templatesPage.getters.templatesLoadingContainer().should('not.exist');

// Should have less templates and collections after selecting a category
templatesPage.getters.templateCountLabel().should(($el) => {
expect(parseInt($el.text().replace(/\D/g, ''), 10)).to.be.lessThan(initialTemplateCount);
});
templatesPage.getters.collectionCountLabel().should(($el) => {
expect(parseInt($el.text().replace(/\D/g, ''), 10)).to.be.lessThan(initialCollectionCount);
});
});
});
});

it('should preserve search query in URL', () => {
cy.visit(templatesPage.url);
templatesPage.getters.templatesLoadingContainer().should('not.exist');
templatesPage.getters.expandCategoriesButton().click();
templatesPage.getters.categoryFilter('sales').should('exist');
templatesPage.getters.categoryFilter('sales').click();
templatesPage.getters.searchInput().type('auto');

cy.url().should('include', '?categories=');
cy.url().should('include', '&search=');

cy.reload();

// Should preserve search query in URL
cy.url().should('include', '?categories=');
cy.url().should('include', '&search=');

// Sales category should still be selected
templatesPage.getters.categoryFilter('sales').find('label').should('have.class', 'is-checked');
// Search input should still have the search query
templatesPage.getters.searchInput().should('have.value', 'auto');
// Sales checkbox should be pushed to the top
templatesPage.getters.categoryFilters().eq(1).then(($el) => {
expect($el.text()).to.equal('Sales');
});
});
});
1,071 changes: 1,071 additions & 0 deletions cypress/fixtures/templates_search/all_templates_search_response.json

Large diffs are not rendered by default.

1,316 changes: 1,316 additions & 0 deletions cypress/fixtures/templates_search/sales_templates_search_response.json

Large diffs are not rendered by default.

19 changes: 19 additions & 0 deletions cypress/fixtures/templates_search/test_template_import.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"id": 60,
"name": "test1 test1",
"workflow": {
"nodes": [
{
"name": "Start",
"type": "n8n-nodes-base.start",
"position": [
250,
300
],
"parameters": {},
"typeVersion": 1
}
],
"connections": {}
}
}
150 changes: 150 additions & 0 deletions cypress/fixtures/templates_search/test_template_preview.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
{
"workflow": {
"id": 60,
"name": "test1 test1",
"views": 120000000,
"recentViews": 0,
"totalViews": 120000000,
"createdAt": "2019-08-30T16:39:31.362Z",
"description": "here is a description. here is a description. here is a description. \n\n![Screenshot from 20190806 091433.png](fileId:88)",
"workflow": {
"nodes": [
{
"name": "Start",
"type": "n8n-nodes-base.start",
"position": [
250,
300
],
"parameters": {},
"typeVersion": 1
}
],
"connections": {}
},
"lastUpdatedBy": null,
"workflowInfo": {
"nodeCount": 1,
"nodeTypes": {
"n8n-nodes-base.start": {
"count": 1
}
}
},
"user": {
"username": "admin"
},
"nodes": [
{
"id": 11,
"icon": "file:amqp.png",
"name": "n8n-nodes-base.amqpTrigger",
"defaults": {
"name": "AMQP Trigger"
},
"iconData": {
"type": "file",
"fileBuffer": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADwAAAA8CAYAAAA6/NlyAAAB7UlEQVRoge2W4W3CMBCFj26QjkBHSEdIR4AR6Ah0BBgBRqAjhBFgBBghHaEVlV29PN0lDr+o9D7JEjhn+975bJ8JIYQQQgghhBBCCCGEEA9CY2bf0NaBW2uyu7UN2XSOzTyY60J2BzNbObbsH7eTmS2mhHJHE1wmCD7A93ngEAquHaHc2omCcysSXQW74g32BHfwfTEiuCoQm9vuDsEndPYpELxKjjBj0foCEXX6XdM3by3c7aOZPZvZzMzeaBzbIh9pzIuZXaG/RqNIMAq7Ur8XCHQ2kx3LC56DMQ39X4LI23zbAd88ruRHD09wTVF5p+/eBZI5g7O8w5FgXOvsZAI7PxRwS4HGIPbm8wRjBL/Sgp/QNyQYHWySmOxgJBgFeGnPfZHgDVyufET+YMEVCdo7gziCTBbGmRKlGQpCMXOnj+1L6B0JFsxndO3cjjZyjo6OnZeqGb5gqhTQS3qKeK1SwbesfB3IrF/awqu+g8Dgs5SLE37SciHiPUv8rLVp7k2wdl63tDDqgTs8lqpINWGXbSTKe9rlJgXME7C9I6V7oGAWsEzv2gzeN2TstkbCZyIJWBYKWUwtF4foKGU9TpRGdZDSdVDpDNXSVVBLt5TeucS9K6X/E3USX3rshBBCCCGEEEIIIYQQ4tExsx8PuuPnwhCIbgAAAABJRU5ErkJggg=="
},
"categories": [
{
"id": 5,
"name": "Development"
},
{
"id": 6,
"name": "Communication"
}
],
"displayName": "AMQP Trigger",
"typeVersion": 1
},
{
"id": 18,
"icon": "file:autopilot.svg",
"name": "n8n-nodes-base.autopilot",
"defaults": {
"name": "Autopilot"
},
"iconData": {
"type": "file",
"fileBuffer": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjM4IDI2IDM1IDM1Ij48Y2lyY2xlIGN4PSI1MCIgY3k9IjUwIiByPSI0MCIgc3Ryb2tlPSIjMThkNGIyIiBzdHJva2Utd2lkdGg9IjMiIGZpbGw9IiMxOGQ0YjIiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNNDUuNCA0Mi42aDE5LjlsMy40LTQuOEg0MmwzLjQgNC44em0zLjEgOC4zaDEzLjFsMy40LTQuOEg0NS40bDMuMSA0Ljh6bTU0LS43Ii8+PC9zdmc+"
},
"categories": [
{
"id": 1,
"name": "Marketing"
}
],
"displayName": "Autopilot",
"typeVersion": 1
},
{
"id": 20,
"icon": "file:lambda.svg",
"name": "n8n-nodes-base.awsLambda",
"defaults": {
"name": "AWS Lambda"
},
"iconData": {
"type": "file",
"fileBuffer": "data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9IjI1MDAiIHZpZXdCb3g9Ii0zLjAyMyAtMC4yMiA0MjAuOTIzIDQzMy41NCIgd2lkdGg9IjI0NDMiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTIwOC40NSAyMjcuODljLTEuNTkgMi4yNi0yLjkzIDQuMTItNC4yMiA2cS0zMC44NiA0NS40Mi02MS43IDkwLjgzLTI4LjY5IDQyLjI0LTU3LjQ0IDg0LjQzYTMuODggMy44OCAwIDAxLTIuNzMgMS41OXEtNDAuNTktLjM1LTgxLjE2LS44OGMtLjMgMC0uNjEtLjA5LTEuMi0uMThhMTQuNDQgMTQuNDQgMCAwMS43Ni0xLjY1cTI4LjMxLTQzLjg5IDU2LjYyLTg3Ljc2IDI1LjExLTM4Ljg4IDUwLjI1LTc3Ljc0IDI3Ljg2LTQzLjE4IDU1LjY5LTg2LjQyYzIuNzQtNC4yNSA1LjU5LTguNDIgOC4xOS0xMi43NWE1LjI2IDUuMjYgMCAwMC41Ni0zLjgzYy01LTE1Ljk0LTEwLjEtMzEuODQtMTUuMTktNDcuNzQtMi4xOC02LjgxLTQuNDYtMTMuNTgtNi41LTIwLjQzLS42Ni0yLjItMS43NS0yLjg3LTQtMi44Ni0xNyAuMDctMzMuOS4wNS01MC44NS4wNS0zLjIyIDAtMy4yMyAwLTMuMjMtMy4xOCAwLTIwLjg0IDAtNDEuNjgtLjA2LTYyLjUyIDAtMi4zMi43Ni0yLjg0IDIuOTQtMi44NHE1MS4xOS4wOSAxMDIuNCAwYTMuMjkgMy4yOSAwIDAxMy42IDIuNDNxMjcgNjcuOTEgNTQgMTM1Ljc3IDMxLjUgNzkuMTQgNjMgMTU4LjNjNi41MiAxNi4zOCAxMy4wOSAzMi43NSAxOS41NCA0OS4xNy43NyAyIDEuNTcgMi4zOCAzLjU5IDEuNzYgMTcuODktNS41MyAzNS44Mi0xMC45MSA1My43LTE2LjQ1IDIuMjUtLjcgMy4wNy0uMjMgMy43NyAyIDYuMSAxOS4xNyAxMi4zMiAzOC4zIDE4LjUgNTcuNDUuMjEuNjYuMzcgMS4zMy42MiAyLjI1LTEuMjguNDctMi40OCAxLTMuNzEgMS4zNHEtNjEgMTkuMzMtMTIxLjkzIDM4LjY4Yy0xLjk0LjYxLTIuNTItLjA1LTMuMTctMS42OHEtMTguNjEtNDcuMTYtMzcuMzEtOTQuMjgtMTguMjktNDYuMTQtMzYuNi05Mi4yOGMtMS44My00LjYyLTMuNjMtOS4yNi01LjQ2LTEzLjg4LS4yOS0uNzktLjY5LTEuNDgtMS4yNy0yLjd6IiBmaWxsPSIjZmE3ZTE0Ii8+PC9zdmc+"
},
"categories": [
{
"id": 5,
"name": "Development"
}
],
"displayName": "AWS Lambda",
"typeVersion": 1
},
{
"id": 40,
"icon": "file:clearbit.svg",
"name": "n8n-nodes-base.clearbit",
"defaults": {
"name": "Clearbit"
},
"iconData": {
"type": "file",
"fileBuffer": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI3MiIgaGVpZ2h0PSI3MiI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJhIiB4MT0iNTAlIiB4Mj0iMTAwJSIgeTE9IjAlIiB5Mj0iMTAwJSI+PHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iI0RFRjJGRSIvPjxzdG9wIG9mZnNldD0iMTAwJSIgc3RvcC1jb2xvcj0iI0RCRjFGRSIvPjwvbGluZWFyR3JhZGllbnQ+PGxpbmVhckdyYWRpZW50IGlkPSJiIiB4MT0iMCUiIHgyPSI1MCUiIHkxPSIwJSIgeTI9IjEwMCUiPjxzdG9wIG9mZnNldD0iMCUiIHN0b3AtY29sb3I9IiM1N0JDRkQiLz48c3RvcCBvZmZzZXQ9IjEwMCUiIHN0b3AtY29sb3I9IiM1MUI1RkQiLz48L2xpbmVhckdyYWRpZW50PjxsaW5lYXJHcmFkaWVudCBpZD0iYyIgeDE9IjM3LjUlIiB4Mj0iNjIuNSUiIHkxPSIwJSIgeTI9IjEwMCUiPjxzdG9wIG9mZnNldD0iMCUiIHN0b3AtY29sb3I9IiMxQ0E3RkQiLz48c3RvcCBvZmZzZXQ9IjEwMCUiIHN0b3AtY29sb3I9IiMxNDhDRkMiLz48L2xpbmVhckdyYWRpZW50PjwvZGVmcz48ZyBmaWxsPSJub25lIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiPjxwYXRoIGZpbGw9InVybCgjYSkiIGQ9Ik03MiAzNnYxNi43N2wtLjAwNC44NjhjLS4wNiA2LjAzNS0uNzUgOC4zNTMtMiAxMC42ODhhMTMuNjMgMTMuNjMgMCAwMS01LjY3IDUuNjdsLS4zMjYuMTcxQzYxLjY1OCA3MS4zNjQgNTkuMTYgNzIgNTIuNzcgNzJIMzZWMzZoMzZ6Ii8+PHBhdGggZmlsbD0idXJsKCNiKSIgZD0iTTY0LjMyNiAyLjAwM2ExMy42MyAxMy42MyAwIDAxNS42NyA1LjY3bC4xNzEuMzI3QzcxLjM2NCAxMC4zNDIgNzIgMTIuODQgNzIgMTkuMjNWMzZIMzZWMGgxNi43N2M2LjY4NyAwIDkuMTEyLjY5NiAxMS41NTYgMi4wMDN6Ii8+PHBhdGggZmlsbD0idXJsKCNjKSIgZD0iTTM2IDB2NzJIMTkuMjNsLS44NjgtLjAwNGMtNi4wMzUtLjA2LTguMzUzLS43NS0xMC42ODgtMmExMy42MyAxMy42MyAwIDAxLTUuNjctNS42N0wxLjgzMiA2NEMuNjM2IDYxLjY1OCAwIDU5LjE2IDAgNTIuNzdWMTkuMjNjMC02LjY4Ny42OTYtOS4xMTIgMi4wMDMtMTEuNTU2YTEzLjYzIDEzLjYzIDAgMDE1LjY3LTUuNjdMOCAxLjgzMkMxMC4zNDIuNjM2IDEyLjg0IDAgMTkuMjMgMEgzNnoiLz48L2c+PC9zdmc+"
},
"categories": [
{
"id": 2,
"name": "Sales"
}
],
"displayName": "Clearbit",
"typeVersion": 1
},
{
"id": 51,
"icon": "file:convertKit.svg",
"name": "n8n-nodes-base.convertKitTrigger",
"defaults": {
"name": "ConvertKit Trigger"
},
"iconData": {
"type": "file",
"fileBuffer": "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTcyIiBoZWlnaHQ9IjE2MCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBkPSJNODIuNzIgMTI2LjMxNmMyOS43NyAwIDUyLjc4LTIyLjYyMiA1Mi43OC01MC41MjYgMC0yNi4xNDMtMjEuNjE3LTQyLjEwNi0zNS45MzUtNDIuMTA2LTE5Ljk0NSAwLTM1LjkzIDE0LjA4NC0zOC4xOTggMzQuOTg4LS40MTggMy44NTYtMy40NzYgNy4wOS03LjM1NSA3LjA2MS02LjQyMy0uMDQ2LTE1Ljc0Ni0uMS0yMS42NTgtLjA4LTIuNTU1LjAwOC00LjY2OS0yLjA2NS00LjU0My00LjYxOC44OS0xOC4xMjMgNi45MTQtMzUuMDcgMTguNDAyLTQ4LjA4N0M1OC45NzYgOC40ODggNzcuNTYxIDAgOTkuNTY1IDBjMzYuOTY5IDAgNzEuODY5IDMzLjc4NiA3MS44NjkgNzUuNzkgMCA0Ni41MDgtMzguMzEyIDg0LjIxLTg3LjkyNyA4NC4yMS0zNS4zODQgMC03MS4wMjEtMjMuMjU4LTgzLjQ2NC01NS43NzVhLjcwMi43MDIgMCAwMS0uMDMtLjM3N2MuMTY1LS45NjIuNDk0LTEuODQxLjgxOC0yLjcwNy40NzEtMS4yNTguOTMxLTIuNDg4Ljg2NC0zLjkwNmwtLjIxNS00LjUyOWE1LjUyMyA1LjUyMyAwIDAxMy4xOC01LjI2M2wxLjc5OC0uODQyYTYuOTgyIDYuOTgyIDAgMDAzLjkxMi01LjA3NSA2Ljk5MyA2Ljk5MyAwIDAxNi44ODctNS43MzZjNS4yODIgMCA5Ljg3NSAzLjUxNSAxMS41OSA4LjUxMiA4LjMwNyAyNC4yMTIgMjEuNTExIDQyLjAxNCA1My44NzMgNDIuMDE0eiIgZmlsbD0iI0ZCNjk3MCIvPjwvc3ZnPg=="
},
"categories": [
{
"id": 1,
"name": "Marketing"
},
{
"id": 2,
"name": "Sales"
}
],
"displayName": "ConvertKit Trigger",
"typeVersion": 1
}
],
"categories": [],
"image": []
}
}
8 changes: 8 additions & 0 deletions cypress/pages/templates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@ export class TemplatesPage extends BasePage {
useTemplateButton: () => cy.getByTestId('use-template-button'),
templateCards: () => cy.getByTestId('template-card'),
firstTemplateCard: () => this.getters.templateCards().first(),
allCategoriesFilter: () => cy.getByTestId('template-filter-all-categories'),
searchInput: () => cy.getByTestId('template-search-input'),
categoryFilters: () => cy.get('[data-test-id^=template-filter]'),
categoryFilter: (category: string) => cy.getByTestId(`template-filter-${category}`),
collectionCountLabel: () => cy.getByTestId('collection-count-label'),
templateCountLabel: () => cy.getByTestId('template-count-label'),
templatesLoadingContainer: () => cy.getByTestId('templates-loading-container'),
expandCategoriesButton: () => cy.getByTestId('expand-categories-button'),
};

actions = {
Expand Down
18 changes: 16 additions & 2 deletions packages/editor-ui/src/Interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -830,6 +830,19 @@ export interface ITemplatesWorkflowInfo {
};
}

export type TemplateSearchFacet = {
field_name: string;
sampled: boolean;
stats: {
total_values: number;
};
counts: Array<{
count: number;
highlighted: string;
value: string;
}>;
};

export interface ITemplatesWorkflowResponse extends ITemplatesWorkflow, IWorkflowTemplate {
description: string | null;
image: ITemplatesImage[];
Expand All @@ -845,7 +858,7 @@ export interface ITemplatesWorkflowFull extends ITemplatesWorkflowResponse {
}

export interface ITemplatesQuery {
categories: number[];
categories: string[];
search: string;
}

Expand Down Expand Up @@ -1357,14 +1370,15 @@ export interface INodeTypesState {
}

export interface ITemplateState {
categories: { [id: string]: ITemplatesCategory };
categories: ITemplatesCategory[];
collections: { [id: string]: ITemplatesCollection };
workflows: { [id: string]: ITemplatesWorkflow | ITemplatesWorkflowFull };
workflowSearches: {
[search: string]: {
workflowIds: string[];
totalWorkflows: number;
loadingMore?: boolean;
categories?: ITemplatesCategory[];
};
};
collectionSearches: {
Expand Down
13 changes: 9 additions & 4 deletions packages/editor-ui/src/api/templates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type {
ITemplatesCollectionResponse,
ITemplatesWorkflowResponse,
IWorkflowTemplate,
TemplateSearchFacet,
} from '@/Interface';
import type { IDataObject } from 'n8n-workflow';
import { get } from '@/utils/apiUtils';
Expand Down Expand Up @@ -40,14 +41,18 @@ export async function getCollections(

export async function getWorkflows(
apiEndpoint: string,
query: { skip: number; limit: number; categories: number[]; search: string },
query: { page: number; limit: number; categories: number[]; search: string },
headers?: IDataObject,
): Promise<{ totalWorkflows: number; workflows: ITemplatesWorkflow[] }> {
): Promise<{
totalWorkflows: number;
workflows: ITemplatesWorkflow[];
filters: TemplateSearchFacet[];
}> {
return get(
apiEndpoint,
'/templates/workflows',
'/templates/search',
{
skip: query.skip,
page: query.page,
rows: query.limit,
category: stringifyArray(query.categories),
search: query.search,
Expand Down
Loading

0 comments on commit 4277e92

Please sign in to comment.