mirror of
https://github.com/n8n-io/n8n.git
synced 2024-12-26 05:04:05 -08:00
feat(editor): Load fixed template list as experiment (#6632)
* feat(editor): Load fixed template list as experiment Signed-off-by: Oleg Ivaniv <me@olegivaniv.com> * Improve templates loading Signed-off-by: Oleg Ivaniv <me@olegivaniv.com> * get rid of endResult message Signed-off-by: Oleg Ivaniv <me@olegivaniv.com> * Do not lazy-load when fixedListExperiment Signed-off-by: Oleg Ivaniv <me@olegivaniv.com> --------- Signed-off-by: Oleg Ivaniv <me@olegivaniv.com>
This commit is contained in:
parent
aa53c46367
commit
e9966224ea
|
@ -31,7 +31,7 @@ import type { PropType } from 'vue';
|
||||||
import type { ITemplatesCollection } from '@/Interface';
|
import type { ITemplatesCollection } from '@/Interface';
|
||||||
import Card from '@/components/CollectionWorkflowCard.vue';
|
import Card from '@/components/CollectionWorkflowCard.vue';
|
||||||
import CollectionCard from '@/components/CollectionCard.vue';
|
import CollectionCard from '@/components/CollectionCard.vue';
|
||||||
import VueAgile from 'vue-agile';
|
import { VueAgile } from 'vue-agile';
|
||||||
|
|
||||||
import { genericHelpers } from '@/mixins/genericHelpers';
|
import { genericHelpers } from '@/mixins/genericHelpers';
|
||||||
|
|
||||||
|
|
|
@ -55,6 +55,7 @@ import { abbreviateNumber, filterTemplateNodes } from '@/utils';
|
||||||
import type { ITemplatesNode, ITemplatesWorkflow, ITemplatesWorkflowFull } from '@/Interface';
|
import type { ITemplatesNode, ITemplatesWorkflow, ITemplatesWorkflowFull } from '@/Interface';
|
||||||
import { mapStores } from 'pinia';
|
import { mapStores } from 'pinia';
|
||||||
import { useTemplatesStore } from '@/stores/templates.store';
|
import { useTemplatesStore } from '@/stores/templates.store';
|
||||||
|
import TimeAgo from '@/components/TimeAgo.vue';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'TemplateDetails',
|
name: 'TemplateDetails',
|
||||||
|
@ -72,6 +73,7 @@ export default defineComponent({
|
||||||
components: {
|
components: {
|
||||||
NodeIcon,
|
NodeIcon,
|
||||||
TemplateDetailsBlock,
|
TemplateDetailsBlock,
|
||||||
|
TimeAgo,
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapStores(useTemplatesStore),
|
...mapStores(useTemplatesStore),
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<div :class="$style.list" v-if="loading || workflows.length">
|
<div :class="$style.list" v-if="loading || workflows.length">
|
||||||
<div :class="$style.header">
|
<div :class="$style.header" v-if="!hideHeader">
|
||||||
<n8n-heading :bold="true" size="medium" color="text-light">
|
<n8n-heading :bold="true" size="medium" color="text-light">
|
||||||
{{ $locale.baseText('templates.workflows') }}
|
{{ $locale.baseText('templates.workflows') }}
|
||||||
<span v-if="!loading && totalWorkflows" v-text="`(${totalWorkflows})`" />
|
<span v-if="!loading && totalWorkflows" v-text="`(${totalWorkflows})`" />
|
||||||
|
@ -57,6 +57,10 @@ export default defineComponent({
|
||||||
totalWorkflows: {
|
totalWorkflows: {
|
||||||
type: Number,
|
type: Number,
|
||||||
},
|
},
|
||||||
|
hideHeader: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
if (this.infiniteScrollEnabled) {
|
if (this.infiniteScrollEnabled) {
|
||||||
|
|
|
@ -526,10 +526,11 @@ export const KEEP_AUTH_IN_NDV_FOR_NODES = [
|
||||||
export const MAIN_AUTH_FIELD_NAME = 'authentication';
|
export const MAIN_AUTH_FIELD_NAME = 'authentication';
|
||||||
export const NODE_RESOURCE_FIELD_NAME = 'resource';
|
export const NODE_RESOURCE_FIELD_NAME = 'resource';
|
||||||
|
|
||||||
export const TEMPLATE_EXPERIMENT = {
|
export const TEMPLATES_EXPERIMENT = {
|
||||||
name: '002_remove_templates',
|
name: '008_template_variants',
|
||||||
control: 'control',
|
control: 'control',
|
||||||
variant: 'variant',
|
variant: 'variant',
|
||||||
|
variantIds: ['1932', '1930', '1931', '1933', '1750', '1748', '1435'],
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ONBOARDING_EXPERIMENT = {
|
export const ONBOARDING_EXPERIMENT = {
|
||||||
|
@ -538,7 +539,7 @@ export const ONBOARDING_EXPERIMENT = {
|
||||||
variant: 'variant',
|
variant: 'variant',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const EXPERIMENTS_TO_TRACK = [TEMPLATE_EXPERIMENT.name, ONBOARDING_EXPERIMENT.name];
|
export const EXPERIMENTS_TO_TRACK = [TEMPLATES_EXPERIMENT.name, ONBOARDING_EXPERIMENT.name];
|
||||||
|
|
||||||
export const NODE_TYPES_EXCLUDED_FROM_OUTPUT_NAME_APPEND = [FILTER_NODE_TYPE];
|
export const NODE_TYPES_EXCLUDED_FROM_OUTPUT_NAME_APPEND = [FILTER_NODE_TYPE];
|
||||||
|
|
||||||
|
|
|
@ -1462,7 +1462,6 @@
|
||||||
"templates.collections": "Collections",
|
"templates.collections": "Collections",
|
||||||
"templates.collectionsNotFound": "Collection could not be found",
|
"templates.collectionsNotFound": "Collection could not be found",
|
||||||
"templates.connectionWarning": "⚠️ There was a problem fetching workflow templates. Check your internet connection.",
|
"templates.connectionWarning": "⚠️ There was a problem fetching workflow templates. Check your internet connection.",
|
||||||
"templates.endResult": "Share your own useful workflows through your <a href='https://n8n.io/dashboard' target='_blank'>n8n.io account</a>",
|
|
||||||
"templates.heading": "Workflow templates",
|
"templates.heading": "Workflow templates",
|
||||||
"templates.newButton": "New blank workflow",
|
"templates.newButton": "New blank workflow",
|
||||||
"templates.noSearchResults": "Nothing found. Try adjusting your search to see more.",
|
"templates.noSearchResults": "Nothing found. Try adjusting your search to see more.",
|
||||||
|
|
|
@ -33,7 +33,6 @@ import VariablesView from '@/views/VariablesView.vue';
|
||||||
import type { IPermissions } from './Interface';
|
import type { IPermissions } from './Interface';
|
||||||
import { LOGIN_STATUS, ROLE } from '@/utils';
|
import { LOGIN_STATUS, ROLE } from '@/utils';
|
||||||
import type { RouteConfigSingleView } from 'vue-router/types/router';
|
import type { RouteConfigSingleView } from 'vue-router/types/router';
|
||||||
import { TEMPLATE_EXPERIMENT, VIEWS } from './constants';
|
|
||||||
import { useSettingsStore } from './stores/settings.store';
|
import { useSettingsStore } from './stores/settings.store';
|
||||||
import { useTemplatesStore } from './stores/templates.store';
|
import { useTemplatesStore } from './stores/templates.store';
|
||||||
import { useSSOStore } from './stores/sso.store';
|
import { useSSOStore } from './stores/sso.store';
|
||||||
|
@ -43,7 +42,7 @@ import SignoutView from '@/views/SignoutView.vue';
|
||||||
import SamlOnboarding from '@/views/SamlOnboarding.vue';
|
import SamlOnboarding from '@/views/SamlOnboarding.vue';
|
||||||
import SettingsSourceControl from './views/SettingsSourceControl.vue';
|
import SettingsSourceControl from './views/SettingsSourceControl.vue';
|
||||||
import SettingsAuditLogs from './views/SettingsAuditLogs.vue';
|
import SettingsAuditLogs from './views/SettingsAuditLogs.vue';
|
||||||
import { usePostHog } from './stores/posthog.store';
|
import { VIEWS } from '@/constants';
|
||||||
|
|
||||||
Vue.use(Router);
|
Vue.use(Router);
|
||||||
|
|
||||||
|
@ -63,12 +62,8 @@ interface IRouteConfig extends RouteConfigSingleView {
|
||||||
|
|
||||||
function getTemplatesRedirect() {
|
function getTemplatesRedirect() {
|
||||||
const settingsStore = useSettingsStore();
|
const settingsStore = useSettingsStore();
|
||||||
const posthog = usePostHog();
|
|
||||||
const isTemplatesEnabled: boolean = settingsStore.isTemplatesEnabled;
|
const isTemplatesEnabled: boolean = settingsStore.isTemplatesEnabled;
|
||||||
if (
|
if (!isTemplatesEnabled) {
|
||||||
!posthog.isVariantEnabled(TEMPLATE_EXPERIMENT.name, TEMPLATE_EXPERIMENT.variant) &&
|
|
||||||
!isTemplatesEnabled
|
|
||||||
) {
|
|
||||||
return { name: VIEWS.NOT_FOUND };
|
return { name: VIEWS.NOT_FOUND };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -10,7 +10,6 @@ import {
|
||||||
EXPERIMENTS_TO_TRACK,
|
EXPERIMENTS_TO_TRACK,
|
||||||
LOCAL_STORAGE_EXPERIMENT_OVERRIDES,
|
LOCAL_STORAGE_EXPERIMENT_OVERRIDES,
|
||||||
ONBOARDING_EXPERIMENT,
|
ONBOARDING_EXPERIMENT,
|
||||||
TEMPLATE_EXPERIMENT,
|
|
||||||
} from '@/constants';
|
} from '@/constants';
|
||||||
import { useTelemetryStore } from './telemetry.store';
|
import { useTelemetryStore } from './telemetry.store';
|
||||||
import { debounce } from 'lodash-es';
|
import { debounce } from 'lodash-es';
|
||||||
|
@ -134,7 +133,6 @@ export const usePostHog = defineStore('posthog', () => {
|
||||||
// does not need to be debounced really, but tracking does not fire without delay on page load
|
// does not need to be debounced really, but tracking does not fire without delay on page load
|
||||||
addExperimentOverrides();
|
addExperimentOverrides();
|
||||||
trackExperimentsDebounced(featureFlags.value);
|
trackExperimentsDebounced(featureFlags.value);
|
||||||
evaluateExperiments(featureFlags.value);
|
|
||||||
} else {
|
} else {
|
||||||
// depend on client side evaluation if serverside evaluation fails
|
// depend on client side evaluation if serverside evaluation fails
|
||||||
window.posthog?.onFeatureFlags?.((keys: string[], map: FeatureFlags) => {
|
window.posthog?.onFeatureFlags?.((keys: string[], map: FeatureFlags) => {
|
||||||
|
@ -143,21 +141,10 @@ export const usePostHog = defineStore('posthog', () => {
|
||||||
|
|
||||||
// must be debounced because it is called multiple times by posthog
|
// must be debounced because it is called multiple times by posthog
|
||||||
trackExperimentsDebounced(featureFlags.value);
|
trackExperimentsDebounced(featureFlags.value);
|
||||||
evaluateExperimentsDebounced(featureFlags.value);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const evaluateExperiments = (featureFlags: FeatureFlags) => {
|
|
||||||
Object.keys(featureFlags).forEach((name) => {
|
|
||||||
const variant = featureFlags[name];
|
|
||||||
if (name === TEMPLATE_EXPERIMENT.name && variant === TEMPLATE_EXPERIMENT.variant) {
|
|
||||||
settingsStore.disableTemplates();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
const evaluateExperimentsDebounced = debounce(evaluateExperiments, 2000);
|
|
||||||
|
|
||||||
const trackExperiments = (featureFlags: FeatureFlags) => {
|
const trackExperiments = (featureFlags: FeatureFlags) => {
|
||||||
EXPERIMENTS_TO_TRACK.forEach((name) => trackExperiment(featureFlags, name));
|
EXPERIMENTS_TO_TRACK.forEach((name) => trackExperiment(featureFlags, name));
|
||||||
};
|
};
|
||||||
|
|
|
@ -18,7 +18,7 @@
|
||||||
</template>
|
</template>
|
||||||
<template #content>
|
<template #content>
|
||||||
<div :class="$style.contentWrapper">
|
<div :class="$style.contentWrapper">
|
||||||
<div :class="$style.filters">
|
<div :class="$style.filters" v-if="!isFixedListExperiment">
|
||||||
<TemplateFilters
|
<TemplateFilters
|
||||||
:categories="templatesStore.allCategories"
|
:categories="templatesStore.allCategories"
|
||||||
:sortOnPopulate="areCategoriesPrepopulated"
|
:sortOnPopulate="areCategoriesPrepopulated"
|
||||||
|
@ -30,36 +30,42 @@
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div :class="$style.search">
|
<div :class="$style.search">
|
||||||
<n8n-input
|
<template v-if="!isFixedListExperiment">
|
||||||
:value="search"
|
<n8n-input
|
||||||
:placeholder="$locale.baseText('templates.searchPlaceholder')"
|
:value="search"
|
||||||
@input="onSearchInput"
|
:placeholder="$locale.baseText('templates.searchPlaceholder')"
|
||||||
@blur="trackSearch"
|
@input="onSearchInput"
|
||||||
clearable
|
@blur="trackSearch"
|
||||||
>
|
clearable
|
||||||
<template #prefix>
|
>
|
||||||
<font-awesome-icon icon="search" />
|
<template #prefix>
|
||||||
</template>
|
<font-awesome-icon icon="search" />
|
||||||
</n8n-input>
|
</template>
|
||||||
<div :class="$style.carouselContainer" v-show="collections.length || loadingCollections">
|
</n8n-input>
|
||||||
<div :class="$style.header">
|
<div
|
||||||
<n8n-heading :bold="true" size="medium" color="text-light">
|
:class="$style.carouselContainer"
|
||||||
{{ $locale.baseText('templates.collections') }}
|
v-show="collections.length || loadingCollections"
|
||||||
<span v-if="!loadingCollections" v-text="`(${collections.length})`" />
|
>
|
||||||
</n8n-heading>
|
<div :class="$style.header">
|
||||||
</div>
|
<n8n-heading :bold="true" size="medium" color="text-light">
|
||||||
|
{{ $locale.baseText('templates.collections') }}
|
||||||
|
<span v-if="!loadingCollections" v-text="`(${collections.length})`" />
|
||||||
|
</n8n-heading>
|
||||||
|
</div>
|
||||||
|
|
||||||
<CollectionsCarousel
|
<CollectionsCarousel
|
||||||
:collections="collections"
|
:collections="collections"
|
||||||
:loading="loadingCollections"
|
:loading="loadingCollections"
|
||||||
@openCollection="onOpenCollection"
|
@openCollection="onOpenCollection"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</template>
|
||||||
<TemplateList
|
<TemplateList
|
||||||
:infinite-scroll-enabled="true"
|
:infinite-scroll-enabled="!isFixedListExperiment"
|
||||||
:loading="loadingWorkflows"
|
:loading="loadingWorkflows"
|
||||||
:total-workflows="totalWorkflows"
|
:total-workflows="totalWorkflows"
|
||||||
:workflows="workflows"
|
:workflows="isFixedListExperiment ? fixedTemplatesList : workflows"
|
||||||
|
:hide-header="isFixedListExperiment"
|
||||||
@loadMore="onLoadMore"
|
@loadMore="onLoadMore"
|
||||||
@openTemplate="onOpenTemplate"
|
@openTemplate="onOpenTemplate"
|
||||||
/>
|
/>
|
||||||
|
@ -91,13 +97,14 @@ import type {
|
||||||
} from '@/Interface';
|
} from '@/Interface';
|
||||||
import type { IDataObject } from 'n8n-workflow';
|
import type { IDataObject } from 'n8n-workflow';
|
||||||
import { setPageTitle } from '@/utils';
|
import { setPageTitle } from '@/utils';
|
||||||
import { VIEWS } from '@/constants';
|
import { TEMPLATES_EXPERIMENT, VIEWS } from '@/constants';
|
||||||
import { debounceHelper } from '@/mixins/debounce';
|
import { debounceHelper } from '@/mixins/debounce';
|
||||||
import { useSettingsStore } from '@/stores/settings.store';
|
import { useSettingsStore } from '@/stores/settings.store';
|
||||||
import { useUsersStore } from '@/stores/users.store';
|
import { useUsersStore } from '@/stores/users.store';
|
||||||
import { useTemplatesStore } from '@/stores/templates.store';
|
import { useTemplatesStore } from '@/stores/templates.store';
|
||||||
import { useUIStore } from '@/stores/ui.store';
|
import { useUIStore } from '@/stores/ui.store';
|
||||||
import { useToast } from '@/composables';
|
import { useToast } from '@/composables';
|
||||||
|
import { usePostHog } from '@/stores/posthog.store';
|
||||||
|
|
||||||
interface ISearchEvent {
|
interface ISearchEvent {
|
||||||
search_string: string;
|
search_string: string;
|
||||||
|
@ -135,7 +142,18 @@ export default defineComponent({
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapStores(useSettingsStore, useTemplatesStore, useUIStore, useUsersStore),
|
...mapStores(useSettingsStore, useTemplatesStore, useUIStore, useUsersStore, usePostHog),
|
||||||
|
isFixedListExperiment() {
|
||||||
|
return this.posthogStore.isVariantEnabled(
|
||||||
|
TEMPLATES_EXPERIMENT.name,
|
||||||
|
TEMPLATES_EXPERIMENT.variant,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
fixedTemplatesList() {
|
||||||
|
return TEMPLATES_EXPERIMENT.variantIds
|
||||||
|
.map((id) => this.templatesStore.workflows[id])
|
||||||
|
.filter(Boolean);
|
||||||
|
},
|
||||||
totalWorkflows(): number {
|
totalWorkflows(): number {
|
||||||
return this.templatesStore.getSearchedWorkflowsTotal(this.query);
|
return this.templatesStore.getSearchedWorkflowsTotal(this.query);
|
||||||
},
|
},
|
||||||
|
@ -149,9 +167,6 @@ export default defineComponent({
|
||||||
if (this.loadingWorkflows) {
|
if (this.loadingWorkflows) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (this.workflows.length && this.workflows.length >= this.totalWorkflows) {
|
|
||||||
return this.$locale.baseText('templates.endResult');
|
|
||||||
}
|
|
||||||
if (
|
if (
|
||||||
!this.loadingCollections &&
|
!this.loadingCollections &&
|
||||||
this.workflows.length === 0 &&
|
this.workflows.length === 0 &&
|
||||||
|
@ -391,6 +406,15 @@ export default defineComponent({
|
||||||
}, 100);
|
}, 100);
|
||||||
},
|
},
|
||||||
async created() {
|
async created() {
|
||||||
|
if (this.isFixedListExperiment) {
|
||||||
|
// Templates are lazy-loaded so we need to make sure the fixed ids are loaded
|
||||||
|
TEMPLATES_EXPERIMENT.variantIds.forEach(async (templateId) =>
|
||||||
|
this.templatesStore.fetchTemplateById(templateId),
|
||||||
|
);
|
||||||
|
// Categorization and filtering based on search is not supported if fixed list is enabled
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (this.$route.query.search && typeof this.$route.query.search === 'string') {
|
if (this.$route.query.search && typeof this.$route.query.search === 'string') {
|
||||||
this.search = this.$route.query.search;
|
this.search = this.$route.query.search;
|
||||||
}
|
}
|
||||||
|
@ -423,11 +447,11 @@ export default defineComponent({
|
||||||
.filters {
|
.filters {
|
||||||
width: 200px;
|
width: 200px;
|
||||||
margin-bottom: var(--spacing-xl);
|
margin-bottom: var(--spacing-xl);
|
||||||
|
margin-right: var(--spacing-2xl);
|
||||||
}
|
}
|
||||||
|
|
||||||
.search {
|
.search {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding-left: var(--spacing-2xl);
|
|
||||||
|
|
||||||
> * {
|
> * {
|
||||||
margin-bottom: var(--spacing-l);
|
margin-bottom: var(--spacing-l);
|
||||||
|
|
Loading…
Reference in a new issue