mirror of
https://github.com/n8n-io/n8n.git
synced 2025-01-23 10:32:17 -08: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:
parent
8ce85e3759
commit
874c735d0a
|
@ -2,15 +2,17 @@ import { WorkflowPage, WorkflowsPage, NDV } from '../pages';
|
|||
|
||||
const workflowsPage = new WorkflowsPage();
|
||||
const workflowPage = new WorkflowPage();
|
||||
const ndv = new NDV()
|
||||
const ndv = new NDV();
|
||||
|
||||
describe('HTTP Request node', () => {
|
||||
before(() => {
|
||||
beforeEach(() => {
|
||||
cy.resetAll();
|
||||
cy.skipSetup();
|
||||
});
|
||||
|
||||
it('should make a request with a URL and receive a response', () => {
|
||||
cy.visit(workflowsPage.url);
|
||||
|
||||
workflowsPage.actions.createWorkflowFromCard();
|
||||
workflowPage.actions.addInitialNodeToCanvas('Manual Trigger');
|
||||
workflowPage.actions.addNodeToCanvas('HTTP Request');
|
||||
|
|
|
@ -0,0 +1,59 @@
|
|||
/* eslint-disable @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment */
|
||||
import type { StoryFn } from '@storybook/vue';
|
||||
import N8nRecycleScroller from './RecycleScroller.vue';
|
||||
import { ComponentInstance } from 'vue';
|
||||
|
||||
export default {
|
||||
title: 'Atoms/RecycleScroller',
|
||||
component: N8nRecycleScroller,
|
||||
argTypes: {},
|
||||
};
|
||||
|
||||
const Template: StoryFn = () => ({
|
||||
components: {
|
||||
N8nRecycleScroller,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
items: Array.from(Array(256).keys()).map((i) => ({ id: i })) as Array<{
|
||||
id: number;
|
||||
height: number;
|
||||
}>,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
resizeItem(item: { id: string; height: string }, fn: (item: { id: string }) => void) {
|
||||
const itemRef = (this as ComponentInstance).$refs[`item-${item.id}`] as HTMLElement;
|
||||
|
||||
item.height = '200px';
|
||||
itemRef.style.height = '200px';
|
||||
fn(item);
|
||||
},
|
||||
getItemStyle(item: { id: string; height?: string }) {
|
||||
return {
|
||||
height: item.height || '100px',
|
||||
width: '100%',
|
||||
backgroundColor: `hsl(${parseInt(item.id, 10) * 1.4}, 100%, 50%)`,
|
||||
cursor: 'pointer',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
};
|
||||
},
|
||||
},
|
||||
template: `<div style="height: calc(100vh - 30px); width: 100%; overflow: auto">
|
||||
<N8nRecycleScroller :items="items" :item-size="100" item-key="id" v-bind="$props">
|
||||
<template #default="{ item, updateItemSize }">
|
||||
<div
|
||||
:ref="'item-' + item.id"
|
||||
:style="getItemStyle(item)"
|
||||
@click="resizeItem(item, updateItemSize)"
|
||||
>
|
||||
{{item.id}}
|
||||
</div>
|
||||
</template>
|
||||
</N8nRecycleScroller>
|
||||
</div>`,
|
||||
});
|
||||
|
||||
export const RecycleScroller = Template.bind({});
|
|
@ -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>
|
|
@ -0,0 +1,3 @@
|
|||
import N8nRecycleScroller from './RecycleScroller.vue';
|
||||
|
||||
export default N8nRecycleScroller;
|
|
@ -11,7 +11,7 @@
|
|||
theme="text"
|
||||
underline
|
||||
size="small"
|
||||
@click.stop.prevent="showAll = true"
|
||||
@click.stop.prevent="onExpand"
|
||||
>
|
||||
{{ t('tags.showMore', hiddenTagsLength) }}
|
||||
</n8n-link>
|
||||
|
@ -67,6 +67,12 @@ export default mixins(Locale).extend({
|
|||
return this.tags.length - this.truncateAt;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onExpand() {
|
||||
this.showAll = true;
|
||||
this.$emit('expand', true);
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
|
@ -45,6 +45,7 @@ import N8nUserInfo from '../components/N8nUserInfo';
|
|||
import N8nUserSelect from '../components/N8nUserSelect';
|
||||
import N8nUsersList from '../components/N8nUsersList';
|
||||
import N8nResizeWrapper from '../components/N8nResizeWrapper';
|
||||
import N8nRecycleScroller from '../components/N8nRecycleScroller';
|
||||
|
||||
export default {
|
||||
install: (app: typeof Vue) => {
|
||||
|
@ -94,5 +95,6 @@ export default {
|
|||
app.component('n8n-users-list', N8nUsersList);
|
||||
app.component('n8n-user-select', N8nUserSelect);
|
||||
app.component('n8n-resize-wrapper', N8nResizeWrapper);
|
||||
app.component('n8n-recycle-scroller', N8nRecycleScroller);
|
||||
},
|
||||
};
|
||||
|
|
|
@ -10,6 +10,12 @@ export async function getNewWorkflow(context: IRestApiContext, name?: string) {
|
|||
};
|
||||
}
|
||||
|
||||
export async function getWorkflow(context: IRestApiContext, id: string, filter?: object) {
|
||||
const sendData = filter ? { filter } : undefined;
|
||||
|
||||
return await makeRestApiRequest(context, 'GET', `/workflows/${id}`, sendData);
|
||||
}
|
||||
|
||||
export async function getWorkflows(context: IRestApiContext, filter?: object) {
|
||||
const sendData = filter ? { filter } : undefined;
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -2,11 +2,10 @@
|
|||
|
||||
import Vue from 'vue';
|
||||
import Fragment from 'vue-fragment';
|
||||
import VueAgile from 'vue-agile';
|
||||
|
||||
import 'regenerator-runtime/runtime';
|
||||
|
||||
import VueAgile from 'vue-agile';
|
||||
|
||||
import ElementUI from 'element-ui';
|
||||
import { Loading, MessageBox, Message, Notification } from 'element-ui';
|
||||
import { designSystemComponents } from 'n8n-design-system';
|
||||
|
@ -14,13 +13,13 @@ import { ElMessageBoxOptions } from 'element-ui/types/message-box';
|
|||
import EnterpriseEdition from '@/components/EnterpriseEdition.ee.vue';
|
||||
|
||||
Vue.use(Fragment.Plugin);
|
||||
Vue.use(VueAgile);
|
||||
|
||||
Vue.use(ElementUI);
|
||||
Vue.use(designSystemComponents);
|
||||
|
||||
Vue.component('enterprise-edition', EnterpriseEdition);
|
||||
|
||||
Vue.use(VueAgile);
|
||||
Vue.use(Loading.directive);
|
||||
|
||||
Vue.prototype.$loading = Loading.service;
|
||||
|
|
|
@ -50,6 +50,7 @@ import {
|
|||
getExecutionData,
|
||||
getFinishedExecutions,
|
||||
getNewWorkflow,
|
||||
getWorkflow,
|
||||
getWorkflows,
|
||||
} from '@/api/workflows';
|
||||
import { useUIStore } from './ui';
|
||||
|
@ -258,6 +259,13 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, {
|
|||
return workflows;
|
||||
},
|
||||
|
||||
async fetchWorkflow(id: string): Promise<IWorkflowDb> {
|
||||
const rootStore = useRootStore();
|
||||
const workflow = await getWorkflow(rootStore.getRestApiContext, id);
|
||||
this.addWorkflow(workflow);
|
||||
return workflow;
|
||||
},
|
||||
|
||||
async getNewWorkflowData(name?: string): Promise<INewWorkflowData> {
|
||||
const workflowsEEStore = useWorkflowsEEStore();
|
||||
|
||||
|
|
|
@ -6,11 +6,12 @@
|
|||
:initialize="initialize"
|
||||
:filters="filters"
|
||||
:additional-filters-handler="onFilter"
|
||||
:item-size="77"
|
||||
@click:add="addCredential"
|
||||
@update:filters="filters = $event"
|
||||
>
|
||||
<template #default="{ data }">
|
||||
<credential-card :data="data" />
|
||||
<credential-card data-test-id="resources-list-item" class="mb-2xs" :data="data" />
|
||||
</template>
|
||||
<template #filters="{ setKeyValue }">
|
||||
<div class="mb-s">
|
||||
|
|
|
@ -22,8 +22,14 @@
|
|||
</template>
|
||||
</n8n-callout>
|
||||
</template>
|
||||
<template #default="{ data }">
|
||||
<workflow-card :data="data" @click:tag="onClickTag" />
|
||||
<template #default="{ data, updateItemSize }">
|
||||
<workflow-card
|
||||
data-test-id="resources-list-item"
|
||||
class="mb-2xs"
|
||||
:data="data"
|
||||
@expand:tags="updateItemSize(data)"
|
||||
@click:tag="onClickTag"
|
||||
/>
|
||||
</template>
|
||||
<template #empty>
|
||||
<div class="text-center mt-s">
|
||||
|
|
Loading…
Reference in a new issue