mirror of
https://github.com/n8n-io/n8n.git
synced 2026-05-28 23:37:00 +02:00
fix(Slack Node): Enable pagination for RLC - listChannels (#21434)
This commit is contained in:
parent
9299a7ee00
commit
bd04340f4f
|
|
@ -1,5 +1,5 @@
|
|||
import { createComponentRenderer } from '@/__tests__/render';
|
||||
import { fireEvent, screen } from '@testing-library/vue';
|
||||
import { fireEvent, screen, waitFor } from '@testing-library/vue';
|
||||
import { vi } from 'vitest';
|
||||
import ResourceLocatorDropdown from './ResourceLocatorDropdown.vue';
|
||||
import type { INodeParameterResourceLocator } from 'n8n-workflow';
|
||||
|
|
@ -251,5 +251,208 @@ describe('ResourceLocatorDropdown', () => {
|
|||
// Should not emit loadMore when hasMore is false
|
||||
expect(wrapper.emitted().loadMore).toBeUndefined();
|
||||
});
|
||||
|
||||
describe('auto-loading when list is not scrollable', () => {
|
||||
it('should emit loadMore when list is not scrollable but hasMore is true', async () => {
|
||||
vi.useFakeTimers();
|
||||
|
||||
const fewResources: IResourceLocatorResultExpanded[] = [
|
||||
{
|
||||
name: 'Workflow 1',
|
||||
value: 'workflow-1',
|
||||
url: '/workflow/workflow-1',
|
||||
},
|
||||
{
|
||||
name: 'Workflow 2',
|
||||
value: 'workflow-2',
|
||||
url: '/workflow/workflow-2',
|
||||
},
|
||||
];
|
||||
|
||||
const props = {
|
||||
show: true,
|
||||
resources: fewResources,
|
||||
hasMore: false,
|
||||
loading: false,
|
||||
filter: '',
|
||||
};
|
||||
|
||||
const wrapper = renderComponent({ props });
|
||||
|
||||
const resultsContainer = await waitFor(
|
||||
() =>
|
||||
wrapper.container
|
||||
.querySelector('[data-test-id="resource-locator-dropdown"]')
|
||||
?.querySelector('[class*="container"]') as HTMLDivElement,
|
||||
);
|
||||
|
||||
Object.defineProperty(resultsContainer, 'scrollHeight', {
|
||||
value: 100,
|
||||
writable: true,
|
||||
});
|
||||
Object.defineProperty(resultsContainer, 'clientHeight', {
|
||||
value: 100,
|
||||
writable: true,
|
||||
});
|
||||
|
||||
await wrapper.rerender({ ...props, hasMore: true });
|
||||
|
||||
await vi.advanceTimersByTimeAsync(500);
|
||||
expect(wrapper.emitted().loadMore).toBeDefined();
|
||||
expect(wrapper.emitted().loadMore).toHaveLength(1);
|
||||
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it('should not emit loadMore when list is scrollable even if hasMore is true', async () => {
|
||||
vi.useFakeTimers();
|
||||
|
||||
const fewResources: IResourceLocatorResultExpanded[] = [
|
||||
{
|
||||
name: 'Workflow 1',
|
||||
value: 'workflow-1',
|
||||
url: '/workflow/workflow-1',
|
||||
},
|
||||
{
|
||||
name: 'Workflow 2',
|
||||
value: 'workflow-2',
|
||||
url: '/workflow/workflow-2',
|
||||
},
|
||||
];
|
||||
|
||||
const props = {
|
||||
show: true,
|
||||
resources: fewResources,
|
||||
hasMore: false,
|
||||
loading: false,
|
||||
filter: '',
|
||||
};
|
||||
|
||||
const wrapper = renderComponent({ props });
|
||||
|
||||
const resultsContainer = await waitFor(
|
||||
() =>
|
||||
wrapper.container
|
||||
.querySelector('[data-test-id="resource-locator-dropdown"]')
|
||||
?.querySelector('[class*="container"]') as HTMLDivElement,
|
||||
);
|
||||
|
||||
Object.defineProperty(resultsContainer, 'scrollHeight', {
|
||||
value: 150,
|
||||
writable: true,
|
||||
});
|
||||
Object.defineProperty(resultsContainer, 'clientHeight', {
|
||||
value: 100,
|
||||
writable: true,
|
||||
});
|
||||
|
||||
await wrapper.rerender({ ...props, hasMore: true });
|
||||
|
||||
await vi.advanceTimersByTimeAsync(500);
|
||||
|
||||
expect(wrapper.emitted().loadMore).toBeUndefined();
|
||||
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it('should not emit loadMore when filter is present', async () => {
|
||||
vi.useFakeTimers();
|
||||
|
||||
const fewResources: IResourceLocatorResultExpanded[] = [
|
||||
{
|
||||
name: 'Workflow 1',
|
||||
value: 'workflow-1',
|
||||
url: '/workflow/workflow-1',
|
||||
},
|
||||
{
|
||||
name: 'Workflow 2',
|
||||
value: 'workflow-2',
|
||||
url: '/workflow/workflow-2',
|
||||
},
|
||||
];
|
||||
|
||||
const props = {
|
||||
show: true,
|
||||
resources: fewResources,
|
||||
hasMore: false,
|
||||
loading: false,
|
||||
filter: 'search',
|
||||
};
|
||||
|
||||
const wrapper = renderComponent({ props });
|
||||
|
||||
const resultsContainer = await waitFor(
|
||||
() =>
|
||||
wrapper.container
|
||||
.querySelector('[data-test-id="resource-locator-dropdown"]')
|
||||
?.querySelector('[class*="container"]') as HTMLDivElement,
|
||||
);
|
||||
|
||||
Object.defineProperty(resultsContainer, 'scrollHeight', {
|
||||
value: 100,
|
||||
writable: true,
|
||||
});
|
||||
Object.defineProperty(resultsContainer, 'clientHeight', {
|
||||
value: 100,
|
||||
writable: true,
|
||||
});
|
||||
|
||||
await wrapper.rerender({ ...props, hasMore: true });
|
||||
|
||||
await vi.advanceTimersByTimeAsync(500);
|
||||
expect(wrapper.emitted().loadMore).toBeUndefined();
|
||||
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it('should not emit loadMore when loading', async () => {
|
||||
vi.useFakeTimers();
|
||||
|
||||
const fewResources: IResourceLocatorResultExpanded[] = [
|
||||
{
|
||||
name: 'Workflow 1',
|
||||
value: 'workflow-1',
|
||||
url: '/workflow/workflow-1',
|
||||
},
|
||||
{
|
||||
name: 'Workflow 2',
|
||||
value: 'workflow-2',
|
||||
url: '/workflow/workflow-2',
|
||||
},
|
||||
];
|
||||
|
||||
const props = {
|
||||
show: true,
|
||||
resources: fewResources,
|
||||
hasMore: false,
|
||||
loading: true,
|
||||
};
|
||||
|
||||
const wrapper = renderComponent({ props });
|
||||
|
||||
const resultsContainer = await waitFor(
|
||||
() =>
|
||||
wrapper.container
|
||||
.querySelector('[data-test-id="resource-locator-dropdown"]')
|
||||
?.querySelector('[class*="container"]') as HTMLDivElement,
|
||||
);
|
||||
|
||||
Object.defineProperty(resultsContainer, 'scrollHeight', {
|
||||
value: 100,
|
||||
writable: true,
|
||||
});
|
||||
Object.defineProperty(resultsContainer, 'clientHeight', {
|
||||
value: 100,
|
||||
writable: true,
|
||||
});
|
||||
|
||||
await wrapper.rerender({ ...props, hasMore: true });
|
||||
|
||||
await vi.advanceTimersByTimeAsync(500);
|
||||
expect(wrapper.emitted().loadMore).toBeUndefined();
|
||||
|
||||
vi.useRealTimers();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
<script setup lang="ts">
|
||||
import { useI18n } from '@n8n/i18n';
|
||||
import { useDebounce } from '@/app/composables/useDebounce';
|
||||
import type { IResourceLocatorResultExpanded } from '@/Interface';
|
||||
import { N8nBadge, N8nIcon, N8nInput, N8nLoading, N8nPopover } from '@n8n/design-system';
|
||||
import { useI18n } from '@n8n/i18n';
|
||||
import type { EventBus } from '@n8n/utils/event-bus';
|
||||
import { createEventBus } from '@n8n/utils/event-bus';
|
||||
import type { INodeParameterResourceLocator } from 'n8n-workflow';
|
||||
import { computed, onBeforeUnmount, onMounted, ref, useCssModule, watch } from 'vue';
|
||||
|
||||
import { N8nBadge, N8nIcon, N8nInput, N8nLoading, N8nPopover } from '@n8n/design-system';
|
||||
const SEARCH_BAR_HEIGHT_PX = 40;
|
||||
const SCROLL_MARGIN_PX = 10;
|
||||
|
||||
|
|
@ -47,6 +48,14 @@ const emit = defineEmits<{
|
|||
addResourceClick: [];
|
||||
}>();
|
||||
|
||||
const { debounce } = useDebounce();
|
||||
const debouncedLoadMore = debounce(
|
||||
() => {
|
||||
emit('loadMore');
|
||||
},
|
||||
{ debounceTime: 500 },
|
||||
);
|
||||
|
||||
const i18n = useI18n();
|
||||
const $style = useCssModule();
|
||||
|
||||
|
|
@ -209,7 +218,7 @@ function onResultsEnd() {
|
|||
resultsContainerRef.value.offsetHeight -
|
||||
(resultsContainerRef.value.scrollHeight - resultsContainerRef.value.scrollTop);
|
||||
if (diff > -SCROLL_MARGIN_PX && diff < SCROLL_MARGIN_PX) {
|
||||
emit('loadMore');
|
||||
debouncedLoadMore();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -219,6 +228,23 @@ function isWithinDropdown(element: HTMLElement) {
|
|||
}
|
||||
|
||||
defineExpose({ isWithinDropdown });
|
||||
|
||||
const canLoadMore = computed(() => {
|
||||
return props.hasMore && !props.loading && !props.filter;
|
||||
});
|
||||
|
||||
watch(
|
||||
canLoadMore,
|
||||
(loadMore) => {
|
||||
const isScrollable =
|
||||
!!resultsContainerRef.value &&
|
||||
resultsContainerRef.value?.scrollHeight > resultsContainerRef.value?.clientHeight;
|
||||
if (loadMore && !isScrollable) {
|
||||
debouncedLoadMore();
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
|
|||
|
|
@ -183,16 +183,23 @@ export class SlackV2 implements INodeType {
|
|||
async getChannels(
|
||||
this: ILoadOptionsFunctions,
|
||||
filter?: string,
|
||||
paginationToken?: string,
|
||||
): Promise<INodeListSearchResult> {
|
||||
const qs = { types: 'public_channel,private_channel', limit: 1000 };
|
||||
const channels = (await slackApiRequestAllItems.call(
|
||||
const qs = {
|
||||
types: 'public_channel,private_channel',
|
||||
limit: 1000,
|
||||
cursor: paginationToken,
|
||||
};
|
||||
const { channels, response_metadata } = (await slackApiRequest.call(
|
||||
this,
|
||||
'channels',
|
||||
'GET',
|
||||
'/conversations.list',
|
||||
{},
|
||||
qs,
|
||||
)) as Array<{ id: string; name: string }>;
|
||||
)) as {
|
||||
channels: Array<{ id: string; name: string }>;
|
||||
response_metadata?: { next_cursor?: string };
|
||||
};
|
||||
const results: INodeListSearchItems[] = channels
|
||||
.map((c) => ({
|
||||
name: c.name,
|
||||
|
|
@ -209,7 +216,8 @@ export class SlackV2 implements INodeType {
|
|||
if (a.name.toLowerCase() > b.name.toLowerCase()) return 1;
|
||||
return 0;
|
||||
});
|
||||
return { results };
|
||||
const nextPaginationToken = response_metadata?.next_cursor || undefined;
|
||||
return { results, paginationToken: nextPaginationToken };
|
||||
},
|
||||
async getUsers(this: ILoadOptionsFunctions, filter?: string): Promise<INodeListSearchResult> {
|
||||
const users = (await slackApiRequestAllItems.call(
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user