feat(editor): Preserve workflow list sort & page size preferences (#15101)

This commit is contained in:
Milorad FIlipović
2025-05-06 16:14:06 +02:00
committed by GitHub
parent c76245519c
commit cf03a28774
8 changed files with 520 additions and 69 deletions

View File

@@ -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', () => {

View File

@@ -240,10 +240,10 @@ onClickOutside(
</N8nOption>
<N8nOption v-else-if="options.length === 0" value="message" disabled>
<span v-if="createEnabled">{{ i18n.baseText('tagsDropdown.typeToCreateATag') }}</span>
<span v-if="allTags.length > 0">{{
<span v-else-if="allTags.length > 0">{{
i18n.baseText('tagsDropdown.noMatchingTagsExist')
}}</span>
<span v-else-if="filter">{{ i18n.baseText('tagsDropdown.noTagsExist') }}</span>
<span v-else>{{ i18n.baseText('tagsDropdown.noTagsExist') }}</span>
</N8nOption>
<N8nOption

View File

@@ -1,8 +1,50 @@
import { setActivePinia } from 'pinia';
import { createTestingPinia } from '@pinia/testing';
import { createComponentRenderer } from '@/__tests__/render';
import ResourcesListLayout from '@/components/layouts/ResourcesListLayout.vue';
import ResourcesListLayout, { type Resource } from '@/components/layouts/ResourcesListLayout.vue';
import type router from 'vue-router';
import type { ProjectSharingData } from 'n8n-workflow';
import { waitAllPromises } from '@/__tests__/utils';
const TEST_HOME_PROJECT: ProjectSharingData = vi.hoisted(() => ({
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<typeof router>();
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]);
});
});

View File

@@ -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<number>(props.customPageSize);
const resettingFilters = ref(false);
const search = ref<HTMLElement | null>(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<number>(props.customPageSize);
const preferredSort = ref<string>(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,
});
</script>
<template>
@@ -556,7 +633,12 @@ const loadPaginationFromQueryString = async () => {
</template>
</n8n-input>
<div :class="$style['sort-and-filter']">
<n8n-select v-model="sortBy" size="small" data-test-id="resources-list-sort">
<n8n-select
v-model="sortBy"
size="small"
data-test-id="resources-list-sort"
@change="setSorting(sortBy)"
>
<n8n-option
v-for="sortOption in sortOptions"
:key="sortOption"
@@ -624,7 +706,11 @@ const loadPaginationFromQueryString = async () => {
</template>
</n8n-recycle-scroller>
<!-- PAGINATED LIST -->
<div v-else-if="type === 'list-paginated'" :class="$style.paginatedListWrapper">
<div
v-else-if="type === 'list-paginated'"
:class="$style.paginatedListWrapper"
data-test-id="paginated-list"
>
<div :class="$style.listItems">
<div v-for="(item, index) in resources" :key="index" :class="$style.listItem">
<slot name="item" :item="item" :index="index">

View File

@@ -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<string, unknown> = {};
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({});
});
});
});

View File

@@ -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<Record<string, WorkflowListPreferences>>(
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<Record<string, WorkflowListPreferences>>(
LOCAL_STORAGE_WORKFLOW_LIST_PREFERENCES_KEY,
{},
);
const projectPreferences: TabSettings =
(localStorage.value[projectKey]?.[tabKey] as TabSettings) || {};
return projectPreferences;
};
return {
saveProjectPreferencesToLocalStorage,
loadProjectPreferencesFromLocalStorage,
getProjectKey,
};
}

View File

@@ -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';

View File

@@ -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<Filters>({
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"
>
<template #header>