mirror of
https://github.com/n8n-io/n8n.git
synced 2024-12-26 05:04:05 -08:00
feat(editor): Add workflow filters to querystring (#7456)
fixes: https://linear.app/n8n/issue/ADO-1222/feature-save-filters-in-workflows
This commit is contained in:
parent
8171ad4fa8
commit
afd637b5ea
118
cypress/e2e/30-workflow-filters.cy.ts
Normal file
118
cypress/e2e/30-workflow-filters.cy.ts
Normal file
|
@ -0,0 +1,118 @@
|
||||||
|
import { WorkflowsPage as WorkflowsPageClass } from '../pages/workflows';
|
||||||
|
import { WorkflowPage as WorkflowPageClass } from '../pages/workflow';
|
||||||
|
|
||||||
|
import { MainSidebar } from '../pages';
|
||||||
|
import { INSTANCE_OWNER } from '../constants';
|
||||||
|
|
||||||
|
const WorkflowsPage = new WorkflowsPageClass();
|
||||||
|
const WorkflowPages = new WorkflowPageClass();
|
||||||
|
const mainSidebar = new MainSidebar();
|
||||||
|
|
||||||
|
describe('Workflow filters', () => {
|
||||||
|
before(() => {
|
||||||
|
cy.enableFeature('sharing', true);
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.visit(WorkflowsPage.url);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should filter by tags', () => {
|
||||||
|
cy.visit(WorkflowsPage.url);
|
||||||
|
WorkflowsPage.getters.newWorkflowButtonCard().click();
|
||||||
|
cy.createFixtureWorkflow('Test_workflow_1.json', `Workflow 1`);
|
||||||
|
cy.visit(WorkflowsPage.url);
|
||||||
|
WorkflowsPage.getters.createWorkflowButton().click();
|
||||||
|
cy.createFixtureWorkflow('Test_workflow_2.json', `Workflow 2`);
|
||||||
|
cy.visit(WorkflowsPage.url);
|
||||||
|
|
||||||
|
WorkflowsPage.getters.workflowFilterButton().click();
|
||||||
|
WorkflowsPage.getters.workflowTagsDropdown().click();
|
||||||
|
WorkflowsPage.getters.workflowTagItem('other-tag-1').click();
|
||||||
|
cy.get('body').click(0, 0);
|
||||||
|
|
||||||
|
WorkflowsPage.getters.workflowCards().should('have.length', 1);
|
||||||
|
WorkflowsPage.getters.workflowCard('Workflow 2').should('contain.text', 'Workflow 2');
|
||||||
|
mainSidebar.actions.goToSettings();
|
||||||
|
cy.go('back');
|
||||||
|
|
||||||
|
WorkflowsPage.getters.workflowCards().should('have.length', 1);
|
||||||
|
WorkflowsPage.getters.workflowCard('Workflow 2').should('contain.text', 'Workflow 2');
|
||||||
|
WorkflowsPage.getters.workflowResetFilters().click();
|
||||||
|
|
||||||
|
WorkflowsPage.getters.workflowCards().each(($el) => {
|
||||||
|
const workflowName = $el.find('[data-test-id="workflow-card-name"]').text();
|
||||||
|
|
||||||
|
WorkflowsPage.getters.workflowCardActions(workflowName).click();
|
||||||
|
WorkflowsPage.getters.workflowDeleteButton().click();
|
||||||
|
|
||||||
|
cy.get('button').contains('delete').click();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should filter by status', () => {
|
||||||
|
cy.visit(WorkflowsPage.url);
|
||||||
|
WorkflowsPage.getters.newWorkflowButtonCard().click();
|
||||||
|
cy.createFixtureWorkflow('Test_workflow_1.json', `Workflow 1`);
|
||||||
|
cy.visit(WorkflowsPage.url);
|
||||||
|
WorkflowsPage.getters.createWorkflowButton().click();
|
||||||
|
cy.createFixtureWorkflow('Test_workflow_3.json', `Workflow 3`);
|
||||||
|
WorkflowPages.getters.activatorSwitch().click();
|
||||||
|
cy.visit(WorkflowsPage.url);
|
||||||
|
|
||||||
|
WorkflowsPage.getters.workflowFilterButton().click();
|
||||||
|
WorkflowsPage.getters.workflowStatusDropdown().click();
|
||||||
|
WorkflowsPage.getters.workflowStatusItem('Active').click();
|
||||||
|
cy.get('body').click(0, 0);
|
||||||
|
|
||||||
|
WorkflowsPage.getters.workflowCards().should('have.length', 1);
|
||||||
|
WorkflowsPage.getters.workflowCard('Workflow 3').should('contain.text', 'Workflow 3');
|
||||||
|
mainSidebar.actions.goToSettings();
|
||||||
|
cy.go('back');
|
||||||
|
|
||||||
|
WorkflowsPage.getters.workflowCards().should('have.length', 1);
|
||||||
|
WorkflowsPage.getters.workflowCard('Workflow 3').should('contain.text', 'Workflow 3');
|
||||||
|
WorkflowsPage.getters.workflowResetFilters().click();
|
||||||
|
|
||||||
|
WorkflowsPage.getters.workflowCards().each(($el) => {
|
||||||
|
const workflowName = $el.find('[data-test-id="workflow-card-name"]').text();
|
||||||
|
|
||||||
|
WorkflowsPage.getters.workflowCardActions(workflowName).click();
|
||||||
|
WorkflowsPage.getters.workflowDeleteButton().click();
|
||||||
|
|
||||||
|
cy.get('button').contains('delete').click();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should filter by owned by', () => {
|
||||||
|
cy.visit(WorkflowsPage.url);
|
||||||
|
|
||||||
|
WorkflowsPage.getters.newWorkflowButtonCard().click();
|
||||||
|
cy.createFixtureWorkflow('Test_workflow_1.json', `Workflow 1`);
|
||||||
|
cy.visit(WorkflowsPage.url);
|
||||||
|
WorkflowsPage.getters.createWorkflowButton().click();
|
||||||
|
cy.createFixtureWorkflow('Test_workflow_3.json', `Workflow 3`);
|
||||||
|
WorkflowPages.getters.activatorSwitch().click();
|
||||||
|
cy.visit(WorkflowsPage.url);
|
||||||
|
|
||||||
|
WorkflowsPage.getters.workflowFilterButton().click();
|
||||||
|
WorkflowsPage.getters.workflowOwnershipDropdown().realClick();
|
||||||
|
WorkflowsPage.getters.workflowOwner(INSTANCE_OWNER.email).click();
|
||||||
|
cy.get('body').click(0, 0);
|
||||||
|
|
||||||
|
WorkflowsPage.getters.workflowCards().should('have.length', 2);
|
||||||
|
mainSidebar.actions.goToSettings();
|
||||||
|
cy.go('back');
|
||||||
|
|
||||||
|
WorkflowsPage.getters.workflowResetFilters().click();
|
||||||
|
|
||||||
|
WorkflowsPage.getters.workflowCards().each(($el) => {
|
||||||
|
const workflowName = $el.find('[data-test-id="workflow-card-name"]').text();
|
||||||
|
|
||||||
|
WorkflowsPage.getters.workflowCardActions(workflowName).click();
|
||||||
|
WorkflowsPage.getters.workflowDeleteButton().click();
|
||||||
|
|
||||||
|
cy.get('button').contains('delete').click();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -23,6 +23,14 @@ export class WorkflowsPage extends BasePage {
|
||||||
this.getters.workflowCard(workflowName).findChildByTestId('workflow-card-actions'),
|
this.getters.workflowCard(workflowName).findChildByTestId('workflow-card-actions'),
|
||||||
workflowDeleteButton: () =>
|
workflowDeleteButton: () =>
|
||||||
cy.getByTestId('action-toggle-dropdown').filter(':visible').contains('Delete'),
|
cy.getByTestId('action-toggle-dropdown').filter(':visible').contains('Delete'),
|
||||||
|
workflowFilterButton: () => cy.getByTestId('resources-list-filters-trigger').filter(':visible'),
|
||||||
|
workflowTagsDropdown: () => cy.getByTestId('tags-dropdown'),
|
||||||
|
workflowTagItem: (tag: string) => cy.getByTestId('tag').contains(tag),
|
||||||
|
workflowStatusDropdown: () => cy.getByTestId('status-dropdown'),
|
||||||
|
workflowStatusItem: (status: string) => cy.getByTestId('status').contains(status),
|
||||||
|
workflowOwnershipDropdown: () => cy.getByTestId('user-select-trigger'),
|
||||||
|
workflowOwner: (email: string) => cy.getByTestId('user-email').contains(email),
|
||||||
|
workflowResetFilters: () => cy.getByTestId('workflows-filter-reset'),
|
||||||
// Not yet implemented
|
// Not yet implemented
|
||||||
// myWorkflows: () => cy.getByTestId('my-workflows'),
|
// myWorkflows: () => cy.getByTestId('my-workflows'),
|
||||||
// allWorkflows: () => cy.getByTestId('all-workflows'),
|
// allWorkflows: () => cy.getByTestId('all-workflows'),
|
||||||
|
|
|
@ -19,7 +19,7 @@
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<n8n-text size="small" color="text-light">{{ email }}</n8n-text>
|
<n8n-text data-test-id="user-email" size="small" color="text-light">{{ email }}</n8n-text>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<n8n-select
|
<n8n-select
|
||||||
|
data-test-id="user-select-trigger"
|
||||||
v-bind="$attrs"
|
v-bind="$attrs"
|
||||||
:modelValue="modelValue"
|
:modelValue="modelValue"
|
||||||
:filterable="true"
|
:filterable="true"
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
ref="selectRef"
|
ref="selectRef"
|
||||||
loading-text="..."
|
loading-text="..."
|
||||||
popper-class="tags-dropdown"
|
popper-class="tags-dropdown"
|
||||||
|
data-test-id="tags-dropdown"
|
||||||
@update:modelValue="onTagsUpdated"
|
@update:modelValue="onTagsUpdated"
|
||||||
@visible-change="onVisibleChange"
|
@visible-change="onVisibleChange"
|
||||||
@remove-tag="onRemoveTag"
|
@remove-tag="onRemoveTag"
|
||||||
|
@ -48,6 +49,7 @@
|
||||||
:key="tag.id + '_' + i"
|
:key="tag.id + '_' + i"
|
||||||
:label="tag.name"
|
:label="tag.name"
|
||||||
class="tag"
|
class="tag"
|
||||||
|
data-test-id="tag"
|
||||||
ref="tagRefs"
|
ref="tagRefs"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
|
@ -107,7 +107,7 @@
|
||||||
<div v-if="showFiltersDropdown" v-show="hasFilters" class="mt-xs">
|
<div v-if="showFiltersDropdown" v-show="hasFilters" class="mt-xs">
|
||||||
<n8n-info-tip :bold="false">
|
<n8n-info-tip :bold="false">
|
||||||
{{ i18n.baseText(`${resourceKey}.filters.active`) }}
|
{{ i18n.baseText(`${resourceKey}.filters.active`) }}
|
||||||
<n8n-link @click="resetFilters" size="small">
|
<n8n-link data-test-id="workflows-filter-reset" @click="resetFilters" size="small">
|
||||||
{{ i18n.baseText(`${resourceKey}.filters.active.reset`) }}
|
{{ i18n.baseText(`${resourceKey}.filters.active.reset`) }}
|
||||||
</n8n-link>
|
</n8n-link>
|
||||||
</n8n-info-tip>
|
</n8n-info-tip>
|
||||||
|
@ -392,6 +392,19 @@ export default defineComponent({
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
await this.$nextTick();
|
await this.$nextTick();
|
||||||
this.focusSearchInput();
|
this.focusSearchInput();
|
||||||
|
|
||||||
|
if (this.hasAppliedFilters()) {
|
||||||
|
this.hasFilters = true;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
hasAppliedFilters(): boolean {
|
||||||
|
return !!this.filterKeys.find(
|
||||||
|
(key) =>
|
||||||
|
key !== 'search' &&
|
||||||
|
(Array.isArray(this.filters[key])
|
||||||
|
? this.filters[key].length > 0
|
||||||
|
: this.filters[key] !== ''),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
setCurrentPage(page: number) {
|
setCurrentPage(page: number) {
|
||||||
this.currentPage = page;
|
this.currentPage = page;
|
||||||
|
|
|
@ -101,12 +101,17 @@
|
||||||
color="text-base"
|
color="text-base"
|
||||||
class="mb-3xs"
|
class="mb-3xs"
|
||||||
/>
|
/>
|
||||||
<n8n-select :modelValue="filters.status" @update:modelValue="setKeyValue('status', $event)">
|
<n8n-select
|
||||||
|
data-test-id="status-dropdown"
|
||||||
|
:modelValue="filters.status"
|
||||||
|
@update:modelValue="setKeyValue('status', $event)"
|
||||||
|
>
|
||||||
<n8n-option
|
<n8n-option
|
||||||
v-for="option in statusFilterOptions"
|
v-for="option in statusFilterOptions"
|
||||||
:key="option.label"
|
:key="option.label"
|
||||||
:label="option.label"
|
:label="option.label"
|
||||||
:value="option.value"
|
:value="option.value"
|
||||||
|
data-test-id="status"
|
||||||
>
|
>
|
||||||
</n8n-option>
|
</n8n-option>
|
||||||
</n8n-select>
|
</n8n-select>
|
||||||
|
@ -130,6 +135,7 @@ import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||||
import { useCredentialsStore } from '@/stores/credentials.store';
|
import { useCredentialsStore } from '@/stores/credentials.store';
|
||||||
import { useSourceControlStore } from '@/stores/sourceControl.store';
|
import { useSourceControlStore } from '@/stores/sourceControl.store';
|
||||||
import { genericHelpers } from '@/mixins/genericHelpers';
|
import { genericHelpers } from '@/mixins/genericHelpers';
|
||||||
|
import { useTagsStore } from '@/stores';
|
||||||
|
|
||||||
type IResourcesListLayoutInstance = InstanceType<typeof ResourcesListLayout>;
|
type IResourcesListLayoutInstance = InstanceType<typeof ResourcesListLayout>;
|
||||||
|
|
||||||
|
@ -153,7 +159,7 @@ const WorkflowsView = defineComponent({
|
||||||
search: '',
|
search: '',
|
||||||
ownedBy: '',
|
ownedBy: '',
|
||||||
sharedWith: '',
|
sharedWith: '',
|
||||||
status: StatusFilter.ALL,
|
status: StatusFilter.ALL as string | boolean,
|
||||||
tags: [] as string[],
|
tags: [] as string[],
|
||||||
},
|
},
|
||||||
sourceControlStoreUnsubscribe: () => {},
|
sourceControlStoreUnsubscribe: () => {},
|
||||||
|
@ -167,6 +173,7 @@ const WorkflowsView = defineComponent({
|
||||||
useWorkflowsStore,
|
useWorkflowsStore,
|
||||||
useCredentialsStore,
|
useCredentialsStore,
|
||||||
useSourceControlStore,
|
useSourceControlStore,
|
||||||
|
useTagsStore,
|
||||||
),
|
),
|
||||||
currentUser(): IUser {
|
currentUser(): IUser {
|
||||||
return this.usersStore.currentUser || ({} as IUser);
|
return this.usersStore.currentUser || ({} as IUser);
|
||||||
|
@ -221,6 +228,8 @@ const WorkflowsView = defineComponent({
|
||||||
filters: { tags: string[]; search: string; status: string | boolean },
|
filters: { tags: string[]; search: string; status: string | boolean },
|
||||||
matches: boolean,
|
matches: boolean,
|
||||||
): boolean {
|
): boolean {
|
||||||
|
this.saveFiltersOnQueryString();
|
||||||
|
|
||||||
if (this.settingsStore.areTagsEnabled && filters.tags.length > 0) {
|
if (this.settingsStore.areTagsEnabled && filters.tags.length > 0) {
|
||||||
matches =
|
matches =
|
||||||
matches &&
|
matches &&
|
||||||
|
@ -243,6 +252,77 @@ const WorkflowsView = defineComponent({
|
||||||
sendFiltersTelemetry(source: string) {
|
sendFiltersTelemetry(source: string) {
|
||||||
(this.$refs.layout as IResourcesListLayoutInstance).sendFiltersTelemetry(source);
|
(this.$refs.layout as IResourcesListLayoutInstance).sendFiltersTelemetry(source);
|
||||||
},
|
},
|
||||||
|
saveFiltersOnQueryString() {
|
||||||
|
const query: { [key: string]: string } = {};
|
||||||
|
|
||||||
|
if (this.filters.search) {
|
||||||
|
query.search = this.filters.search;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof this.filters.status !== 'string') {
|
||||||
|
query.status = this.filters.status.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.filters.tags.length) {
|
||||||
|
query.tags = this.filters.tags.join(',');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.filters.ownedBy) {
|
||||||
|
query.ownedBy = this.filters.ownedBy;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.filters.sharedWith) {
|
||||||
|
query.sharedWith = this.filters.sharedWith;
|
||||||
|
}
|
||||||
|
|
||||||
|
void this.$router.replace({
|
||||||
|
name: VIEWS.WORKFLOWS,
|
||||||
|
query,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
isValidUserId(userId: string) {
|
||||||
|
return Object.keys(this.usersStore.users).includes(userId);
|
||||||
|
},
|
||||||
|
setFiltersFromQueryString() {
|
||||||
|
const { tags, status, search, ownedBy, sharedWith } = this.$route.query;
|
||||||
|
|
||||||
|
const filtersToApply: { [key: string]: string | string[] | boolean } = {};
|
||||||
|
|
||||||
|
if (ownedBy && typeof ownedBy === 'string' && this.isValidUserId(ownedBy)) {
|
||||||
|
filtersToApply.ownedBy = ownedBy;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sharedWith && typeof sharedWith === 'string' && this.isValidUserId(sharedWith)) {
|
||||||
|
filtersToApply.sharedWith = sharedWith;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (search && typeof search === 'string') {
|
||||||
|
filtersToApply.search = search;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tags && typeof tags === 'string') {
|
||||||
|
const currentTags = this.tagsStore.allTags.map((tag) => tag.id);
|
||||||
|
const savedTags = tags.split(',').filter((tag) => currentTags.includes(tag));
|
||||||
|
if (savedTags.length) {
|
||||||
|
filtersToApply.tags = savedTags;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
status &&
|
||||||
|
typeof status === 'string' &&
|
||||||
|
[StatusFilter.ACTIVE.toString(), StatusFilter.DEACTIVATED.toString()].includes(status)
|
||||||
|
) {
|
||||||
|
filtersToApply.status = status === 'true';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.keys(filtersToApply).length) {
|
||||||
|
this.filters = {
|
||||||
|
...this.filters,
|
||||||
|
...filtersToApply,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
'filters.tags'() {
|
'filters.tags'() {
|
||||||
|
@ -250,6 +330,8 @@ const WorkflowsView = defineComponent({
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
|
this.setFiltersFromQueryString();
|
||||||
|
|
||||||
void this.usersStore.showPersonalizationSurvey();
|
void this.usersStore.showPersonalizationSurvey();
|
||||||
|
|
||||||
this.sourceControlStoreUnsubscribe = this.sourceControlStore.$onAction(({ name, after }) => {
|
this.sourceControlStoreUnsubscribe = this.sourceControlStore.$onAction(({ name, after }) => {
|
||||||
|
|
Loading…
Reference in a new issue