mirror of
https://github.com/n8n-io/n8n.git
synced 2024-12-25 12:44:07 -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'),
|
||||
workflowDeleteButton: () =>
|
||||
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
|
||||
// myWorkflows: () => cy.getByTestId('my-workflows'),
|
||||
// allWorkflows: () => cy.getByTestId('all-workflows'),
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
</span>
|
||||
</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>
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
<template>
|
||||
<n8n-select
|
||||
data-test-id="user-select-trigger"
|
||||
v-bind="$attrs"
|
||||
:modelValue="modelValue"
|
||||
:filterable="true"
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
ref="selectRef"
|
||||
loading-text="..."
|
||||
popper-class="tags-dropdown"
|
||||
data-test-id="tags-dropdown"
|
||||
@update:modelValue="onTagsUpdated"
|
||||
@visible-change="onVisibleChange"
|
||||
@remove-tag="onRemoveTag"
|
||||
|
@ -48,6 +49,7 @@
|
|||
:key="tag.id + '_' + i"
|
||||
:label="tag.name"
|
||||
class="tag"
|
||||
data-test-id="tag"
|
||||
ref="tagRefs"
|
||||
/>
|
||||
|
||||
|
|
|
@ -107,7 +107,7 @@
|
|||
<div v-if="showFiltersDropdown" v-show="hasFilters" class="mt-xs">
|
||||
<n8n-info-tip :bold="false">
|
||||
{{ 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`) }}
|
||||
</n8n-link>
|
||||
</n8n-info-tip>
|
||||
|
@ -392,6 +392,19 @@ export default defineComponent({
|
|||
this.loading = false;
|
||||
await this.$nextTick();
|
||||
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) {
|
||||
this.currentPage = page;
|
||||
|
|
|
@ -101,12 +101,17 @@
|
|||
color="text-base"
|
||||
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
|
||||
v-for="option in statusFilterOptions"
|
||||
:key="option.label"
|
||||
:label="option.label"
|
||||
:value="option.value"
|
||||
data-test-id="status"
|
||||
>
|
||||
</n8n-option>
|
||||
</n8n-select>
|
||||
|
@ -130,6 +135,7 @@ import { useWorkflowsStore } from '@/stores/workflows.store';
|
|||
import { useCredentialsStore } from '@/stores/credentials.store';
|
||||
import { useSourceControlStore } from '@/stores/sourceControl.store';
|
||||
import { genericHelpers } from '@/mixins/genericHelpers';
|
||||
import { useTagsStore } from '@/stores';
|
||||
|
||||
type IResourcesListLayoutInstance = InstanceType<typeof ResourcesListLayout>;
|
||||
|
||||
|
@ -153,7 +159,7 @@ const WorkflowsView = defineComponent({
|
|||
search: '',
|
||||
ownedBy: '',
|
||||
sharedWith: '',
|
||||
status: StatusFilter.ALL,
|
||||
status: StatusFilter.ALL as string | boolean,
|
||||
tags: [] as string[],
|
||||
},
|
||||
sourceControlStoreUnsubscribe: () => {},
|
||||
|
@ -167,6 +173,7 @@ const WorkflowsView = defineComponent({
|
|||
useWorkflowsStore,
|
||||
useCredentialsStore,
|
||||
useSourceControlStore,
|
||||
useTagsStore,
|
||||
),
|
||||
currentUser(): IUser {
|
||||
return this.usersStore.currentUser || ({} as IUser);
|
||||
|
@ -221,6 +228,8 @@ const WorkflowsView = defineComponent({
|
|||
filters: { tags: string[]; search: string; status: string | boolean },
|
||||
matches: boolean,
|
||||
): boolean {
|
||||
this.saveFiltersOnQueryString();
|
||||
|
||||
if (this.settingsStore.areTagsEnabled && filters.tags.length > 0) {
|
||||
matches =
|
||||
matches &&
|
||||
|
@ -243,6 +252,77 @@ const WorkflowsView = defineComponent({
|
|||
sendFiltersTelemetry(source: string) {
|
||||
(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: {
|
||||
'filters.tags'() {
|
||||
|
@ -250,6 +330,8 @@ const WorkflowsView = defineComponent({
|
|||
},
|
||||
},
|
||||
mounted() {
|
||||
this.setFiltersFromQueryString();
|
||||
|
||||
void this.usersStore.showPersonalizationSurvey();
|
||||
|
||||
this.sourceControlStoreUnsubscribe = this.sourceControlStore.$onAction(({ name, after }) => {
|
||||
|
|
Loading…
Reference in a new issue