diff --git a/cypress/e2e/1-workflows.cy.ts b/cypress/e2e/1-workflows.cy.ts index 022d4ec73b..0b5a3069ab 100644 --- a/cypress/e2e/1-workflows.cy.ts +++ b/cypress/e2e/1-workflows.cy.ts @@ -99,7 +99,7 @@ describe('Workflows', () => { WorkflowsPage.getters.workflowCards().should('have.length', 1); }); - it('should preserve filters and pagination in URL', () => { + it('should preserve filters in URL', () => { // Add a search query WorkflowsPage.getters.searchBar().type('My'); // Add a tag filter @@ -118,8 +118,6 @@ describe('Workflows', () => { 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(); @@ -136,8 +134,6 @@ describe('Workflows', () => { // 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'); }); it('should be able to share workflows from workflows list', () => { diff --git a/packages/frontend/editor-ui/src/components/TagsDropdown.vue b/packages/frontend/editor-ui/src/components/TagsDropdown.vue index d2431f0e6b..db5c2cbca1 100644 --- a/packages/frontend/editor-ui/src/components/TagsDropdown.vue +++ b/packages/frontend/editor-ui/src/components/TagsDropdown.vue @@ -240,10 +240,10 @@ onClickOutside( {{ i18n.baseText('tagsDropdown.typeToCreateATag') }} - {{ + {{ i18n.baseText('tagsDropdown.noMatchingTagsExist') }} - {{ i18n.baseText('tagsDropdown.noTagsExist') }} + {{ i18n.baseText('tagsDropdown.noTagsExist') }} ({ + id: '1', + type: 'team', + name: 'Test Project 1', + icon: { + type: 'emoji', + value: '🗂️', + }, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), +})); + +const TEST_LOCAL_STORAGE_VALUES = vi.hoisted(() => ({ + sort: 'name', + pageSize: 50, +})); + +const TEST_WORKFLOWS: Resource[] = vi.hoisted(() => [ + { + resourceType: 'workflow', + id: '1', + name: 'Test Workflow 1', + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + active: true, + readOnly: false, + homeProject: TEST_HOME_PROJECT, + }, + { + resourceType: 'workflow', + id: '2', + name: 'Test Workflow 2', + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + active: true, + readOnly: false, + homeProject: TEST_HOME_PROJECT, + }, +]); vi.mock('@/composables/useGlobalEntityCreation', () => ({ useGlobalEntityCreation: () => ({ @@ -10,18 +52,42 @@ vi.mock('@/composables/useGlobalEntityCreation', () => ({ }), })); +vi.mock('@/composables/useN8nLocalStorage', () => { + const loadProjectPreferencesFromLocalStorage = vi.fn().mockReturnValue(TEST_LOCAL_STORAGE_VALUES); + const getProjectKey = vi.fn(() => TEST_HOME_PROJECT.id); + + return { + useN8nLocalStorage: () => ({ + loadProjectPreferencesFromLocalStorage, + getProjectKey, + }), + }; +}); + vi.mock('vue-router', async (importOriginal) => { const { RouterLink } = await importOriginal(); return { RouterLink, useRoute: () => ({ - params: {}, + params: { + projectId: TEST_HOME_PROJECT.id, + }, + path: `/projects/${TEST_HOME_PROJECT.id}/workflows`, + query: {}, }), useRouter: vi.fn(), }; }); -const renderComponent = createComponentRenderer(ResourcesListLayout); +const renderComponent = createComponentRenderer(ResourcesListLayout, { + props: { + resourceKey: 'workflows', + resources: [], + disabled: false, + typeProps: { itemSize: 80 }, + loading: false, + }, +}); describe('ResourcesListLayout', () => { beforeEach(() => { @@ -38,4 +104,49 @@ describe('ResourcesListLayout', () => { expect(container.querySelectorAll('.el-skeleton__p')).toHaveLength(25); }); + + it('should render scrollable list based on `type` prop', () => { + const { getByTestId } = renderComponent({ + props: { + resources: TEST_WORKFLOWS, + type: 'list-full', + }, + }); + expect(getByTestId('resources-list-wrapper')).toBeTruthy(); + expect(getByTestId('resources-list')).toBeTruthy(); + }); + + it('should render paginated list based on `type` prop', () => { + const { getByTestId } = renderComponent({ + props: { + resources: TEST_WORKFLOWS, + type: 'list-paginated', + }, + }); + expect(getByTestId('resources-list-wrapper')).toBeTruthy(); + expect(getByTestId('paginated-list')).toBeTruthy(); + }); + + it('should render paginated table list based on `type` prop', () => { + const { getByTestId } = renderComponent({ + props: { + resources: TEST_WORKFLOWS, + type: 'datatable', + }, + }); + expect(getByTestId('resources-list-wrapper')).toBeTruthy(); + expect(getByTestId('resources-table')).toBeTruthy(); + }); + + it('should load sort & page size from local storage', async () => { + const { emitted } = renderComponent({ + props: { + resources: TEST_WORKFLOWS, + type: 'list-paginated', + }, + }); + await waitAllPromises(); + expect(emitted()['update:pagination-and-sort']).toBeTruthy(); + expect(emitted()['update:pagination-and-sort'].pop()).toEqual([TEST_LOCAL_STORAGE_VALUES]); + }); }); diff --git a/packages/frontend/editor-ui/src/components/layouts/ResourcesListLayout.vue b/packages/frontend/editor-ui/src/components/layouts/ResourcesListLayout.vue index 67d970c455..9f876d8794 100644 --- a/packages/frontend/editor-ui/src/components/layouts/ResourcesListLayout.vue +++ b/packages/frontend/editor-ui/src/components/layouts/ResourcesListLayout.vue @@ -16,6 +16,7 @@ import type { BaseTextKey } from '@/plugins/i18n'; import type { Scope } from '@n8n/permissions'; import type { BaseFolderItem, BaseResource, ITag, ResourceParentFolder } from '@/Interface'; import { isSharedResource, isResourceSortableByDate } from '@/utils/typeGuards'; +import { useN8nLocalStorage } from '@/composables/useN8nLocalStorage'; type ResourceKeyType = 'credentials' | 'workflows' | 'variables' | 'folders'; @@ -63,12 +64,19 @@ export type BaseFilters = { [key: string]: boolean | string | string[]; }; +export type SortingAndPaginationUpdates = { + page?: number; + pageSize?: number; + sort?: string; +}; + const route = useRoute(); const router = useRouter(); const i18n = useI18n(); const { callDebounced } = useDebounce(); const usersStore = useUsersStore(); const telemetry = useTelemetry(); +const n8nLocalStorage = useN8nLocalStorage(); const props = withDefaults( defineProps<{ @@ -126,12 +134,15 @@ const rowsPerPage = ref(props.customPageSize); const resettingFilters = ref(false); const search = ref(null); +// Preferred sorting and page size +// These refs store the values that are set by the user and preserved in local storage +const preferredPageSize = ref(props.customPageSize); +const preferredSort = ref(props.sortOptions[0]); + const emit = defineEmits<{ 'update:filters': [value: BaseFilters]; 'click:add': [event: Event]; - 'update:current-page': [page: number]; - 'update:page-size': [pageSize: number]; - sort: [value: string]; + 'update:pagination-and-sort': [value: SortingAndPaginationUpdates]; 'update:search': [value: string]; }>(); @@ -286,24 +297,18 @@ watch( }, ); -watch( - () => sortBy.value, - (newValue) => { - emit('sort', newValue); - sendSortingTelemetry(); - }, -); - watch( () => route?.params?.projectId, async () => { await resetFilters(); + await loadPaginationPreferences(); + await props.initialize(); }, ); // Lifecycle hooks onMounted(async () => { - await loadPaginationFromQueryString(); + await loadPaginationPreferences(); await props.initialize(); await nextTick(); @@ -350,21 +355,33 @@ const hasAppliedFilters = (): boolean => { const setRowsPerPage = async (numberOfRowsPerPage: number) => { rowsPerPage.value = numberOfRowsPerPage; - await savePaginationToQueryString(); - emit('update:page-size', numberOfRowsPerPage); + await savePaginationPreferences(); + emit('update:pagination-and-sort', { + pageSize: numberOfRowsPerPage, + }); }; -const setCurrentPage = async (page: number) => { +const setSorting = async (sort: string, persistUpdate = true) => { + sortBy.value = sort; + if (persistUpdate) { + await savePaginationPreferences(); + } + emit('update:pagination-and-sort', { + sort, + }); + sendSortingTelemetry(); +}; + +const setCurrentPage = async (page: number, persistUpdate = true) => { currentPage.value = page; - await savePaginationToQueryString(); - emit('update:current-page', page); + if (persistUpdate) { + await savePaginationPreferences(); + } + emit('update:pagination-and-sort', { + page, + }); }; -defineExpose({ - currentPage, - setCurrentPage, -}); - const sendFiltersTelemetry = (source: string) => { // Prevent sending multiple telemetry events when resetting filters // Timeout is required to wait for search debounce to be over @@ -409,7 +426,7 @@ const resetFilters = async () => { }); // Reset the current page - await setCurrentPage(1); + await setCurrentPage(1, false); resettingFilters.value = true; hasFilters.value = false; @@ -452,7 +469,10 @@ const findNearestPageSize = (size: number): number => { ); }; -const savePaginationToQueryString = async () => { +/** + * Saves the current pagination preferences to local storage and updates the URL query parameters. + */ +const savePaginationPreferences = async () => { // For now, only available for paginated lists if (props.type !== 'list-paginated') { return; @@ -466,38 +486,95 @@ const savePaginationToQueryString = async () => { delete currentQuery.page; } - if (rowsPerPage.value !== props.customPageSize) { + // Only update sort & page size if they are different from the default values + // otherwise, remove them from the query + if (rowsPerPage.value !== preferredPageSize.value) { currentQuery.pageSize = rowsPerPage.value.toString(); + preferredPageSize.value = rowsPerPage.value; } else { delete currentQuery.pageSize; } + if (sortBy.value !== preferredSort.value) { + currentQuery.sort = sortBy.value; + preferredSort.value = sortBy.value; + } else { + delete currentQuery.sort; + } + + n8nLocalStorage.saveProjectPreferencesToLocalStorage( + (route.params.projectId as string) ?? '', + 'workflows', + { + sort: sortBy.value, + pageSize: rowsPerPage.value, + }, + ); + await router.replace({ query: Object.keys(currentQuery).length ? currentQuery : undefined, }); }; -const loadPaginationFromQueryString = async () => { +/** + * Loads the pagination preferences from local storage or URL query parameter + * Current page is only saved in the URL query parameters + * Page size and sort are saved both in local storage and URL query parameters, with query parameters taking precedence + */ +const loadPaginationPreferences = async () => { // For now, only available for paginated lists if (props.type !== 'list-paginated') { return; } - const query = router.currentRoute.value.query; + const query = route.query; + // For now, only load workflow list preferences from local storage + const localStorageValues = n8nLocalStorage.loadProjectPreferencesFromLocalStorage( + (route.params.projectId as string) ?? '', + 'workflows', + ); + + const emitPayload: SortingAndPaginationUpdates = {}; if (query.page) { - await setCurrentPage(parseInt(query.page as string, 10)); + const newPage = parseInt(query.page as string, 10); + if (newPage > 1) { + currentPage.value = newPage; + emitPayload.page = newPage; + } } - if (query.pageSize) { - const parsedSize = parseInt(query.pageSize as string, 10); + if (query.pageSize ?? localStorageValues.pageSize) { + const parsedSize = parseInt( + (query.pageSize as string) || String(localStorageValues.pageSize), + 10, + ); // Round to the nearest available page size, this will prevent users from passing arbitrary values - await setRowsPerPage(findNearestPageSize(parsedSize)); + const newPageSize = findNearestPageSize(parsedSize); + rowsPerPage.value = newPageSize; + emitPayload.pageSize = newPageSize; + preferredPageSize.value = newPageSize; + } else { + rowsPerPage.value = props.customPageSize; + emitPayload.pageSize = props.customPageSize; } if (query.sort) { - sortBy.value = query.sort as string; + // Update the sortBy value and emit the event based on the query parameter + sortBy.value = emitPayload.sort = preferredSort.value = query.sort as string; + } else if (localStorageValues.sort) { + await setSorting(localStorageValues.sort, false); + emitPayload.sort = localStorageValues.sort; + preferredSort.value = localStorageValues.sort; + } else { + sortBy.value = props.sortOptions[0]; } + emit('update:pagination-and-sort', emitPayload); }; + +defineExpose({ + currentPage, + setCurrentPage, +});
- + { -
+
diff --git a/packages/frontend/editor-ui/src/composables/useN8nLocalStorage.test.ts b/packages/frontend/editor-ui/src/composables/useN8nLocalStorage.test.ts new file mode 100644 index 0000000000..ca410c0181 --- /dev/null +++ b/packages/frontend/editor-ui/src/composables/useN8nLocalStorage.test.ts @@ -0,0 +1,181 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { useN8nLocalStorage } from './useN8nLocalStorage'; + +const mockOverview = { + isOverviewSubPage: false, +}; + +// Create a shared storage object that persists between calls +let mockLocalStorageValue: Record = {}; + +vi.mock('@/composables/useOverview', () => ({ + useOverview: vi.fn(() => mockOverview), +})); + +vi.mock('@vueuse/core', () => ({ + useLocalStorage: vi.fn((_key, defaultValue) => { + // Only initialize with default value if the mock is empty + if (Object.keys(mockLocalStorageValue).length === 0) { + Object.assign(mockLocalStorageValue, structuredClone(defaultValue)); + } + + return { + value: mockLocalStorageValue, + }; + }), +})); + +describe('useN8nLocalStorage', () => { + beforeEach(() => { + vi.clearAllMocks(); + mockOverview.isOverviewSubPage = false; + mockLocalStorageValue = {}; + }); + + describe('getProjectKey', () => { + it('returns the projectId when not on overview page', () => { + const { getProjectKey } = useN8nLocalStorage(); + const projectId = 'test-project'; + + expect(getProjectKey(projectId)).toBe(projectId); + }); + + it('returns "home" when on overview page', () => { + // Override the mock for this specific test + mockOverview.isOverviewSubPage = true; + + const { getProjectKey } = useN8nLocalStorage(); + const projectId = 'test-project'; + + expect(getProjectKey(projectId)).toBe('home'); + + // Reset for subsequent tests + mockOverview.isOverviewSubPage = false; + }); + + it('returns the provided projectId when it is an empty string', () => { + const { getProjectKey } = useN8nLocalStorage(); + const projectId = ''; + + expect(getProjectKey(projectId)).toBe(''); + }); + + it('returns undefined when projectId is undefined', () => { + const { getProjectKey } = useN8nLocalStorage(); + + expect(getProjectKey(undefined)).toBeUndefined(); + }); + }); + + describe('saveProjectPreferencesToLocalStorage', () => { + it('saves new preferences to localStorage', () => { + const { saveProjectPreferencesToLocalStorage } = useN8nLocalStorage(); + const projectId = 'test-project'; + const tabKey = 'workflows'; + const preferences = { sort: 'name', pageSize: 25 }; + + saveProjectPreferencesToLocalStorage(projectId, tabKey, preferences); + + expect(mockLocalStorageValue).toEqual({ + 'test-project': { + workflows: { sort: 'name', pageSize: 25 }, + }, + }); + }); + + it('merges new preferences with existing ones', () => { + const { saveProjectPreferencesToLocalStorage } = useN8nLocalStorage(); + const projectId = 'test-project'; + const tabKey = 'workflows'; + + // First save + saveProjectPreferencesToLocalStorage(projectId, tabKey, { sort: 'name' }); + // Second save with different preference + saveProjectPreferencesToLocalStorage(projectId, tabKey, { pageSize: 25 }); + + expect(mockLocalStorageValue).toEqual({ + 'test-project': { + workflows: { sort: 'name', pageSize: 25 }, + }, + }); + }); + + it('does nothing when projectKey is falsy', () => { + mockOverview.isOverviewSubPage = false; + + const { saveProjectPreferencesToLocalStorage } = useN8nLocalStorage(); + const projectId = ''; // This will result in a falsy projectKey + const tabKey = 'workflows'; + const preferences = { sort: 'name', pageSize: 25 }; + + saveProjectPreferencesToLocalStorage(projectId, tabKey, preferences); + + expect(mockLocalStorageValue).toEqual({}); + }); + + it('supports saving credentials tab preferences', () => { + const { saveProjectPreferencesToLocalStorage } = useN8nLocalStorage(); + const projectId = 'test-project'; + const tabKey = 'credentials'; + const preferences = { sort: 'type', pageSize: 50 }; + + saveProjectPreferencesToLocalStorage(projectId, tabKey, preferences); + + expect(mockLocalStorageValue).toEqual({ + 'test-project': { + credentials: { sort: 'type', pageSize: 50 }, + }, + }); + }); + }); + + describe('loadProjectPreferencesFromLocalStorage', () => { + it('loads preferences from localStorage', () => { + mockLocalStorageValue['test-project'] = { + workflows: { sort: 'name', pageSize: 25 }, + }; + + const { loadProjectPreferencesFromLocalStorage } = useN8nLocalStorage(); + const projectId = 'test-project'; + const tabKey = 'workflows'; + + const result = loadProjectPreferencesFromLocalStorage(projectId, tabKey); + + expect(result).toEqual({ sort: 'name', pageSize: 25 }); + }); + + it('returns empty object when preferences do not exist', () => { + const { loadProjectPreferencesFromLocalStorage } = useN8nLocalStorage(); + const projectId = 'non-existent-project'; + const tabKey = 'workflows'; + + const result = loadProjectPreferencesFromLocalStorage(projectId, tabKey); + + expect(result).toEqual({}); + }); + + it('returns empty object when projectKey is falsy', () => { + const { loadProjectPreferencesFromLocalStorage } = useN8nLocalStorage(); + const projectId = ''; // This will result in a falsy projectKey + const tabKey = 'workflows'; + + const result = loadProjectPreferencesFromLocalStorage(projectId, tabKey); + + expect(result).toEqual({}); + }); + + it('returns empty object when tab does not exist', () => { + mockLocalStorageValue['test-project'] = { + workflows: { sort: 'name', pageSize: 25 }, + }; + + const { loadProjectPreferencesFromLocalStorage } = useN8nLocalStorage(); + const projectId = 'test-project'; + const tabKey = 'credentials'; + + const result = loadProjectPreferencesFromLocalStorage(projectId, tabKey); + + expect(result).toEqual({}); + }); + }); +}); diff --git a/packages/frontend/editor-ui/src/composables/useN8nLocalStorage.ts b/packages/frontend/editor-ui/src/composables/useN8nLocalStorage.ts new file mode 100644 index 0000000000..1759003af0 --- /dev/null +++ b/packages/frontend/editor-ui/src/composables/useN8nLocalStorage.ts @@ -0,0 +1,78 @@ +import { useOverview } from '@/composables/useOverview'; +import { LOCAL_STORAGE_WORKFLOW_LIST_PREFERENCES_KEY } from '@/constants'; +import { useLocalStorage } from '@vueuse/core'; + +// Workflow list user preferences +type TabSettings = { + sort?: string; + pageSize?: number; +}; + +// We are currently only saving workflow tab settings +// but we are keeping the credentials tab settings here for future use +export type WorkflowListPreferences = { + [projectId: string]: { + [tabName in 'workflows' | 'credentials']: TabSettings; + }; +}; + +/** + * Simple n8n wrapper around vueuse's useLocalStorage. + * Provides util functions to read and write n8n values to local storage. + * Currently only used for workflow list user preferences. + */ +export function useN8nLocalStorage() { + const overview = useOverview(); + + const getProjectKey = (projectId?: string) => { + return overview.isOverviewSubPage ? 'home' : projectId; + }; + + const saveProjectPreferencesToLocalStorage = ( + projectId: string, + tabKey: 'workflows' | 'credentials', + preferences: TabSettings, + ) => { + const projectKey = getProjectKey(projectId); + if (!projectKey) { + return; + } + + const localStorage = useLocalStorage>( + LOCAL_STORAGE_WORKFLOW_LIST_PREFERENCES_KEY, + {}, + ); + + if (!localStorage.value[projectKey]) { + localStorage.value[projectKey] = {}; + } + + localStorage.value[projectKey][tabKey] = { + ...localStorage.value[projectKey][tabKey], + ...preferences, + }; + }; + + const loadProjectPreferencesFromLocalStorage = ( + projectId: string, + tabKey: 'workflows' | 'credentials', + ) => { + const projectKey = getProjectKey(projectId); + if (!projectKey) { + return {}; + } + const localStorage = useLocalStorage>( + LOCAL_STORAGE_WORKFLOW_LIST_PREFERENCES_KEY, + {}, + ); + const projectPreferences: TabSettings = + (localStorage.value[projectKey]?.[tabKey] as TabSettings) || {}; + return projectPreferences; + }; + + return { + saveProjectPreferencesToLocalStorage, + loadProjectPreferencesFromLocalStorage, + getProjectKey, + }; +} diff --git a/packages/frontend/editor-ui/src/constants.ts b/packages/frontend/editor-ui/src/constants.ts index 6f6d294ecf..48d8c9f28d 100644 --- a/packages/frontend/editor-ui/src/constants.ts +++ b/packages/frontend/editor-ui/src/constants.ts @@ -480,6 +480,7 @@ export const LOCAL_STORAGE_HIDE_GITHUB_STAR_BUTTON = 'N8N_HIDE_HIDE_GITHUB_STAR_ export const LOCAL_STORAGE_NDV_INPUT_PANEL_DISPLAY_MODE = 'N8N_NDV_INPUT_PANEL_DISPLAY_MODE'; export const LOCAL_STORAGE_NDV_OUTPUT_PANEL_DISPLAY_MODE = 'N8N_NDV_OUTPUT_PANEL_DISPLAY_MODE'; export const LOCAL_STORAGE_LOGS_PANEL_OPEN = 'N8N_LOGS_PANEL_OPEN'; +export const LOCAL_STORAGE_WORKFLOW_LIST_PREFERENCES_KEY = 'N8N_WORKFLOWS_LIST_PREFERENCES'; export const BASE_NODE_SURVEY_URL = 'https://n8n-community.typeform.com/to/BvmzxqYv#nodename='; export const COMMUNITY_PLUS_DOCS_URL = 'https://docs.n8n.io/hosting/community-edition-features/#registered-community-edition'; diff --git a/packages/frontend/editor-ui/src/views/WorkflowsView.vue b/packages/frontend/editor-ui/src/views/WorkflowsView.vue index 06d24ab6f7..da8f7e664c 100644 --- a/packages/frontend/editor-ui/src/views/WorkflowsView.vue +++ b/packages/frontend/editor-ui/src/views/WorkflowsView.vue @@ -5,6 +5,7 @@ import type { BaseFilters, FolderResource, Resource, + SortingAndPaginationUpdates, WorkflowResource, } from '@/components/layouts/ResourcesListLayout.vue'; import ResourcesListLayout from '@/components/layouts/ResourcesListLayout.vue'; @@ -112,7 +113,9 @@ const documentTitle = useDocumentTitle(); const { callDebounced } = useDebounce(); const overview = useOverview(); -const loading = ref(false); +// We render component in a loading state until initialization is done +// This will prevent any additional workflow fetches while initializing +const loading = ref(true); const breadcrumbsLoading = ref(false); const filters = ref({ search: '', @@ -363,7 +366,7 @@ const showRegisteredCommunityCTA = computed( watch( () => route.params?.projectId, async () => { - await initialize(); + loading.value = true; }, ); @@ -542,18 +545,6 @@ const fetchWorkflows = async () => { }; // Filter and sort methods - -const onSortUpdated = async (sort: string) => { - currentSort.value = - WORKFLOWS_SORT_MAP[sort as keyof typeof WORKFLOWS_SORT_MAP] ?? 'updatedAt:desc'; - if (currentSort.value !== 'updatedAt:desc') { - void router.replace({ query: { ...route.query, sort } }); - } else { - void router.replace({ query: { ...route.query, sort: undefined } }); - } - await fetchWorkflows(); -}; - const onFiltersUpdated = async () => { currentPage.value = 1; saveFiltersOnQueryString(); @@ -571,14 +562,23 @@ const onSearchUpdated = async (search: string) => { } }; -const setCurrentPage = async (page: number) => { - currentPage.value = page; - await callDebounced(fetchWorkflows, { debounceTime: FILTERS_DEBOUNCE_TIME, trailing: true }); -}; - -const setPageSize = async (size: number) => { - pageSize.value = size; - await callDebounced(fetchWorkflows, { debounceTime: FILTERS_DEBOUNCE_TIME, trailing: true }); +const setPaginationAndSort = async (payload: SortingAndPaginationUpdates) => { + if (payload.page) { + currentPage.value = payload.page; + } + if (payload.pageSize) { + pageSize.value = payload.pageSize; + } + if (payload.sort) { + currentSort.value = + WORKFLOWS_SORT_MAP[payload.sort as keyof typeof WORKFLOWS_SORT_MAP] ?? 'updatedAt:desc'; + } + // Don't fetch workflows if we are loading + // This will prevent unnecessary API calls when changing sort and pagination from url/local storage + // when switching between projects + if (!loading.value) { + await callDebounced(fetchWorkflows, { debounceTime: FILTERS_DEBOUNCE_TIME, trailing: true }); + } }; const onClickTag = async (tagId: string) => { @@ -1340,10 +1340,8 @@ const onNameSubmit = async ({ :has-empty-state="foldersStore.totalWorkflowCount === 0 && !currentFolderId" @click:add="addWorkflow" @update:search="onSearchUpdated" - @update:current-page="setCurrentPage" - @update:page-size="setPageSize" @update:filters="onFiltersUpdated" - @sort="onSortUpdated" + @update:pagination-and-sort="setPaginationAndSort" @mouseleave="folderHelpers.resetDropTarget" >