mirror of
https://github.com/n8n-io/n8n.git
synced 2024-12-25 04:34:06 -08:00
refactor(editor): Fix type errors in ResourcesListLayout.vue (no-changelog) (#9461)
This commit is contained in:
parent
87f965e905
commit
75919397d6
|
@ -1,8 +1,6 @@
|
|||
<template>
|
||||
<PageViewLayout>
|
||||
<template #header>
|
||||
<slot name="header" />
|
||||
</template>
|
||||
<template #header> <slot name="header" /> </template>
|
||||
<div v-if="loading">
|
||||
<n8n-loading :class="[$style['header-loading'], 'mb-l']" variant="custom" />
|
||||
<n8n-loading :class="[$style['card-loading'], 'mb-2xs']" variant="custom" />
|
||||
|
@ -16,18 +14,18 @@
|
|||
emoji="👋"
|
||||
:heading="
|
||||
i18n.baseText(
|
||||
usersStore.currentUser.firstName
|
||||
? `${resourceKey}.empty.heading`
|
||||
: `${resourceKey}.empty.heading.userNotSetup`,
|
||||
usersStore.currentUser?.firstName
|
||||
? (`${resourceKey}.empty.heading` as BaseTextKey)
|
||||
: (`${resourceKey}.empty.heading.userNotSetup` as BaseTextKey),
|
||||
{
|
||||
interpolate: { name: usersStore.currentUser.firstName },
|
||||
interpolate: { name: usersStore.currentUser?.firstName ?? '' },
|
||||
},
|
||||
)
|
||||
"
|
||||
:description="i18n.baseText(`${resourceKey}.empty.description`)"
|
||||
:button-text="i18n.baseText(`${resourceKey}.empty.button`)"
|
||||
:description="i18n.baseText(`${resourceKey}.empty.description` as BaseTextKey)"
|
||||
:button-text="i18n.baseText(`${resourceKey}.empty.button` as BaseTextKey)"
|
||||
button-type="secondary"
|
||||
@click:button="$emit('click:add', $event)"
|
||||
@click:button="onAddButtonClick"
|
||||
/>
|
||||
</slot>
|
||||
</div>
|
||||
|
@ -39,7 +37,7 @@
|
|||
ref="search"
|
||||
:model-value="filtersModel.search"
|
||||
:class="[$style['search'], 'mr-2xs']"
|
||||
:placeholder="i18n.baseText(`${resourceKey}.search.placeholder`)"
|
||||
:placeholder="i18n.baseText(`${resourceKey}.search.placeholder` as BaseTextKey)"
|
||||
clearable
|
||||
data-test-id="resources-list-search"
|
||||
@update:model-value="onSearch"
|
||||
|
@ -54,7 +52,7 @@
|
|||
:reset="resetFilters"
|
||||
:model-value="filtersModel"
|
||||
:shareable="shareable"
|
||||
@update:model-value="$emit('update:filters', $event)"
|
||||
@update:model-value="onUpdateFilters"
|
||||
@update:filters-length="onUpdateFiltersLength"
|
||||
>
|
||||
<template #default="resourceFiltersSlotProps">
|
||||
|
@ -68,7 +66,7 @@
|
|||
:key="sortOption"
|
||||
data-test-id="resources-list-sort-item"
|
||||
:value="sortOption"
|
||||
:label="i18n.baseText(`${resourceKey}.sort.${sortOption}`)"
|
||||
:label="i18n.baseText(`${resourceKey}.sort.${sortOption}` as BaseTextKey)"
|
||||
/>
|
||||
</n8n-select>
|
||||
</div>
|
||||
|
@ -78,9 +76,9 @@
|
|||
size="large"
|
||||
:disabled="disabled"
|
||||
data-test-id="resources-list-add"
|
||||
@click="$emit('click:add', $event)"
|
||||
@click="onAddButtonClick"
|
||||
>
|
||||
{{ i18n.baseText(`${resourceKey}.add`) }}
|
||||
{{ i18n.baseText(`${resourceKey}.add` as BaseTextKey) }}
|
||||
</n8n-button>
|
||||
</slot>
|
||||
</div>
|
||||
|
@ -89,9 +87,9 @@
|
|||
|
||||
<div v-if="showFiltersDropdown" v-show="hasFilters" class="mt-xs">
|
||||
<n8n-info-tip :bold="false">
|
||||
{{ i18n.baseText(`${resourceKey}.filters.active`) }}
|
||||
{{ i18n.baseText(`${resourceKey}.filters.active` as BaseTextKey) }}
|
||||
<n8n-link data-test-id="workflows-filter-reset" size="small" @click="resetFilters">
|
||||
{{ i18n.baseText(`${resourceKey}.filters.active.reset`) }}
|
||||
{{ i18n.baseText(`${resourceKey}.filters.active.reset` as BaseTextKey) }}
|
||||
</n8n-link>
|
||||
</n8n-info-tip>
|
||||
</div>
|
||||
|
@ -110,7 +108,7 @@
|
|||
v-if="type === 'list'"
|
||||
data-test-id="resources-list"
|
||||
:items="filteredAndSortedResources"
|
||||
:item-size="typeProps.itemSize"
|
||||
:item-size="itemSize()"
|
||||
item-key="id"
|
||||
>
|
||||
<template #default="{ item, updateItemSize }">
|
||||
|
@ -121,10 +119,10 @@
|
|||
</template>
|
||||
</n8n-recycle-scroller>
|
||||
<n8n-datatable
|
||||
v-if="typeProps.columns"
|
||||
v-if="type === 'datatable'"
|
||||
data-test-id="resources-table"
|
||||
:class="$style.datatable"
|
||||
:columns="typeProps.columns"
|
||||
:columns="getColumns()"
|
||||
:rows="filteredAndSortedResources"
|
||||
:current-page="currentPage"
|
||||
:rows-per-page="rowsPerPage"
|
||||
|
@ -138,7 +136,7 @@
|
|||
</div>
|
||||
|
||||
<n8n-text v-else color="text-base" size="medium" data-test-id="resources-list-empty">
|
||||
{{ i18n.baseText(`${resourceKey}.noResults`) }}
|
||||
{{ i18n.baseText(`${resourceKey}.noResults` as BaseTextKey) }}
|
||||
</n8n-text>
|
||||
|
||||
<slot name="postamble" />
|
||||
|
@ -148,20 +146,22 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import { computed, defineComponent, nextTick, ref, onMounted, watch } from 'vue';
|
||||
import type { PropType } from 'vue';
|
||||
import { mapStores } from 'pinia';
|
||||
|
||||
import type { ProjectSharingData } from '@/features/projects/projects.types';
|
||||
import PageViewLayout from '@/components/layouts/PageViewLayout.vue';
|
||||
import PageViewLayoutList from '@/components/layouts/PageViewLayoutList.vue';
|
||||
import { EnterpriseEditionFeature } from '@/constants';
|
||||
import ResourceFiltersDropdown from '@/components/forms/ResourceFiltersDropdown.vue';
|
||||
import { useSettingsStore } from '@/stores/settings.store';
|
||||
import { useUsersStore } from '@/stores/users.store';
|
||||
import type { N8nInput, DatatableColumn } from 'n8n-design-system';
|
||||
import type { DatatableColumn } from 'n8n-design-system';
|
||||
import { useI18n } from '@/composables/useI18n';
|
||||
import { useDebounce } from '@/composables/useDebounce';
|
||||
import { useTelemetry } from '@/composables/useTelemetry';
|
||||
import { useRoute } from 'vue-router';
|
||||
|
||||
// eslint-disable-next-line unused-imports/no-unused-imports, @typescript-eslint/no-unused-vars
|
||||
import type { BaseTextKey } from '@/plugins/i18n';
|
||||
|
||||
export interface IResource {
|
||||
id: string;
|
||||
|
@ -178,7 +178,6 @@ interface IFilters {
|
|||
}
|
||||
|
||||
type IResourceKeyType = 'credentials' | 'workflows';
|
||||
type SearchRef = InstanceType<typeof N8nInput>;
|
||||
|
||||
export default defineComponent({
|
||||
name: 'ResourcesListLayout',
|
||||
|
@ -197,7 +196,7 @@ export default defineComponent({
|
|||
default: (resource: IResource) => resource.name,
|
||||
},
|
||||
resources: {
|
||||
type: Array,
|
||||
type: Array as PropType<IResource[]>,
|
||||
default: (): IResource[] => [],
|
||||
},
|
||||
disabled: {
|
||||
|
@ -244,162 +243,113 @@ export default defineComponent({
|
|||
}),
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
emits: ['update:filters', 'click:add', 'sort'],
|
||||
setup(props, { emit }) {
|
||||
const route = useRoute();
|
||||
const i18n = useI18n();
|
||||
const { callDebounced } = useDebounce();
|
||||
const usersStore = useUsersStore();
|
||||
const telemetry = useTelemetry();
|
||||
|
||||
return {
|
||||
i18n,
|
||||
callDebounced,
|
||||
};
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loading: true,
|
||||
sortBy: this.sortOptions[0],
|
||||
hasFilters: false,
|
||||
filtersModel: { ...this.filters },
|
||||
currentPage: 1,
|
||||
rowsPerPage: 10 as number | '*',
|
||||
resettingFilters: false,
|
||||
EnterpriseEditionFeature,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapStores(useSettingsStore, useUsersStore),
|
||||
filterKeys(): string[] {
|
||||
return Object.keys(this.filtersModel);
|
||||
},
|
||||
filteredAndSortedResources(): IResource[] {
|
||||
const filtered: IResource[] = this.resources.filter((resource: IResource) => {
|
||||
const loading = ref(true);
|
||||
const sortBy = ref(props.sortOptions[0]);
|
||||
const hasFilters = ref(false);
|
||||
const filtersModel = ref(props.filters);
|
||||
const currentPage = ref(1);
|
||||
const rowsPerPage = ref<number | '*'>(10);
|
||||
const resettingFilters = ref(false);
|
||||
const search = ref<HTMLElement | null>(null);
|
||||
|
||||
//computed
|
||||
|
||||
const filterKeys = computed(() => {
|
||||
return Object.keys(filtersModel.value);
|
||||
});
|
||||
|
||||
const filteredAndSortedResources = computed(() => {
|
||||
const filtered = props.resources.filter((resource) => {
|
||||
let matches = true;
|
||||
|
||||
if (this.filtersModel.homeProject) {
|
||||
if (filtersModel.value.homeProject) {
|
||||
matches =
|
||||
matches &&
|
||||
!!(resource.homeProject && resource.homeProject.id === this.filtersModel.homeProject);
|
||||
!!(resource.homeProject && resource.homeProject.id === filtersModel.value.homeProject);
|
||||
}
|
||||
|
||||
if (this.filtersModel.search) {
|
||||
const searchString = this.filtersModel.search.toLowerCase();
|
||||
|
||||
matches = matches && this.displayName(resource).toLowerCase().includes(searchString);
|
||||
if (filtersModel.value.search) {
|
||||
const searchString = filtersModel.value.search.toLowerCase();
|
||||
matches = matches && props.displayName(resource).toLowerCase().includes(searchString);
|
||||
}
|
||||
|
||||
if (this.additionalFiltersHandler) {
|
||||
matches = this.additionalFiltersHandler(resource, this.filtersModel, matches);
|
||||
if (props.additionalFiltersHandler) {
|
||||
matches = props.additionalFiltersHandler(resource, filtersModel.value, matches);
|
||||
}
|
||||
|
||||
return matches;
|
||||
});
|
||||
|
||||
return filtered.sort((a, b) => {
|
||||
switch (this.sortBy) {
|
||||
switch (sortBy.value) {
|
||||
case 'lastUpdated':
|
||||
return this.sortFns.lastUpdated
|
||||
? this.sortFns.lastUpdated(a, b)
|
||||
return props.sortFns.lastUpdated
|
||||
? props.sortFns.lastUpdated(a, b)
|
||||
: new Date(b.updatedAt).valueOf() - new Date(a.updatedAt).valueOf();
|
||||
case 'lastCreated':
|
||||
return this.sortFns.lastCreated
|
||||
? this.sortFns.lastCreated(a, b)
|
||||
return props.sortFns.lastCreated
|
||||
? props.sortFns.lastCreated(a, b)
|
||||
: new Date(b.createdAt).valueOf() - new Date(a.createdAt).valueOf();
|
||||
case 'nameAsc':
|
||||
return this.sortFns.nameAsc
|
||||
? this.sortFns.nameAsc(a, b)
|
||||
: this.displayName(a).trim().localeCompare(this.displayName(b).trim());
|
||||
return props.sortFns.nameAsc
|
||||
? props.sortFns.nameAsc(a, b)
|
||||
: props.displayName(a).trim().localeCompare(props.displayName(b).trim());
|
||||
case 'nameDesc':
|
||||
return this.sortFns.nameDesc
|
||||
? this.sortFns.nameDesc(a, b)
|
||||
: this.displayName(b).trim().localeCompare(this.displayName(a).trim());
|
||||
return props.sortFns.nameDesc
|
||||
? props.sortFns.nameDesc(a, b)
|
||||
: props.displayName(b).trim().localeCompare(props.displayName(a).trim());
|
||||
default:
|
||||
return this.sortFns[this.sortBy] ? this.sortFns[this.sortBy](a, b) : 0;
|
||||
return props.sortFns[sortBy.value] ? props.sortFns[sortBy.value](a, b) : 0;
|
||||
}
|
||||
});
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
filters(value) {
|
||||
this.filtersModel = value;
|
||||
},
|
||||
'filtersModel.homeProject'() {
|
||||
this.sendFiltersTelemetry('homeProject');
|
||||
},
|
||||
'filtersModel.search'() {
|
||||
void this.callDebounced(
|
||||
this.sendFiltersTelemetry,
|
||||
{ debounceTime: 1000, trailing: true },
|
||||
'search',
|
||||
);
|
||||
},
|
||||
sortBy(newValue) {
|
||||
this.$emit('sort', newValue);
|
||||
this.sendSortingTelemetry();
|
||||
},
|
||||
'$route.params.projectId'() {
|
||||
this.resetFilters();
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
void this.onMounted();
|
||||
},
|
||||
methods: {
|
||||
async onMounted() {
|
||||
await this.initialize();
|
||||
});
|
||||
|
||||
this.loading = false;
|
||||
await this.$nextTick();
|
||||
this.focusSearchInput();
|
||||
//methods
|
||||
|
||||
if (this.hasAppliedFilters()) {
|
||||
this.hasFilters = true;
|
||||
const focusSearchInput = () => {
|
||||
if (search.value) {
|
||||
search.value.focus();
|
||||
}
|
||||
},
|
||||
hasAppliedFilters(): boolean {
|
||||
return !!this.filterKeys.find(
|
||||
};
|
||||
|
||||
const hasAppliedFilters = (): boolean => {
|
||||
return !!filterKeys.value.find(
|
||||
(key) =>
|
||||
key !== 'search' &&
|
||||
(Array.isArray(this.filters[key])
|
||||
? this.filters[key].length > 0
|
||||
: this.filters[key] !== ''),
|
||||
(Array.isArray(props.filters[key])
|
||||
? props.filters[key].length > 0
|
||||
: props.filters[key] !== ''),
|
||||
);
|
||||
},
|
||||
setCurrentPage(page: number) {
|
||||
this.currentPage = page;
|
||||
},
|
||||
setRowsPerPage(rowsPerPage: number | '*') {
|
||||
this.rowsPerPage = rowsPerPage;
|
||||
},
|
||||
resetFilters() {
|
||||
Object.keys(this.filtersModel).forEach((key) => {
|
||||
this.filtersModel[key] = Array.isArray(this.filtersModel[key]) ? [] : '';
|
||||
});
|
||||
};
|
||||
|
||||
this.resettingFilters = true;
|
||||
this.sendFiltersTelemetry('reset');
|
||||
this.$emit('update:filters', this.filtersModel);
|
||||
},
|
||||
focusSearchInput() {
|
||||
if (this.$refs.search) {
|
||||
(this.$refs.search as SearchRef).focus();
|
||||
}
|
||||
},
|
||||
sendSortingTelemetry() {
|
||||
this.$telemetry.track(`User changed sorting in ${this.resourceKey} list`, {
|
||||
sorting: this.sortBy,
|
||||
});
|
||||
},
|
||||
sendFiltersTelemetry(source: string) {
|
||||
const setRowsPerPage = (numberOfRowsPerPage: number | '*') => {
|
||||
rowsPerPage.value = numberOfRowsPerPage;
|
||||
};
|
||||
|
||||
const setCurrentPage = (page: number) => {
|
||||
currentPage.value = page;
|
||||
};
|
||||
|
||||
const sendFiltersTelemetry = (source: string) => {
|
||||
// Prevent sending multiple telemetry events when resetting filters
|
||||
// Timeout is required to wait for search debounce to be over
|
||||
if (this.resettingFilters) {
|
||||
if (resettingFilters.value) {
|
||||
if (source !== 'reset') {
|
||||
return;
|
||||
}
|
||||
|
||||
setTimeout(() => (this.resettingFilters = false), 1500);
|
||||
setTimeout(() => (resettingFilters.value = false), 1500);
|
||||
}
|
||||
|
||||
const filters = this.filtersModel as Record<string, string[] | string | boolean>;
|
||||
const filters = filtersModel.value as Record<string, string[] | string | boolean>;
|
||||
const filtersSet: string[] = [];
|
||||
const filterValues: Array<string[] | string | boolean | null> = [];
|
||||
|
||||
|
@ -410,20 +360,134 @@ export default defineComponent({
|
|||
}
|
||||
});
|
||||
|
||||
this.$telemetry.track(`User set filters in ${this.resourceKey} list`, {
|
||||
telemetry.track(`User set filters in ${props.resourceKey} list`, {
|
||||
filters_set: filtersSet,
|
||||
filter_values: filterValues,
|
||||
[`${this.resourceKey}_total_in_view`]: this.resources.length,
|
||||
[`${this.resourceKey}_after_filtering`]: this.filteredAndSortedResources.length,
|
||||
[`${props.resourceKey}_total_in_view`]: props.resources.length,
|
||||
[`${props.resourceKey}_after_filtering`]: filteredAndSortedResources.value.length,
|
||||
});
|
||||
};
|
||||
|
||||
const onAddButtonClick = (e: Event) => {
|
||||
emit('click:add', e);
|
||||
};
|
||||
|
||||
const onUpdateFilters = (e: Event) => {
|
||||
emit('update:filters', e);
|
||||
};
|
||||
|
||||
const resetFilters = () => {
|
||||
Object.keys(filtersModel.value).forEach((key) => {
|
||||
filtersModel.value[key] = Array.isArray(filtersModel.value[key]) ? [] : '';
|
||||
});
|
||||
|
||||
resettingFilters.value = true;
|
||||
sendFiltersTelemetry('reset');
|
||||
emit('update:filters', filtersModel.value);
|
||||
};
|
||||
|
||||
const itemSize = () => {
|
||||
if ('itemSize' in props.typeProps) {
|
||||
return props.typeProps.itemSize;
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
const getColumns = () => {
|
||||
if ('columns' in props.typeProps) {
|
||||
return props.typeProps.columns;
|
||||
}
|
||||
return {};
|
||||
};
|
||||
|
||||
const sendSortingTelemetry = () => {
|
||||
telemetry.track(`User changed sorting in ${props.resourceKey} list`, {
|
||||
sorting: sortBy.value,
|
||||
});
|
||||
};
|
||||
|
||||
const onUpdateFiltersLength = (length: number) => {
|
||||
hasFilters.value = length > 0;
|
||||
};
|
||||
|
||||
const onSearch = (s: string) => {
|
||||
filtersModel.value.search = s;
|
||||
emit('update:filters', filtersModel.value);
|
||||
};
|
||||
|
||||
//watchers
|
||||
|
||||
watch(
|
||||
() => props.filters,
|
||||
(value) => {
|
||||
filtersModel.value = value;
|
||||
},
|
||||
onUpdateFiltersLength(length: number) {
|
||||
this.hasFilters = length > 0;
|
||||
);
|
||||
|
||||
watch(
|
||||
() => filtersModel.value.homeProject,
|
||||
() => {
|
||||
sendFiltersTelemetry('homeProject');
|
||||
},
|
||||
onSearch(search: string) {
|
||||
this.filtersModel.search = search;
|
||||
this.$emit('update:filters', this.filtersModel);
|
||||
);
|
||||
|
||||
watch(
|
||||
() => filtersModel.value.search,
|
||||
() => callDebounced(sendFiltersTelemetry, { debounceTime: 1000, trailing: true }, 'search'),
|
||||
);
|
||||
|
||||
watch(
|
||||
() => sortBy.value,
|
||||
(newValue) => {
|
||||
emit('sort', newValue);
|
||||
sendSortingTelemetry();
|
||||
},
|
||||
);
|
||||
|
||||
watch(
|
||||
() => route?.params?.projectId,
|
||||
() => {
|
||||
resetFilters();
|
||||
},
|
||||
);
|
||||
|
||||
onMounted(async () => {
|
||||
await props.initialize();
|
||||
loading.value = false;
|
||||
await nextTick();
|
||||
|
||||
focusSearchInput();
|
||||
|
||||
if (hasAppliedFilters()) {
|
||||
hasFilters.value = true;
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
loading,
|
||||
i18n,
|
||||
search,
|
||||
usersStore,
|
||||
filterKeys,
|
||||
currentPage,
|
||||
rowsPerPage,
|
||||
filteredAndSortedResources,
|
||||
hasFilters,
|
||||
sortBy,
|
||||
resettingFilters,
|
||||
filtersModel,
|
||||
sendFiltersTelemetry,
|
||||
getColumns,
|
||||
itemSize,
|
||||
onAddButtonClick,
|
||||
onUpdateFiltersLength,
|
||||
onUpdateFilters,
|
||||
resetFilters,
|
||||
callDebounced,
|
||||
setCurrentPage,
|
||||
setRowsPerPage,
|
||||
onSearch,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -6,13 +6,16 @@ export interface DebounceOptions {
|
|||
trailing?: boolean;
|
||||
}
|
||||
|
||||
export type DebouncedFunction<R = void> = (...args: unknown[]) => R;
|
||||
export type DebouncedFunction<Args extends unknown[] = unknown[], R = void> = (...args: Args) => R;
|
||||
|
||||
export function useDebounce() {
|
||||
// Create a ref for the WeakMap to store debounced functions.
|
||||
const debounceCache = ref(new WeakMap<DebouncedFunction<unknown>, DebouncedFunction<unknown>>());
|
||||
const debounceCache = ref(
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
new WeakMap<DebouncedFunction<any, any>, DebouncedFunction<any, any>>(),
|
||||
);
|
||||
|
||||
const debounce = <T extends DebouncedFunction<ReturnType<T>>>(
|
||||
const debounce = <T extends DebouncedFunction<Parameters<T>, ReturnType<T>>>(
|
||||
fn: T,
|
||||
options: DebounceOptions,
|
||||
): T => {
|
||||
|
@ -24,7 +27,7 @@ export function useDebounce() {
|
|||
// If a debounced version is not found, create one and store it in the WeakMap.
|
||||
if (debouncedFn === undefined) {
|
||||
debouncedFn = _debounce(
|
||||
async (...args: unknown[]) => {
|
||||
async (...args: Parameters<T>) => {
|
||||
return fn(...args);
|
||||
},
|
||||
debounceTime,
|
||||
|
@ -37,7 +40,7 @@ export function useDebounce() {
|
|||
return debouncedFn as T;
|
||||
};
|
||||
|
||||
const callDebounced = <T extends DebouncedFunction<ReturnType<T>>>(
|
||||
const callDebounced = <T extends DebouncedFunction<Parameters<T>, ReturnType<T>>>(
|
||||
fn: T,
|
||||
options: DebounceOptions,
|
||||
...inputParameters: Parameters<T>
|
||||
|
|
Loading…
Reference in a new issue