diff --git a/packages/editor-ui/src/__tests__/server/endpoints/index.ts b/packages/editor-ui/src/__tests__/server/endpoints/index.ts index edc073f2a0c67..93a48d3dfec6d 100644 --- a/packages/editor-ui/src/__tests__/server/endpoints/index.ts +++ b/packages/editor-ui/src/__tests__/server/endpoints/index.ts @@ -6,6 +6,8 @@ import { routesForVariables } from './variable'; import { routesForSettings } from './settings'; import { routesForSSO } from './sso'; import { routesForSourceControl } from './sourceControl'; +import { routesForWorkflows } from './workflow'; +import { routesForTags } from './tag'; const endpoints: Array<(server: Server) => void> = [ routesForCredentials, @@ -15,6 +17,8 @@ const endpoints: Array<(server: Server) => void> = [ routesForSettings, routesForSSO, routesForSourceControl, + routesForWorkflows, + routesForTags, ]; export { endpoints }; diff --git a/packages/editor-ui/src/__tests__/server/endpoints/tag.ts b/packages/editor-ui/src/__tests__/server/endpoints/tag.ts new file mode 100644 index 0000000000000..7891c20262b6d --- /dev/null +++ b/packages/editor-ui/src/__tests__/server/endpoints/tag.ts @@ -0,0 +1,11 @@ +import type { Server } from 'miragejs'; +import { Response } from 'miragejs'; +import type { AppSchema } from '../types'; + +export function routesForTags(server: Server) { + server.get('/rest/tags', (schema: AppSchema) => { + const { models: data } = schema.all('tag'); + + return new Response(200, {}, { data }); + }); +} diff --git a/packages/editor-ui/src/__tests__/server/endpoints/workflow.ts b/packages/editor-ui/src/__tests__/server/endpoints/workflow.ts new file mode 100644 index 0000000000000..4418cdb061377 --- /dev/null +++ b/packages/editor-ui/src/__tests__/server/endpoints/workflow.ts @@ -0,0 +1,16 @@ +import type { Server } from 'miragejs'; +import { Response } from 'miragejs'; +import type { AppSchema } from '../types'; + +export function routesForWorkflows(server: Server) { + server.get('/rest/workflows', (schema: AppSchema) => { + const { models: data } = schema.all('workflow'); + + return new Response(200, {}, { data }); + }); + server.get('/rest/active-workflows', (schema: AppSchema) => { + const { models: data } = schema.all('workflow'); + + return new Response(200, {}, { data }); + }); +} diff --git a/packages/editor-ui/src/__tests__/server/factories/index.ts b/packages/editor-ui/src/__tests__/server/factories/index.ts index 9b5155c28f1e8..77de915bad3a5 100644 --- a/packages/editor-ui/src/__tests__/server/factories/index.ts +++ b/packages/editor-ui/src/__tests__/server/factories/index.ts @@ -2,6 +2,8 @@ import { userFactory } from './user'; import { credentialFactory } from './credential'; import { credentialTypeFactory } from './credentialType'; import { variableFactory } from './variable'; +import { workflowFactory } from './workflow'; +import { tagFactory } from './tag'; export * from './user'; export * from './credential'; @@ -13,4 +15,6 @@ export const factories = { credentialType: credentialTypeFactory, user: userFactory, variable: variableFactory, + workflow: workflowFactory, + tag: tagFactory, }; diff --git a/packages/editor-ui/src/__tests__/server/factories/tag.ts b/packages/editor-ui/src/__tests__/server/factories/tag.ts new file mode 100644 index 0000000000000..bed3ca227679d --- /dev/null +++ b/packages/editor-ui/src/__tests__/server/factories/tag.ts @@ -0,0 +1,12 @@ +import { Factory } from 'miragejs'; +import type { ITag } from '@/Interface'; +import { faker } from '@faker-js/faker'; + +export const tagFactory = Factory.extend({ + id(i: string) { + return i; + }, + name() { + return faker.lorem.word(); + }, +}); diff --git a/packages/editor-ui/src/__tests__/server/factories/workflow.ts b/packages/editor-ui/src/__tests__/server/factories/workflow.ts new file mode 100644 index 0000000000000..6d12a8958b180 --- /dev/null +++ b/packages/editor-ui/src/__tests__/server/factories/workflow.ts @@ -0,0 +1,18 @@ +import { Factory } from 'miragejs'; +import type { IWorkflowDb } from '@/Interface'; +import { faker } from '@faker-js/faker'; + +export const workflowFactory = Factory.extend({ + id(i: string) { + return i; + }, + name() { + return faker.lorem.word(); + }, + createdAt() { + return faker.date.recent().toISOString(); + }, + tags() { + return faker.lorem.words(2.5).split(' '); + }, +}); diff --git a/packages/editor-ui/src/__tests__/server/fixtures/index.ts b/packages/editor-ui/src/__tests__/server/fixtures/index.ts new file mode 100644 index 0000000000000..12f59798817da --- /dev/null +++ b/packages/editor-ui/src/__tests__/server/fixtures/index.ts @@ -0,0 +1,7 @@ +import { tags } from './tags'; +import { workflows } from './workflows'; + +export const fixtures = { + tags, + workflows, +}; diff --git a/packages/editor-ui/src/__tests__/server/fixtures/tags.ts b/packages/editor-ui/src/__tests__/server/fixtures/tags.ts new file mode 100644 index 0000000000000..740acef117a84 --- /dev/null +++ b/packages/editor-ui/src/__tests__/server/fixtures/tags.ts @@ -0,0 +1,15 @@ +import type { ITag } from '@/Interface'; +export const tags: ITag[] = [ + { + id: '1', + name: 'tag1', + }, + { + id: '2', + name: 'tag2', + }, + { + id: '3', + name: 'tag3', + }, +]; diff --git a/packages/editor-ui/src/__tests__/server/fixtures/workflows.ts b/packages/editor-ui/src/__tests__/server/fixtures/workflows.ts new file mode 100644 index 0000000000000..68e05e0d9da6f --- /dev/null +++ b/packages/editor-ui/src/__tests__/server/fixtures/workflows.ts @@ -0,0 +1,47 @@ +import type { IWorkflowDb } from '@/Interface'; +import { faker } from '@faker-js/faker'; + +export const workflows = [ + { + id: '1', + name: 'workflow1', + tags: [], + }, + { + id: '2', + name: 'workflow2', + tags: [ + { id: '1', name: 'tag1' }, + { id: '2', name: 'tag2' }, + ], + }, + { + id: '3', + name: 'workflow3', + tags: [ + { id: '1', name: 'tag1' }, + { id: '3', name: 'tag3' }, + ], + }, + { + id: '4', + name: 'workflow4', + tags: [ + { id: '2', name: 'tag2' }, + { id: '3', name: 'tag3' }, + ], + }, + { + id: '5', + name: 'workflow5', + tags: [ + { id: '1', name: 'tag1' }, + { id: '2', name: 'tag2' }, + { id: '3', name: 'tag3' }, + ], + }, +].map((wf, idx) => ({ + ...wf, + createdAt: faker.date.recent().toISOString(), + updatedAt: new Date(`2024-1-${idx + 1}`).toISOString(), +})) as IWorkflowDb[]; diff --git a/packages/editor-ui/src/__tests__/server/index.ts b/packages/editor-ui/src/__tests__/server/index.ts index 18ea0a4be8b67..ef8d4e7a23cbf 100644 --- a/packages/editor-ui/src/__tests__/server/index.ts +++ b/packages/editor-ui/src/__tests__/server/index.ts @@ -2,12 +2,15 @@ import { createServer } from 'miragejs'; import { endpoints } from './endpoints'; import { models } from './models'; import { factories } from './factories'; +import { fixtures } from './fixtures'; export function setupServer() { const server = createServer({ models, factories, + fixtures, seeds(server) { + server.loadFixtures('tags', 'workflows'); server.createList('credentialType', 8); server.create('user', { firstName: 'Nathan', diff --git a/packages/editor-ui/src/__tests__/server/models/index.ts b/packages/editor-ui/src/__tests__/server/models/index.ts index 6b9f39327b9bf..f947fb9c531c1 100644 --- a/packages/editor-ui/src/__tests__/server/models/index.ts +++ b/packages/editor-ui/src/__tests__/server/models/index.ts @@ -2,10 +2,14 @@ import { UserModel } from './user'; import { CredentialModel } from './credential'; import { CredentialTypeModel } from './credentialType'; import { VariableModel } from './variable'; +import { WorkflowModel } from './workflow'; +import { TagModel } from './tag'; export const models = { credential: CredentialModel, credentialType: CredentialTypeModel, user: UserModel, variable: VariableModel, + workflow: WorkflowModel, + tag: TagModel, }; diff --git a/packages/editor-ui/src/__tests__/server/models/tag.ts b/packages/editor-ui/src/__tests__/server/models/tag.ts new file mode 100644 index 0000000000000..737e2288562c0 --- /dev/null +++ b/packages/editor-ui/src/__tests__/server/models/tag.ts @@ -0,0 +1,5 @@ +import type { ITag } from '@/Interface'; +import { Model } from 'miragejs'; +import type { ModelDefinition } from 'miragejs/-types'; + +export const TagModel: ModelDefinition = Model.extend({}); diff --git a/packages/editor-ui/src/__tests__/server/models/workflow.ts b/packages/editor-ui/src/__tests__/server/models/workflow.ts new file mode 100644 index 0000000000000..7be4f100035df --- /dev/null +++ b/packages/editor-ui/src/__tests__/server/models/workflow.ts @@ -0,0 +1,5 @@ +import type { IWorkflowDb } from '@/Interface'; +import { Model } from 'miragejs'; +import type { ModelDefinition } from 'miragejs/-types'; + +export const WorkflowModel: ModelDefinition = Model.extend({}); diff --git a/packages/editor-ui/src/components/layouts/ResourcesListLayout.vue b/packages/editor-ui/src/components/layouts/ResourcesListLayout.vue index 47e7a9a4c7882..a3d932b13da4c 100644 --- a/packages/editor-ui/src/components/layouts/ResourcesListLayout.vue +++ b/packages/editor-ui/src/components/layouts/ResourcesListLayout.vue @@ -454,6 +454,7 @@ export default defineComponent({ this.resettingFilters = true; this.sendFiltersTelemetry('reset'); + this.$emit('update:filters', this.filtersModel); }, focusSearchInput() { if (this.$refs.search) { diff --git a/packages/editor-ui/src/views/__tests__/WorkflowsView.test.ts b/packages/editor-ui/src/views/__tests__/WorkflowsView.test.ts new file mode 100644 index 0000000000000..9adbd8c637238 --- /dev/null +++ b/packages/editor-ui/src/views/__tests__/WorkflowsView.test.ts @@ -0,0 +1,99 @@ +import { afterAll, beforeAll } from 'vitest'; +import { setActivePinia, createPinia } from 'pinia'; +import { waitFor } from '@testing-library/vue'; +import userEvent from '@testing-library/user-event'; +import { setupServer } from '@/__tests__/server'; +import WorkflowsView from '@/views/WorkflowsView.vue'; +import { useSettingsStore } from '@/stores/settings.store'; +import { useUsersStore } from '@/stores/users.store'; +import { createComponentRenderer } from '@/__tests__/render'; + +const originalOffsetHeight = Object.getOwnPropertyDescriptor( + HTMLElement.prototype, + 'offsetHeight', +) as PropertyDescriptor; + +describe('WorkflowsView', () => { + let server: ReturnType; + let pinia: ReturnType; + let settingsStore: ReturnType; + let usersStore: ReturnType; + + const renderComponent = createComponentRenderer(WorkflowsView, { + global: { + mocks: { + $route: { + query: {}, + }, + $router: { + replace: vi.fn(), + }, + }, + }, + }); + + beforeAll(() => { + Object.defineProperties(HTMLElement.prototype, { + offsetHeight: { + get() { + return this.getAttribute('data-test-id') === 'resources-list' ? 1000 : 100; + }, + }, + }); + + server = setupServer(); + }); + + afterAll(() => { + Object.defineProperty(HTMLElement.prototype, 'offsetHeight', originalOffsetHeight); + }); + + beforeEach(async () => { + pinia = createPinia(); + setActivePinia(pinia); + + settingsStore = useSettingsStore(); + usersStore = useUsersStore(); + await settingsStore.getSettings(); + await usersStore.fetchUsers(); + await usersStore.loginWithCookie(); + }); + + afterAll(() => { + server.shutdown(); + }); + + it('should filter workflows by tags', async () => { + const { container, getByTestId, getAllByTestId, queryByTestId } = renderComponent({ + pinia, + }); + + expect(container.querySelectorAll('.n8n-loading')).toHaveLength(3); + expect(queryByTestId('resources-list')).not.toBeInTheDocument(); + + await waitFor(() => { + expect(container.querySelectorAll('.n8n-loading')).toHaveLength(0); + // There are 5 workflows defined in server fixtures + expect(getAllByTestId('resources-list-item')).toHaveLength(5); + }); + + await userEvent.click( + getAllByTestId('resources-list-item')[0].querySelector('.n8n-tag') as HTMLElement, + ); + await waitFor(() => { + expect(getAllByTestId('resources-list-item').length).toBeLessThan(5); + }); + + await userEvent.click(getByTestId('workflows-filter-reset')); + await waitFor(() => { + expect(getAllByTestId('resources-list-item')).toHaveLength(5); + }); + + await userEvent.click( + getAllByTestId('resources-list-item')[3].querySelector('.n8n-tag') as HTMLElement, + ); + await waitFor(() => { + expect(getAllByTestId('resources-list-item').length).toBeLessThan(5); + }); + }); +});