From d1e65a1cd5841f1d4e815f8da36713cdb18281a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Milorad=20FIlipovi=C4=87?= Date: Mon, 17 Feb 2025 11:38:35 +0100 Subject: [PATCH] fix(editor): Prevent pagination setting from being overwritten in URL (#13266) --- cypress/e2e/1-workflows.cy.ts | 41 ++++++++++++++ cypress/pages/workflows.ts | 12 +++++ .../layouts/ResourcesListLayout.vue | 2 + .../editor-ui/src/views/WorkflowsView.vue | 54 +++++++++++++------ 4 files changed, 92 insertions(+), 17 deletions(-) diff --git a/cypress/e2e/1-workflows.cy.ts b/cypress/e2e/1-workflows.cy.ts index a6683bbee4..82d1b9b90b 100644 --- a/cypress/e2e/1-workflows.cy.ts +++ b/cypress/e2e/1-workflows.cy.ts @@ -97,4 +97,45 @@ describe('Workflows', () => { WorkflowsPage.getters.workflowCards().should('have.length', 1); }); + + it('should preserve filters and pagination in URL', () => { + // Add a search query + WorkflowsPage.getters.searchBar().type('My'); + // Add a tag filter + WorkflowsPage.getters.workflowFilterButton().click(); + WorkflowsPage.getters.workflowTagsDropdown().click(); + WorkflowsPage.getters.workflowTagItem('other-tag-1').click(); + WorkflowsPage.getters.workflowsListContainer().click(); + // Update sort order + WorkflowsPage.getters.workflowSortDropdown().click(); + WorkflowsPage.getters.workflowSortItem('Sort by last created').click({ force: true }); + // Update page size + WorkflowsPage.getters.workflowListPageSizeDropdown().click(); + WorkflowsPage.getters.workflowListPageSizeItem('25').click(); + + // URL should contain all applied filters and pagination + cy.url().should('include', 'search=My'); + // Cannot really know tag id, so just check if it contains 'tags=' + cy.url().should('include', 'tags='); + cy.url().should('include', 'sort=lastCreated'); + cy.url().should('include', 'pageSize=25'); + + // Reload the page + cy.reload(); + // Check if filters and pagination are preserved + WorkflowsPage.getters.searchBar().should('have.value', 'My'); + WorkflowsPage.getters.workflowFilterButton().click(); + WorkflowsPage.getters.workflowTagsDropdown().should('contain.text', 'other-tag-1'); + WorkflowsPage.getters + .workflowSortItem('Sort by last created') + .should('have.attr', 'aria-selected', 'true'); + WorkflowsPage.getters + .workflowListPageSizeItem('25', false) + .should('have.attr', 'aria-selected', 'true'); + // Aso, check if the URL is preserved + cy.url().should('include', 'search=My'); + cy.url().should('include', 'tags='); + cy.url().should('include', 'sort=lastCreated'); + cy.url().should('include', 'pageSize=25'); + }); }); diff --git a/cypress/pages/workflows.ts b/cypress/pages/workflows.ts index 7441bfa256..87507c80a1 100644 --- a/cypress/pages/workflows.ts +++ b/cypress/pages/workflows.ts @@ -47,6 +47,18 @@ export class WorkflowsPage extends BasePage { workflowOwnershipDropdown: () => cy.getByTestId('user-select-trigger'), workflowOwner: (email: string) => cy.getByTestId('user-email').contains(email), workflowResetFilters: () => cy.getByTestId('workflows-filter-reset'), + workflowSortDropdown: () => cy.getByTestId('resources-list-sort'), + workflowSortItem: (sort: string) => + cy.getByTestId('resources-list-sort-item').contains(sort).parent(), + workflowPagination: () => cy.getByTestId('resources-list-pagination'), + workflowListPageSizeDropdown: () => this.getters.workflowPagination().find('.select-trigger'), + workflowListPageSizeItem: (pageSize: string, visible: boolean = true) => { + if (visible) { + return cy.get('[role=option]').filter(':visible').contains(`${pageSize}/page`); + } + return cy.get('[role=option]').contains(`${pageSize}/page`).parent(); + }, + workflowsListContainer: () => cy.getByTestId('resources-list-wrapper'), // Not yet implemented // myWorkflows: () => cy.getByTestId('my-workflows'), // allWorkflows: () => cy.getByTestId('all-workflows'), diff --git a/packages/editor-ui/src/components/layouts/ResourcesListLayout.vue b/packages/editor-ui/src/components/layouts/ResourcesListLayout.vue index f0a1859de0..db811c61e9 100644 --- a/packages/editor-ui/src/components/layouts/ResourcesListLayout.vue +++ b/packages/editor-ui/src/components/layouts/ResourcesListLayout.vue @@ -557,6 +557,7 @@ const loadPaginationFromQueryString = async () => {
@@ -588,6 +589,7 @@ const loadPaginationFromQueryString = async () => { :total="totalItems" :page-sizes="availablePageSizeOptions" layout="total, prev, pager, next, sizes" + data-test-id="resources-list-pagination" @update:current-page="setCurrentPage" @size-change="setRowsPerPage" > diff --git a/packages/editor-ui/src/views/WorkflowsView.vue b/packages/editor-ui/src/views/WorkflowsView.vue index 4a48854e95..e67099f78a 100644 --- a/packages/editor-ui/src/views/WorkflowsView.vue +++ b/packages/editor-ui/src/views/WorkflowsView.vue @@ -185,12 +185,11 @@ const onFiltersUpdated = async () => { }; const onSearchUpdated = async (search: string) => { + currentPage.value = 1; + saveFiltersOnQueryString(); if (search) { - currentPage.value = 1; - saveFiltersOnQueryString(); await callDebounced(fetchWorkflows, { debounceTime: 500, trailing: true }); } else { - currentPage.value = 1; // No need to debounce when clearing search await fetchWorkflows(); } @@ -306,46 +305,67 @@ function isValidProjectId(projectId: string) { } const setFiltersFromQueryString = async () => { + const newQuery: LocationQueryRaw = { ...route.query }; const { tags, status, search, homeProject, sort } = route.query ?? {}; - const newQuery: LocationQueryRaw = {}; - if (homeProject && typeof homeProject === 'string') { + // Helper to check if string value is not empty + const isValidString = (value: unknown): value is string => + typeof value === 'string' && value.trim().length > 0; + + // Handle home project + if (isValidString(homeProject)) { await projectsStore.getAvailableProjects(); if (isValidProjectId(homeProject)) { newQuery.homeProject = homeProject; filters.value.homeProject = homeProject; + } else { + delete newQuery.homeProject; } + } else { + delete newQuery.homeProject; } - if (search && typeof search === 'string') { + // Handle search + if (isValidString(search)) { newQuery.search = search; filters.value.search = search; + } else { + delete newQuery.search; } - if (tags && typeof tags === 'string') { + // Handle tags + if (isValidString(tags)) { await tagsStore.fetchAll(); - const currentTags = tagsStore.allTags.map((tag) => tag.id); - const validTags = tags.split(',').filter((tag) => currentTags.includes(tag)); + const validTags = tags + .split(',') + .filter((tag) => tagsStore.allTags.map((t) => t.id).includes(tag)); if (validTags.length) { newQuery.tags = validTags.join(','); filters.value.tags = validTags; + } else { + delete newQuery.tags; } + } else { + delete newQuery.tags; } - if ( - status && - typeof status === 'string' && - [StatusFilter.ACTIVE.toString(), StatusFilter.DEACTIVATED.toString()].includes(status) - ) { - newQuery.status = status; // Keep as string in URL - filters.value.status = status === 'true'; // Convert to boolean for filters + // Handle status + const validStatusValues = [StatusFilter.ACTIVE.toString(), StatusFilter.DEACTIVATED.toString()]; + if (isValidString(status) && validStatusValues.includes(status)) { + newQuery.status = status; + filters.value.status = status === 'true'; + } else { + delete newQuery.status; } - if (sort && typeof sort === 'string') { + // Handle sort + if (isValidString(sort)) { const newSort = WORKFLOWS_SORT_MAP[sort as keyof typeof WORKFLOWS_SORT_MAP] ?? 'updatedAt:desc'; newQuery.sort = sort; currentSort.value = newSort; + } else { + delete newQuery.sort; } void router.replace({ query: newQuery });