mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 18:12:04 +00:00
feat: Improve workflow list performance using RecycleScroller and on-demand sharing data loading (#5181)
* feat(editor): Load workflow sharedWith info only when opening share modal (#5125) * feat(editor): load workflow sharedWith info only when opening share modal * fix(editor): update workflow share modal loading state at the end of initialize fn * feat: initial recycle scroller commit * feat: prepare recycle scroller for dynamic item sizes (no-changelog) * feat: add recycle scroller with variable size support and caching * feat: integrated recycle scroller with existing resources list * feat: improve recycle scroller performance * fix: fix recycle-scroller storybook * fix: update recycle-scroller styles to fix scrollbar size * chore: undo vite config changes * chore: undo installed packages * chore: remove commented code * chore: remove vue-virtual-scroller code. * feat: update size cache updating mechanism * chore: remove console.log * fix: adjust code for e2e tests * fix: fix linting issues
This commit is contained in:
@@ -0,0 +1,274 @@
|
||||
<script lang="ts">
|
||||
/* eslint-disable @typescript-eslint/no-use-before-define */
|
||||
|
||||
import {
|
||||
computed,
|
||||
defineComponent,
|
||||
onMounted,
|
||||
onBeforeMount,
|
||||
ref,
|
||||
PropType,
|
||||
nextTick,
|
||||
watch,
|
||||
} from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'n8n-recycle-scroller',
|
||||
props: {
|
||||
itemSize: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
items: {
|
||||
type: Array as PropType<Array<Record<string, string>>>,
|
||||
required: true,
|
||||
},
|
||||
itemKey: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
offset: {
|
||||
type: Number,
|
||||
default: 2,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const wrapperRef = ref<HTMLElement | null>(null);
|
||||
const scrollerRef = ref<HTMLElement | null>(null);
|
||||
const itemsRef = ref<HTMLElement | null>(null);
|
||||
const itemRefs = ref<Record<string, HTMLElement | null>>({});
|
||||
|
||||
const scrollTop = ref(0);
|
||||
const wrapperHeight = ref(0);
|
||||
const windowHeight = ref(0);
|
||||
|
||||
const itemCount = computed(() => props.items.length);
|
||||
|
||||
/**
|
||||
* Cache
|
||||
*/
|
||||
|
||||
const itemSizeCache = ref<Record<string, number>>({});
|
||||
const itemPositionCache = computed(() => {
|
||||
return props.items.reduce<Record<string, number>>((acc, item, index) => {
|
||||
const key = item[props.itemKey];
|
||||
const prevItem = props.items[index - 1];
|
||||
const prevItemPosition = prevItem ? acc[prevItem[props.itemKey]] : 0;
|
||||
const prevItemSize = prevItem ? itemSizeCache.value[prevItem[props.itemKey]] : 0;
|
||||
|
||||
acc[key] = prevItemPosition + prevItemSize;
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
});
|
||||
|
||||
/**
|
||||
* Indexes
|
||||
*/
|
||||
|
||||
const startIndex = computed(() => {
|
||||
const foundIndex =
|
||||
props.items.findIndex((item) => {
|
||||
const itemPosition = itemPositionCache.value[item[props.itemKey]];
|
||||
|
||||
return itemPosition >= scrollTop.value;
|
||||
}) - 1;
|
||||
const index = foundIndex - props.offset;
|
||||
|
||||
return index < 0 ? 0 : index;
|
||||
});
|
||||
|
||||
const endIndex = computed(() => {
|
||||
const foundIndex = props.items.findIndex((item) => {
|
||||
const itemPosition = itemPositionCache.value[item[props.itemKey]];
|
||||
const itemSize = itemSizeCache.value[item[props.itemKey]];
|
||||
|
||||
return itemPosition + itemSize >= scrollTop.value + wrapperHeight.value;
|
||||
});
|
||||
const index = foundIndex + props.offset;
|
||||
|
||||
return index === -1 ? props.items.length - 1 : index;
|
||||
});
|
||||
|
||||
const visibleItems = computed(() => {
|
||||
return props.items.slice(startIndex.value, endIndex.value + 1);
|
||||
});
|
||||
|
||||
watch(
|
||||
() => visibleItems.value,
|
||||
(currentValue, previousValue) => {
|
||||
const difference = currentValue.filter(
|
||||
(currentItem) =>
|
||||
!previousValue.find(
|
||||
(previousItem) => previousItem[props.itemKey] === currentItem[props.itemKey],
|
||||
),
|
||||
);
|
||||
|
||||
if (difference.length > 0) {
|
||||
updateItemSizeCache(difference);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* Computed sizes and styles
|
||||
*/
|
||||
|
||||
const scrollerHeight = computed(() => {
|
||||
const lastItem = props.items[props.items.length - 1];
|
||||
const lastItemPosition = lastItem ? itemPositionCache.value[lastItem[props.itemKey]] : 0;
|
||||
const lastItemSize = lastItem ? itemSizeCache.value[lastItem[props.itemKey]] : props.itemSize;
|
||||
|
||||
return lastItemPosition + lastItemSize;
|
||||
});
|
||||
|
||||
const scrollerStyles = computed(() => ({
|
||||
height: `${scrollerHeight.value}px`,
|
||||
}));
|
||||
|
||||
const itemsStyles = computed(() => {
|
||||
const offset = itemPositionCache.value[props.items[startIndex.value][props.itemKey]];
|
||||
|
||||
return {
|
||||
transform: `translateY(${offset}px)`,
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* Lifecycle hooks
|
||||
*/
|
||||
|
||||
onBeforeMount(() => {
|
||||
initializeItemSizeCache();
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
if (wrapperRef.value) {
|
||||
wrapperRef.value.addEventListener('scroll', onScroll);
|
||||
updateItemSizeCache(visibleItems.value);
|
||||
}
|
||||
|
||||
window.addEventListener('resize', onWindowResize);
|
||||
onWindowResize();
|
||||
});
|
||||
|
||||
/**
|
||||
* Event handlers
|
||||
*/
|
||||
|
||||
function initializeItemSizeCache() {
|
||||
props.items.forEach((item) => {
|
||||
itemSizeCache.value = {
|
||||
...itemSizeCache.value,
|
||||
[item[props.itemKey]]: props.itemSize,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function updateItemSizeCache(items: Array<Record<string, string>>) {
|
||||
for (const item of items) {
|
||||
onUpdateItemSize(item);
|
||||
}
|
||||
}
|
||||
|
||||
function onUpdateItemSize(item: { [key: string]: string }) {
|
||||
nextTick(() => {
|
||||
const itemId = item[props.itemKey];
|
||||
const itemRef = itemRefs.value[itemId];
|
||||
const previousSize = itemSizeCache.value[itemId];
|
||||
const size = itemRef ? itemRef.offsetHeight : props.itemSize;
|
||||
const difference = size - previousSize;
|
||||
|
||||
itemSizeCache.value = {
|
||||
...itemSizeCache.value,
|
||||
[item[props.itemKey]]: size,
|
||||
};
|
||||
|
||||
if (wrapperRef.value && scrollTop.value) {
|
||||
wrapperRef.value.scrollTop = wrapperRef.value.scrollTop + difference;
|
||||
scrollTop.value = wrapperRef.value.scrollTop;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function onWindowResize() {
|
||||
if (wrapperRef.value) {
|
||||
wrapperHeight.value = wrapperRef.value.offsetHeight;
|
||||
nextTick(() => {
|
||||
updateItemSizeCache(visibleItems.value);
|
||||
});
|
||||
}
|
||||
|
||||
windowHeight.value = window.innerHeight;
|
||||
}
|
||||
|
||||
function onScroll() {
|
||||
if (!wrapperRef.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
scrollTop.value = wrapperRef.value.scrollTop;
|
||||
}
|
||||
|
||||
return {
|
||||
startIndex,
|
||||
endIndex,
|
||||
itemCount,
|
||||
itemSizeCache,
|
||||
itemPositionCache,
|
||||
itemsVisible: visibleItems,
|
||||
itemsStyles,
|
||||
scrollerStyles,
|
||||
scrollerScrollTop: scrollTop,
|
||||
scrollerRef,
|
||||
wrapperRef,
|
||||
itemsRef,
|
||||
itemRefs,
|
||||
onUpdateItemSize,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="recycle-scroller-wrapper" ref="wrapperRef">
|
||||
<div class="recycle-scroller" :style="scrollerStyles" ref="scrollerRef">
|
||||
<div class="recycle-scroller-items-wrapper" :style="itemsStyles" ref="itemsRef">
|
||||
<div
|
||||
v-for="item in itemsVisible"
|
||||
:key="item[itemKey]"
|
||||
class="recycle-scroller-item"
|
||||
:ref="(element) => (itemRefs[item[itemKey]] = element)"
|
||||
>
|
||||
<slot :item="item" :updateItemSize="onUpdateItemSize" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.recycle-scroller-wrapper {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
overflow: auto;
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.recycle-scroller {
|
||||
width: 100%;
|
||||
display: block;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.recycle-scroller-items-wrapper {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.recycle-scroller-item {
|
||||
display: flex;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user