mirror of
https://github.com/n8n-io/n8n.git
synced 2024-12-27 21:49:41 -08:00
refactor(editor): Migrate TemplatesSearchView.vue
to composition API (#11571)
Some checks are pending
Test Master / install-and-build (push) Waiting to run
Test Master / Unit tests (18.x) (push) Blocked by required conditions
Test Master / Unit tests (20.x) (push) Blocked by required conditions
Test Master / Unit tests (22.4) (push) Blocked by required conditions
Test Master / Lint (push) Blocked by required conditions
Test Master / Notify Slack on failure (push) Blocked by required conditions
Benchmark Docker Image CI / build (push) Waiting to run
Some checks are pending
Test Master / install-and-build (push) Waiting to run
Test Master / Unit tests (18.x) (push) Blocked by required conditions
Test Master / Unit tests (20.x) (push) Blocked by required conditions
Test Master / Unit tests (22.4) (push) Blocked by required conditions
Test Master / Lint (push) Blocked by required conditions
Test Master / Notify Slack on failure (push) Blocked by required conditions
Benchmark Docker Image CI / build (push) Waiting to run
This commit is contained in:
parent
8a484077af
commit
c1a7f68236
|
@ -1,27 +1,22 @@
|
||||||
<script lang="ts">
|
<script setup lang="ts">
|
||||||
import { defineComponent } from 'vue';
|
import { computed, onMounted, ref, watch } from 'vue';
|
||||||
import { mapStores } from 'pinia';
|
|
||||||
import TemplatesInfoCarousel from '@/components/TemplatesInfoCarousel.vue';
|
import TemplatesInfoCarousel from '@/components/TemplatesInfoCarousel.vue';
|
||||||
import TemplateFilters from '@/components/TemplateFilters.vue';
|
import TemplateFilters from '@/components/TemplateFilters.vue';
|
||||||
import TemplateList from '@/components/TemplateList.vue';
|
import TemplateList from '@/components/TemplateList.vue';
|
||||||
import TemplatesView from '@/views/TemplatesView.vue';
|
import TemplatesView from '@/views/TemplatesView.vue';
|
||||||
|
|
||||||
import type {
|
import type { ITemplatesCategory } from '@/Interface';
|
||||||
ITemplatesCollection,
|
|
||||||
ITemplatesWorkflow,
|
|
||||||
ITemplatesQuery,
|
|
||||||
ITemplatesCategory,
|
|
||||||
} from '@/Interface';
|
|
||||||
import type { IDataObject } from 'n8n-workflow';
|
import type { IDataObject } from 'n8n-workflow';
|
||||||
import { CREATOR_HUB_URL, VIEWS } from '@/constants';
|
import { CREATOR_HUB_URL, VIEWS } from '@/constants';
|
||||||
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 { useToast } from '@/composables/useToast';
|
import { useToast } from '@/composables/useToast';
|
||||||
import { usePostHog } from '@/stores/posthog.store';
|
|
||||||
import { useDebounce } from '@/composables/useDebounce';
|
import { useDebounce } from '@/composables/useDebounce';
|
||||||
import { useDocumentTitle } from '@/composables/useDocumentTitle';
|
import { useDocumentTitle } from '@/composables/useDocumentTitle';
|
||||||
|
import { useI18n } from '@/composables/useI18n';
|
||||||
|
import { useRoute, onBeforeRouteLeave, useRouter } from 'vue-router';
|
||||||
|
import { useTelemetry } from '@/composables/useTelemetry';
|
||||||
|
|
||||||
interface ISearchEvent {
|
interface ISearchEvent {
|
||||||
search_string: string;
|
search_string: string;
|
||||||
|
@ -31,220 +26,65 @@ interface ISearchEvent {
|
||||||
wf_template_repo_session_id: string;
|
wf_template_repo_session_id: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default defineComponent({
|
const areCategoriesPrepopulated = ref(false);
|
||||||
name: 'TemplatesSearchView',
|
const categories = ref<ITemplatesCategory[]>([]);
|
||||||
components: {
|
const loadingCategories = ref(true);
|
||||||
TemplatesInfoCarousel,
|
const loadingCollections = ref(true);
|
||||||
TemplateFilters,
|
const loadingWorkflows = ref(true);
|
||||||
TemplateList,
|
const search = ref('');
|
||||||
TemplatesView,
|
const searchEventToTrack = ref<ISearchEvent | null>(null);
|
||||||
},
|
const errorLoadingWorkflows = ref(false);
|
||||||
beforeRouteLeave(_to, _from, next) {
|
|
||||||
const contentArea = document.getElementById('content');
|
|
||||||
if (contentArea) {
|
|
||||||
// When leaving this page, store current scroll position in route data
|
|
||||||
this.$route.meta?.setScrollPosition?.(contentArea.scrollTop);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.trackSearch();
|
const { callDebounced } = useDebounce();
|
||||||
next();
|
const toast = useToast();
|
||||||
},
|
const documentTitle = useDocumentTitle();
|
||||||
setup() {
|
|
||||||
const { callDebounced } = useDebounce();
|
|
||||||
|
|
||||||
return {
|
const settingsStore = useSettingsStore();
|
||||||
callDebounced,
|
const templatesStore = useTemplatesStore();
|
||||||
...useToast(),
|
const usersStore = useUsersStore();
|
||||||
documentTitle: useDocumentTitle(),
|
const i18n = useI18n();
|
||||||
};
|
const route = useRoute();
|
||||||
},
|
const router = useRouter();
|
||||||
data() {
|
const telemetry = useTelemetry();
|
||||||
return {
|
|
||||||
areCategoriesPrepopulated: false,
|
|
||||||
categories: [] as ITemplatesCategory[],
|
|
||||||
loading: true,
|
|
||||||
loadingCategories: true,
|
|
||||||
loadingCollections: true,
|
|
||||||
loadingWorkflows: true,
|
|
||||||
search: '',
|
|
||||||
searchEventToTrack: null as null | ISearchEvent,
|
|
||||||
errorLoadingWorkflows: false,
|
|
||||||
creatorHubUrl: CREATOR_HUB_URL as string,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
...mapStores(useSettingsStore, useTemplatesStore, useUIStore, useUsersStore, usePostHog),
|
|
||||||
totalWorkflows(): number {
|
|
||||||
return this.templatesStore.getSearchedWorkflowsTotal(this.createQueryObject('name'));
|
|
||||||
},
|
|
||||||
workflows(): ITemplatesWorkflow[] {
|
|
||||||
return this.templatesStore.getSearchedWorkflows(this.createQueryObject('name')) ?? [];
|
|
||||||
},
|
|
||||||
collections(): ITemplatesCollection[] {
|
|
||||||
return this.templatesStore.getSearchedCollections(this.createQueryObject('id')) ?? [];
|
|
||||||
},
|
|
||||||
endOfSearchMessage(): string | null {
|
|
||||||
if (this.loadingWorkflows) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
!this.loadingCollections &&
|
|
||||||
this.workflows.length === 0 &&
|
|
||||||
this.collections.length === 0
|
|
||||||
) {
|
|
||||||
if (!this.settingsStore.isTemplatesEndpointReachable && this.errorLoadingWorkflows) {
|
|
||||||
return this.$locale.baseText('templates.connectionWarning');
|
|
||||||
}
|
|
||||||
return this.$locale.baseText('templates.noSearchResults');
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
const createQueryObject = (categoryId: 'name' | 'id') => {
|
||||||
},
|
|
||||||
nothingFound(): boolean {
|
|
||||||
return (
|
|
||||||
!this.loadingWorkflows &&
|
|
||||||
!this.loadingCollections &&
|
|
||||||
this.workflows.length === 0 &&
|
|
||||||
this.collections.length === 0
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
workflows(newWorkflows) {
|
|
||||||
if (newWorkflows.length === 0) {
|
|
||||||
this.scrollTo(0);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
async mounted() {
|
|
||||||
this.documentTitle.set('Templates');
|
|
||||||
await this.loadCategories();
|
|
||||||
void this.loadWorkflowsAndCollections(true);
|
|
||||||
void this.usersStore.showPersonalizationSurvey();
|
|
||||||
|
|
||||||
this.restoreSearchFromRoute();
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
// Check if there is scroll position saved in route and scroll to it
|
|
||||||
const scrollOffset = this.$route.meta?.scrollOffset;
|
|
||||||
if (typeof scrollOffset === 'number' && scrollOffset > 0) {
|
|
||||||
this.scrollTo(scrollOffset, 'auto');
|
|
||||||
}
|
|
||||||
}, 100);
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
createQueryObject(categoryId: 'name' | 'id'): ITemplatesQuery {
|
|
||||||
// We are using category names for template search and ids for collection search
|
// We are using category names for template search and ids for collection search
|
||||||
return {
|
return {
|
||||||
categories: this.categories.map((category) =>
|
categories: categories.value.map((category) =>
|
||||||
categoryId === 'name' ? category.name : String(category.id),
|
categoryId === 'name' ? category.name : String(category.id),
|
||||||
),
|
),
|
||||||
search: this.search,
|
search: search.value,
|
||||||
};
|
};
|
||||||
},
|
};
|
||||||
restoreSearchFromRoute() {
|
|
||||||
let updateSearch = false;
|
const totalWorkflows = computed(() =>
|
||||||
if (this.$route.query.search && typeof this.$route.query.search === 'string') {
|
templatesStore.getSearchedWorkflowsTotal(createQueryObject('name')),
|
||||||
this.search = this.$route.query.search;
|
);
|
||||||
updateSearch = true;
|
|
||||||
|
const workflows = computed(
|
||||||
|
() => templatesStore.getSearchedWorkflows(createQueryObject('name')) ?? [],
|
||||||
|
);
|
||||||
|
|
||||||
|
const collections = computed(
|
||||||
|
() => templatesStore.getSearchedCollections(createQueryObject('id')) ?? [],
|
||||||
|
);
|
||||||
|
|
||||||
|
const endOfSearchMessage = computed(() => {
|
||||||
|
if (loadingWorkflows.value) {
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
if (typeof this.$route.query.categories === 'string' && this.$route.query.categories.length) {
|
if (!loadingCollections.value && workflows.value.length === 0 && collections.value.length === 0) {
|
||||||
const categoriesFromURL = this.$route.query.categories.split(',');
|
if (!settingsStore.isTemplatesEndpointReachable && errorLoadingWorkflows.value) {
|
||||||
this.categories = this.templatesStore.allCategories.filter((category) =>
|
return i18n.baseText('templates.connectionWarning');
|
||||||
categoriesFromURL.includes(category.id.toString()),
|
|
||||||
);
|
|
||||||
updateSearch = true;
|
|
||||||
}
|
}
|
||||||
if (updateSearch) {
|
return i18n.baseText('templates.noSearchResults');
|
||||||
this.updateSearch();
|
|
||||||
this.trackCategories();
|
|
||||||
this.areCategoriesPrepopulated = true;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onOpenCollection({ event, id }: { event: MouseEvent; id: number }) {
|
|
||||||
this.navigateTo(event, VIEWS.COLLECTION, id);
|
|
||||||
},
|
|
||||||
onOpenTemplate({ event, id }: { event: MouseEvent; id: number }) {
|
|
||||||
this.navigateTo(event, VIEWS.TEMPLATE, id);
|
|
||||||
},
|
|
||||||
navigateTo(e: MouseEvent, page: string, id: number) {
|
|
||||||
if (e.metaKey || e.ctrlKey) {
|
|
||||||
const route = this.$router.resolve({ name: page, params: { id } });
|
|
||||||
window.open(route.href, '_blank');
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
void this.$router.push({ name: page, params: { id } });
|
|
||||||
}
|
|
||||||
},
|
|
||||||
updateSearch() {
|
|
||||||
this.updateQueryParam(this.search, this.categories.map((category) => category.id).join(','));
|
|
||||||
void this.loadWorkflowsAndCollections(false);
|
|
||||||
},
|
|
||||||
updateSearchTracking(search: string, categories: number[]) {
|
|
||||||
if (!search) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (this.searchEventToTrack && this.searchEventToTrack.search_string.length > search.length) {
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.searchEventToTrack = {
|
return null;
|
||||||
search_string: search,
|
});
|
||||||
workflow_results_count: this.workflows.length,
|
|
||||||
collection_results_count: this.collections.length,
|
|
||||||
categories_applied: categories.map((categoryId) =>
|
|
||||||
this.templatesStore.getCategoryById(categoryId.toString()),
|
|
||||||
) as ITemplatesCategory[],
|
|
||||||
wf_template_repo_session_id: this.templatesStore.currentSessionId,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
trackSearch() {
|
|
||||||
if (this.searchEventToTrack) {
|
|
||||||
this.$telemetry.track(
|
|
||||||
'User searched workflow templates',
|
|
||||||
this.searchEventToTrack as unknown as IDataObject,
|
|
||||||
);
|
|
||||||
this.searchEventToTrack = null;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onSearchInput(search: string) {
|
|
||||||
this.loadingWorkflows = true;
|
|
||||||
this.loadingCollections = true;
|
|
||||||
this.search = search;
|
|
||||||
void this.callDebounced(this.updateSearch, {
|
|
||||||
debounceTime: 500,
|
|
||||||
trailing: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (search.length === 0) {
|
const updateQueryParam = (search: string, category: string) => {
|
||||||
this.trackSearch();
|
const query = Object.assign({}, route.query);
|
||||||
}
|
|
||||||
},
|
|
||||||
onCategorySelected(selected: ITemplatesCategory) {
|
|
||||||
this.categories = this.categories.concat(selected);
|
|
||||||
this.updateSearch();
|
|
||||||
this.trackCategories();
|
|
||||||
},
|
|
||||||
onCategoryUnselected(selected: ITemplatesCategory) {
|
|
||||||
this.categories = this.categories.filter((category) => category.id !== selected.id);
|
|
||||||
this.updateSearch();
|
|
||||||
this.trackCategories();
|
|
||||||
},
|
|
||||||
onCategoriesCleared() {
|
|
||||||
this.categories = [];
|
|
||||||
this.updateSearch();
|
|
||||||
},
|
|
||||||
trackCategories() {
|
|
||||||
if (this.categories.length) {
|
|
||||||
this.$telemetry.track('User changed template filters', {
|
|
||||||
search_string: this.search,
|
|
||||||
categories_applied: this.categories,
|
|
||||||
wf_template_repo_session_id: this.templatesStore.currentSessionId,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
updateQueryParam(search: string, category: string) {
|
|
||||||
const query = Object.assign({}, this.$route.query);
|
|
||||||
|
|
||||||
if (category.length) {
|
if (category.length) {
|
||||||
query.categories = category;
|
query.categories = category;
|
||||||
|
@ -258,71 +98,170 @@ export default defineComponent({
|
||||||
delete query.search;
|
delete query.search;
|
||||||
}
|
}
|
||||||
|
|
||||||
void this.$router.replace({ query });
|
void router.replace({ query });
|
||||||
},
|
};
|
||||||
async onLoadMore() {
|
|
||||||
if (this.workflows.length >= this.totalWorkflows) {
|
const updateSearch = () => {
|
||||||
|
updateQueryParam(search.value, categories.value.map((category) => category.id).join(','));
|
||||||
|
void loadWorkflowsAndCollections(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadWorkflows = async () => {
|
||||||
|
try {
|
||||||
|
loadingWorkflows.value = true;
|
||||||
|
await templatesStore.getWorkflows({
|
||||||
|
search: search.value,
|
||||||
|
categories: categories.value.map((category) => category.name),
|
||||||
|
});
|
||||||
|
errorLoadingWorkflows.value = false;
|
||||||
|
} catch (e) {
|
||||||
|
errorLoadingWorkflows.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
loadingWorkflows.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadCollections = async () => {
|
||||||
|
try {
|
||||||
|
loadingCollections.value = true;
|
||||||
|
await templatesStore.getCollections({
|
||||||
|
categories: categories.value.map((category) => String(category.id)),
|
||||||
|
search: search.value,
|
||||||
|
});
|
||||||
|
} catch (e) {}
|
||||||
|
|
||||||
|
loadingCollections.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateSearchTracking = (search: string, categories: number[]) => {
|
||||||
|
if (!search) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (searchEventToTrack.value && searchEventToTrack.value.search_string.length > search.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
searchEventToTrack.value = {
|
||||||
|
search_string: search,
|
||||||
|
workflow_results_count: workflows.value.length,
|
||||||
|
collection_results_count: collections.value.length,
|
||||||
|
categories_applied: categories.map((categoryId) =>
|
||||||
|
templatesStore.getCategoryById(categoryId.toString()),
|
||||||
|
) as ITemplatesCategory[],
|
||||||
|
wf_template_repo_session_id: templatesStore.currentSessionId,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const trackCategories = () => {
|
||||||
|
if (categories.value.length) {
|
||||||
|
telemetry.track('User changed template filters', {
|
||||||
|
search_string: search.value,
|
||||||
|
categories_applied: categories.value,
|
||||||
|
wf_template_repo_session_id: templatesStore.currentSessionId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadWorkflowsAndCollections = async (initialLoad: boolean) => {
|
||||||
|
const _categories = [...categories.value];
|
||||||
|
const _search = search.value;
|
||||||
|
await Promise.all([loadWorkflows(), loadCollections()]);
|
||||||
|
if (!initialLoad) {
|
||||||
|
updateSearchTracking(
|
||||||
|
_search,
|
||||||
|
_categories.map((category) => category.id),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const navigateTo = (e: MouseEvent, page: string, id: number) => {
|
||||||
|
if (e.metaKey || e.ctrlKey) {
|
||||||
|
const route = router.resolve({ name: page, params: { id } });
|
||||||
|
window.open(route.href, '_blank');
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
void router.push({ name: page, params: { id } });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onOpenCollection = ({ event, id }: { event: MouseEvent; id: number }) => {
|
||||||
|
navigateTo(event, VIEWS.COLLECTION, id);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onOpenTemplate = ({ event, id }: { event: MouseEvent; id: number }) => {
|
||||||
|
navigateTo(event, VIEWS.TEMPLATE, id);
|
||||||
|
};
|
||||||
|
|
||||||
|
const trackSearch = () => {
|
||||||
|
if (searchEventToTrack.value) {
|
||||||
|
telemetry.track(
|
||||||
|
'User searched workflow templates',
|
||||||
|
searchEventToTrack.value as unknown as IDataObject,
|
||||||
|
);
|
||||||
|
searchEventToTrack.value = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSearchInput = (searchText: string) => {
|
||||||
|
loadingWorkflows.value = true;
|
||||||
|
loadingCollections.value = true;
|
||||||
|
search.value = searchText;
|
||||||
|
void callDebounced(updateSearch, {
|
||||||
|
debounceTime: 500,
|
||||||
|
trailing: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (searchText.length === 0) {
|
||||||
|
trackSearch();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onCategorySelected = (selected: ITemplatesCategory) => {
|
||||||
|
categories.value = categories.value.concat(selected);
|
||||||
|
updateSearch();
|
||||||
|
trackCategories();
|
||||||
|
};
|
||||||
|
|
||||||
|
const onCategoryUnselected = (selected: ITemplatesCategory) => {
|
||||||
|
categories.value = categories.value.filter((category) => category.id !== selected.id);
|
||||||
|
updateSearch();
|
||||||
|
trackCategories();
|
||||||
|
};
|
||||||
|
|
||||||
|
const onCategoriesCleared = () => {
|
||||||
|
categories.value = [];
|
||||||
|
updateSearch();
|
||||||
|
};
|
||||||
|
|
||||||
|
const onLoadMore = async () => {
|
||||||
|
if (workflows.value.length >= totalWorkflows.value) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
this.loadingWorkflows = true;
|
loadingWorkflows.value = true;
|
||||||
await this.templatesStore.getMoreWorkflows({
|
await templatesStore.getMoreWorkflows({
|
||||||
categories: this.categories.map((category) => category.name),
|
categories: categories.value.map((category) => category.name),
|
||||||
search: this.search,
|
search: search.value,
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.showMessage({
|
toast.showMessage({
|
||||||
title: 'Error',
|
title: 'Error',
|
||||||
message: 'Could not load more workflows',
|
message: 'Could not load more workflows',
|
||||||
type: 'error',
|
type: 'error',
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
this.loadingWorkflows = false;
|
loadingWorkflows.value = false;
|
||||||
}
|
}
|
||||||
},
|
};
|
||||||
async loadCategories() {
|
|
||||||
try {
|
|
||||||
await this.templatesStore.getCategories();
|
|
||||||
} catch (e) {}
|
|
||||||
this.loadingCategories = false;
|
|
||||||
},
|
|
||||||
async loadCollections() {
|
|
||||||
try {
|
|
||||||
this.loadingCollections = true;
|
|
||||||
await this.templatesStore.getCollections({
|
|
||||||
categories: this.categories.map((category) => String(category.id)),
|
|
||||||
search: this.search,
|
|
||||||
});
|
|
||||||
} catch (e) {}
|
|
||||||
|
|
||||||
this.loadingCollections = false;
|
const loadCategories = async () => {
|
||||||
},
|
|
||||||
async loadWorkflows() {
|
|
||||||
try {
|
try {
|
||||||
this.loadingWorkflows = true;
|
await templatesStore.getCategories();
|
||||||
await this.templatesStore.getWorkflows({
|
} catch (e) {}
|
||||||
search: this.search,
|
loadingCategories.value = false;
|
||||||
categories: this.categories.map((category) => category.name),
|
};
|
||||||
});
|
|
||||||
this.errorLoadingWorkflows = false;
|
|
||||||
} catch (e) {
|
|
||||||
this.errorLoadingWorkflows = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.loadingWorkflows = false;
|
const scrollTo = (position: number, behavior: ScrollBehavior = 'smooth') => {
|
||||||
},
|
|
||||||
async loadWorkflowsAndCollections(initialLoad: boolean) {
|
|
||||||
const search = this.search;
|
|
||||||
const categories = [...this.categories];
|
|
||||||
await Promise.all([this.loadWorkflows(), this.loadCollections()]);
|
|
||||||
if (!initialLoad) {
|
|
||||||
this.updateSearchTracking(
|
|
||||||
search,
|
|
||||||
categories.map((category) => category.id),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
scrollTo(position: number, behavior: ScrollBehavior = 'smooth') {
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const contentArea = document.getElementById('content');
|
const contentArea = document.getElementById('content');
|
||||||
if (contentArea) {
|
if (contentArea) {
|
||||||
|
@ -332,8 +271,60 @@ export default defineComponent({
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, 0);
|
}, 0);
|
||||||
},
|
};
|
||||||
},
|
|
||||||
|
const restoreSearchFromRoute = () => {
|
||||||
|
let shouldUpdateSearch = false;
|
||||||
|
if (route.query.search && typeof route.query.search === 'string') {
|
||||||
|
search.value = route.query.search;
|
||||||
|
shouldUpdateSearch = true;
|
||||||
|
}
|
||||||
|
if (typeof route.query.categories === 'string' && route.query.categories.length) {
|
||||||
|
const categoriesFromURL = route.query.categories.split(',');
|
||||||
|
categories.value = templatesStore.allCategories.filter((category) =>
|
||||||
|
categoriesFromURL.includes(category.id.toString()),
|
||||||
|
);
|
||||||
|
shouldUpdateSearch = true;
|
||||||
|
}
|
||||||
|
if (shouldUpdateSearch) {
|
||||||
|
updateSearch();
|
||||||
|
trackCategories();
|
||||||
|
areCategoriesPrepopulated.value = true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
documentTitle.set('Templates');
|
||||||
|
await loadCategories();
|
||||||
|
void loadWorkflowsAndCollections(true);
|
||||||
|
void usersStore.showPersonalizationSurvey();
|
||||||
|
|
||||||
|
restoreSearchFromRoute();
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
// Check if there is scroll position saved in route and scroll to it
|
||||||
|
const scrollOffset = route.meta?.scrollOffset;
|
||||||
|
if (typeof scrollOffset === 'number' && scrollOffset > 0) {
|
||||||
|
scrollTo(scrollOffset, 'auto');
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
});
|
||||||
|
|
||||||
|
onBeforeRouteLeave((_to, _from, next) => {
|
||||||
|
const contentArea = document.getElementById('content');
|
||||||
|
if (contentArea) {
|
||||||
|
// When leaving this page, store current scroll position in route data
|
||||||
|
route.meta?.setScrollPosition?.(contentArea.scrollTop);
|
||||||
|
}
|
||||||
|
|
||||||
|
trackSearch();
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(workflows, (newWorkflows) => {
|
||||||
|
if (newWorkflows.length === 0) {
|
||||||
|
window.scrollTo(0, 0);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -343,7 +334,7 @@ export default defineComponent({
|
||||||
<div :class="$style.wrapper">
|
<div :class="$style.wrapper">
|
||||||
<div :class="$style.title">
|
<div :class="$style.title">
|
||||||
<n8n-heading tag="h1" size="2xlarge">
|
<n8n-heading tag="h1" size="2xlarge">
|
||||||
{{ $locale.baseText('templates.heading') }}
|
{{ i18n.baseText('templates.heading') }}
|
||||||
</n8n-heading>
|
</n8n-heading>
|
||||||
</div>
|
</div>
|
||||||
<div :class="$style.button">
|
<div :class="$style.button">
|
||||||
|
@ -351,8 +342,8 @@ export default defineComponent({
|
||||||
size="large"
|
size="large"
|
||||||
type="secondary"
|
type="secondary"
|
||||||
element="a"
|
element="a"
|
||||||
:href="creatorHubUrl"
|
:href="CREATOR_HUB_URL"
|
||||||
:label="$locale.baseText('templates.shareWorkflow')"
|
:label="i18n.baseText('templates.shareWorkflow')"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -374,7 +365,7 @@ export default defineComponent({
|
||||||
<div :class="$style.search">
|
<div :class="$style.search">
|
||||||
<n8n-input
|
<n8n-input
|
||||||
:model-value="search"
|
:model-value="search"
|
||||||
:placeholder="$locale.baseText('templates.searchPlaceholder')"
|
:placeholder="i18n.baseText('templates.searchPlaceholder')"
|
||||||
clearable
|
clearable
|
||||||
data-test-id="template-search-input"
|
data-test-id="template-search-input"
|
||||||
@update:model-value="onSearchInput"
|
@update:model-value="onSearchInput"
|
||||||
|
@ -387,7 +378,7 @@ export default defineComponent({
|
||||||
<div v-show="collections.length || loadingCollections" :class="$style.carouselContainer">
|
<div v-show="collections.length || loadingCollections" :class="$style.carouselContainer">
|
||||||
<div :class="$style.header">
|
<div :class="$style.header">
|
||||||
<n8n-heading :bold="true" size="medium" color="text-light">
|
<n8n-heading :bold="true" size="medium" color="text-light">
|
||||||
{{ $locale.baseText('templates.collections') }}
|
{{ i18n.baseText('templates.collections') }}
|
||||||
<span
|
<span
|
||||||
v-if="!loadingCollections"
|
v-if="!loadingCollections"
|
||||||
data-test-id="collection-count-label"
|
data-test-id="collection-count-label"
|
||||||
|
|
Loading…
Reference in a new issue