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:
@@ -28,6 +28,7 @@
|
||||
:truncateAt="3"
|
||||
truncate
|
||||
@click="onClickTag"
|
||||
@expand="onExpandTags"
|
||||
data-test-id="workflow-card-tags"
|
||||
/>
|
||||
</span>
|
||||
@@ -189,6 +190,9 @@ export default mixins(showMessage, restApi).extend({
|
||||
|
||||
this.$emit('click:tag', tagId, event);
|
||||
},
|
||||
onExpandTags() {
|
||||
this.$emit('expand:tags');
|
||||
},
|
||||
async onAction(action: string) {
|
||||
if (action === WORKFLOW_LIST_ITEM_ACTIONS.OPEN) {
|
||||
await this.onClick();
|
||||
|
||||
@@ -154,6 +154,7 @@ import { useWorkflowsStore } from '@/stores/workflows';
|
||||
import { useWorkflowsEEStore } from '@/stores/workflows.ee';
|
||||
import { ITelemetryTrackProperties } from 'n8n-workflow';
|
||||
import { useUsageStore } from '@/stores/usage';
|
||||
import { BaseTextKey } from '@/plugins/i18n';
|
||||
|
||||
export default mixins(showMessage).extend({
|
||||
name: 'workflow-share-modal',
|
||||
@@ -175,7 +176,7 @@ export default mixins(showMessage).extend({
|
||||
|
||||
return {
|
||||
WORKFLOW_SHARE_MODAL_KEY,
|
||||
loading: false,
|
||||
loading: true,
|
||||
modalBus: new Vue(),
|
||||
sharedWith: [...(workflow.sharedWith || [])] as Array<Partial<IUser>>,
|
||||
EnterpriseEditionFeature,
|
||||
@@ -199,8 +200,9 @@ export default mixins(showMessage).extend({
|
||||
modalTitle(): string {
|
||||
return this.$locale.baseText(
|
||||
this.isSharingEnabled
|
||||
? this.uiStore.contextBasedTranslationKeys.workflows.sharing.title
|
||||
: this.uiStore.contextBasedTranslationKeys.workflows.sharing.unavailable.title,
|
||||
? (this.uiStore.contextBasedTranslationKeys.workflows.sharing.title as BaseTextKey)
|
||||
: (this.uiStore.contextBasedTranslationKeys.workflows.sharing.unavailable
|
||||
.title as BaseTextKey),
|
||||
{
|
||||
interpolate: { name: this.workflow.name },
|
||||
},
|
||||
@@ -380,7 +382,7 @@ export default mixins(showMessage).extend({
|
||||
},
|
||||
),
|
||||
this.$locale.baseText('workflows.shareModal.list.delete.confirm.title', {
|
||||
interpolate: { name: user.fullName },
|
||||
interpolate: { name: user.fullName as string },
|
||||
}),
|
||||
null,
|
||||
this.$locale.baseText('workflows.shareModal.list.delete.confirm.confirmButtonText'),
|
||||
@@ -437,18 +439,37 @@ export default mixins(showMessage).extend({
|
||||
});
|
||||
},
|
||||
goToUpgrade() {
|
||||
let linkUrl = this.$locale.baseText(this.uiStore.contextBasedTranslationKeys.upgradeLinkUrl);
|
||||
let linkUrl = this.$locale.baseText(
|
||||
this.uiStore.contextBasedTranslationKeys.upgradeLinkUrl as BaseTextKey,
|
||||
);
|
||||
if (linkUrl.includes('subscription')) {
|
||||
linkUrl = `${this.usageStore.viewPlansUrl}&source=workflow_sharing`;
|
||||
}
|
||||
|
||||
window.open(linkUrl, '_blank');
|
||||
},
|
||||
async initialize() {
|
||||
if (this.isSharingEnabled) {
|
||||
await this.loadUsers();
|
||||
|
||||
if (
|
||||
this.workflow.id !== PLACEHOLDER_EMPTY_WORKFLOW_ID &&
|
||||
!this.workflow.sharedWith?.length // Sharing info already loaded
|
||||
) {
|
||||
await this.workflowsStore.fetchWorkflow(this.workflow.id);
|
||||
}
|
||||
}
|
||||
|
||||
this.loading = false;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
if (this.isSharingEnabled) {
|
||||
this.loadUsers();
|
||||
}
|
||||
this.initialize();
|
||||
},
|
||||
watch: {
|
||||
workflow(workflow) {
|
||||
this.sharedWith = workflow.sharedWith;
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div :class="$style.wrapper">
|
||||
<div :class="$style.list">
|
||||
<div v-if="$slots.header">
|
||||
<div v-if="$slots.header" :class="$style.header">
|
||||
<slot name="header" />
|
||||
</div>
|
||||
<div :class="$style.body">
|
||||
@@ -21,12 +21,16 @@
|
||||
.list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.body {
|
||||
overflow: auto;
|
||||
.header {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.body {
|
||||
overflow: hidden;
|
||||
flex: 1 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -106,56 +106,56 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<slot name="callout"></slot>
|
||||
|
||||
<div v-show="hasFilters" class="mt-xs">
|
||||
<n8n-info-tip :bold="false">
|
||||
{{ $locale.baseText(`${resourceKey}.filters.active`) }}
|
||||
<n8n-link @click="resetFilters" size="small">
|
||||
{{ $locale.baseText(`${resourceKey}.filters.active.reset`) }}
|
||||
</n8n-link>
|
||||
</n8n-info-tip>
|
||||
</div>
|
||||
|
||||
<div class="pb-xs" />
|
||||
</template>
|
||||
|
||||
<slot name="callout"></slot>
|
||||
<n8n-recycle-scroller
|
||||
v-if="filteredAndSortedSubviewResources.length > 0"
|
||||
data-test-id="resources-list"
|
||||
:class="[$style.list, 'list-style-none']"
|
||||
:items="filteredAndSortedSubviewResources"
|
||||
:item-size="itemSize"
|
||||
item-key="id"
|
||||
>
|
||||
<template #default="{ item, updateItemSize }">
|
||||
<slot :data="item" :updateItemSize="updateItemSize" />
|
||||
</template>
|
||||
</n8n-recycle-scroller>
|
||||
|
||||
<div v-show="hasFilters" class="mt-xs">
|
||||
<n8n-info-tip :bold="false">
|
||||
{{ $locale.baseText(`${resourceKey}.filters.active`) }}
|
||||
<n8n-link @click="resetFilters" size="small">
|
||||
{{ $locale.baseText(`${resourceKey}.filters.active.reset`) }}
|
||||
</n8n-link>
|
||||
</n8n-info-tip>
|
||||
</div>
|
||||
<n8n-text color="text-base" size="medium" data-test-id="resources-list-empty" v-else>
|
||||
{{ $locale.baseText(`${resourceKey}.noResults`) }}
|
||||
<template v-if="shouldSwitchToAllSubview">
|
||||
<span v-if="!filters.search">
|
||||
({{ $locale.baseText(`${resourceKey}.noResults.switchToShared.preamble`) }}
|
||||
<n8n-link @click="setOwnerSubview(false)">
|
||||
{{ $locale.baseText(`${resourceKey}.noResults.switchToShared.link`) }} </n8n-link
|
||||
>)
|
||||
</span>
|
||||
|
||||
<div class="mt-xs mb-l">
|
||||
<ul
|
||||
:class="[$style.list, 'list-style-none']"
|
||||
v-if="filteredAndSortedSubviewResources.length > 0"
|
||||
data-test-id="resources-list"
|
||||
>
|
||||
<li
|
||||
v-for="resource in filteredAndSortedSubviewResources"
|
||||
:key="resource.id"
|
||||
class="mb-2xs"
|
||||
data-test-id="resources-list-item"
|
||||
>
|
||||
<slot :data="resource" />
|
||||
</li>
|
||||
</ul>
|
||||
<n8n-text color="text-base" size="medium" data-test-id="resources-list-empty" v-else>
|
||||
{{ $locale.baseText(`${resourceKey}.noResults`) }}
|
||||
<template v-if="shouldSwitchToAllSubview">
|
||||
<span v-if="!filters.search">
|
||||
({{ $locale.baseText(`${resourceKey}.noResults.switchToShared.preamble`) }}
|
||||
<n8n-link @click="setOwnerSubview(false)">{{
|
||||
$locale.baseText(`${resourceKey}.noResults.switchToShared.link`)
|
||||
}}</n8n-link
|
||||
>)
|
||||
</span>
|
||||
<span v-else>
|
||||
({{
|
||||
$locale.baseText(`${resourceKey}.noResults.withSearch.switchToShared.preamble`)
|
||||
}}
|
||||
<n8n-link @click="setOwnerSubview(false)">{{
|
||||
<span v-else>
|
||||
({{
|
||||
$locale.baseText(`${resourceKey}.noResults.withSearch.switchToShared.preamble`)
|
||||
}}
|
||||
<n8n-link @click="setOwnerSubview(false)">
|
||||
{{
|
||||
$locale.baseText(`${resourceKey}.noResults.withSearch.switchToShared.link`)
|
||||
}}</n8n-link
|
||||
>)
|
||||
</span>
|
||||
</template>
|
||||
</n8n-text>
|
||||
</div>
|
||||
}} </n8n-link
|
||||
>)
|
||||
</span>
|
||||
</template>
|
||||
</n8n-text>
|
||||
</page-view-layout-list>
|
||||
</template>
|
||||
</page-view-layout>
|
||||
@@ -217,6 +217,10 @@ export default mixins(showMessage, debounceHelper).extend({
|
||||
type: Array,
|
||||
default: (): IResource[] => [],
|
||||
},
|
||||
itemSize: {
|
||||
type: Number,
|
||||
default: 80,
|
||||
},
|
||||
initialize: {
|
||||
type: Function as PropType<() => Promise<void>>,
|
||||
default: () => () => Promise.resolve(),
|
||||
@@ -438,8 +442,8 @@ export default mixins(showMessage, debounceHelper).extend({
|
||||
}
|
||||
|
||||
.list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
//display: flex;
|
||||
//flex-direction: column;
|
||||
}
|
||||
|
||||
.sort-and-filter {
|
||||
|
||||
Reference in New Issue
Block a user