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:
Alex Grozav
2023-01-27 09:51:32 +02:00
committed by GitHub
parent 8ce85e3759
commit 874c735d0a
15 changed files with 468 additions and 69 deletions

View File

@@ -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();

View File

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

View File

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

View File

@@ -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 {