mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -08:00
refactor(editor): ResourceListLayout to script setup (#11526)
This commit is contained in:
parent
e10968b26f
commit
5f3deea60f
|
@ -1,6 +1,5 @@
|
||||||
<script lang="ts">
|
<script lang="ts" setup>
|
||||||
import { computed, defineComponent, nextTick, ref, onMounted, watch } from 'vue';
|
import { computed, nextTick, ref, onMounted, watch } from 'vue';
|
||||||
import type { PropType } from 'vue';
|
|
||||||
|
|
||||||
import { type ProjectSharingData, ProjectTypes } from '@/types/projects.types';
|
import { type ProjectSharingData, ProjectTypes } from '@/types/projects.types';
|
||||||
import PageViewLayout from '@/components/layouts/PageViewLayout.vue';
|
import PageViewLayout from '@/components/layouts/PageViewLayout.vue';
|
||||||
|
@ -15,7 +14,6 @@ import { useTelemetry } from '@/composables/useTelemetry';
|
||||||
import { useRoute } from 'vue-router';
|
import { useRoute } from 'vue-router';
|
||||||
import { useProjectsStore } from '@/stores/projects.store';
|
import { useProjectsStore } from '@/stores/projects.store';
|
||||||
|
|
||||||
// eslint-disable-next-line unused-imports/no-unused-imports, @typescript-eslint/no-unused-vars
|
|
||||||
import type { BaseTextKey } from '@/plugins/i18n';
|
import type { BaseTextKey } from '@/plugins/i18n';
|
||||||
import type { Scope } from '@n8n/permissions';
|
import type { Scope } from '@n8n/permissions';
|
||||||
|
|
||||||
|
@ -32,365 +30,325 @@ export type IResource = {
|
||||||
sharedWithProjects?: ProjectSharingData[];
|
sharedWithProjects?: ProjectSharingData[];
|
||||||
};
|
};
|
||||||
|
|
||||||
interface IFilters {
|
export interface IFilters {
|
||||||
search: string;
|
search: string;
|
||||||
homeProject: string;
|
homeProject: string;
|
||||||
[key: string]: boolean | string | string[];
|
[key: string]: boolean | string | string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
type IResourceKeyType = 'credentials' | 'workflows';
|
type IResourceKeyType = 'credentials' | 'workflows' | 'variables';
|
||||||
|
|
||||||
export default defineComponent({
|
const props = withDefaults(
|
||||||
name: 'ResourcesListLayout',
|
defineProps<{
|
||||||
components: {
|
resourceKey: IResourceKeyType;
|
||||||
PageViewLayout,
|
displayName?: (resource: IResource) => string;
|
||||||
PageViewLayoutList,
|
resources: IResource[];
|
||||||
ResourceFiltersDropdown,
|
disabled: boolean;
|
||||||
ResourceListHeader,
|
initialize: () => Promise<void>;
|
||||||
|
filters?: IFilters;
|
||||||
|
additionalFiltersHandler?: (
|
||||||
|
resource: IResource,
|
||||||
|
filters: IFilters,
|
||||||
|
matches: boolean,
|
||||||
|
) => boolean;
|
||||||
|
shareable?: boolean;
|
||||||
|
showFiltersDropdown?: boolean;
|
||||||
|
sortFns?: Record<string, (a: IResource, b: IResource) => number>;
|
||||||
|
sortOptions?: string[];
|
||||||
|
type?: 'datatable' | 'list';
|
||||||
|
typeProps: { itemSize: number } | { columns: DatatableColumn[] };
|
||||||
|
loading: boolean;
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
displayName: (resource: IResource) => resource.name,
|
||||||
|
initialize: async () => {},
|
||||||
|
filters: () => ({ search: '', homeProject: '' }),
|
||||||
|
sortFns: () => ({}),
|
||||||
|
sortOptions: () => ['lastUpdated', 'lastCreated', 'nameAsc', 'nameDesc'],
|
||||||
|
type: 'list',
|
||||||
|
typeProps: () => ({ itemSize: 80 }),
|
||||||
|
loading: true,
|
||||||
|
additionalFiltersHandler: undefined,
|
||||||
|
showFiltersDropdown: true,
|
||||||
|
shareable: true,
|
||||||
},
|
},
|
||||||
props: {
|
);
|
||||||
resourceKey: {
|
|
||||||
type: String,
|
const emit = defineEmits<{
|
||||||
default: '' as IResourceKeyType,
|
'update:filters': [value: IFilters];
|
||||||
},
|
'click:add': [event: Event];
|
||||||
displayName: {
|
sort: [value: string];
|
||||||
type: Function as PropType<(resource: IResource) => string>,
|
}>();
|
||||||
default: (resource: IResource) => resource.name,
|
|
||||||
},
|
defineSlots<{
|
||||||
resources: {
|
header(): unknown;
|
||||||
type: Array as PropType<IResource[]>,
|
empty(): unknown;
|
||||||
default: (): IResource[] => [],
|
preamble(): unknown;
|
||||||
},
|
postamble(): unknown;
|
||||||
disabled: {
|
'add-button'(props: { disabled: boolean }): unknown;
|
||||||
type: Boolean,
|
callout(): unknown;
|
||||||
default: false,
|
filters(props: {
|
||||||
},
|
filters: Record<string, boolean | string | string[]>;
|
||||||
initialize: {
|
setKeyValue: (key: string, value: unknown) => void;
|
||||||
type: Function as PropType<() => Promise<void>>,
|
}): unknown;
|
||||||
default: () => async () => {},
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
},
|
default(props: { data: any; updateItemSize: (data: any) => void }): unknown;
|
||||||
filters: {
|
}>();
|
||||||
type: Object,
|
|
||||||
default: (): IFilters => ({ search: '', homeProject: '' }),
|
const route = useRoute();
|
||||||
},
|
const i18n = useI18n();
|
||||||
additionalFiltersHandler: {
|
const { callDebounced } = useDebounce();
|
||||||
type: Function,
|
const usersStore = useUsersStore();
|
||||||
required: false,
|
const projectsStore = useProjectsStore();
|
||||||
default: undefined,
|
const telemetry = useTelemetry();
|
||||||
},
|
|
||||||
shareable: {
|
const sortBy = ref(props.sortOptions[0]);
|
||||||
type: Boolean,
|
const hasFilters = ref(false);
|
||||||
default: true,
|
const filtersModel = ref(props.filters);
|
||||||
},
|
const currentPage = ref(1);
|
||||||
showFiltersDropdown: {
|
const rowsPerPage = ref<number>(10);
|
||||||
type: Boolean,
|
const resettingFilters = ref(false);
|
||||||
default: true,
|
const search = ref<HTMLElement | null>(null);
|
||||||
},
|
|
||||||
sortFns: {
|
//computed
|
||||||
type: Object as PropType<Record<string, (a: IResource, b: IResource) => number>>,
|
|
||||||
default: (): Record<string, (a: IResource, b: IResource) => number> => ({}),
|
const filterKeys = computed(() => {
|
||||||
},
|
return Object.keys(filtersModel.value);
|
||||||
sortOptions: {
|
});
|
||||||
type: Array as PropType<string[]>,
|
|
||||||
default: () => ['lastUpdated', 'lastCreated', 'nameAsc', 'nameDesc'],
|
const filteredAndSortedResources = computed(() => {
|
||||||
},
|
const filtered = props.resources.filter((resource) => {
|
||||||
type: {
|
let matches = true;
|
||||||
type: String as PropType<'datatable' | 'list'>,
|
|
||||||
default: 'list',
|
if (filtersModel.value.homeProject) {
|
||||||
},
|
matches =
|
||||||
typeProps: {
|
matches &&
|
||||||
type: Object as PropType<{ itemSize: number } | { columns: DatatableColumn[] }>,
|
!!(resource.homeProject && resource.homeProject.id === filtersModel.value.homeProject);
|
||||||
default: () => ({
|
}
|
||||||
itemSize: 80,
|
|
||||||
}),
|
if (filtersModel.value.search) {
|
||||||
},
|
const searchString = filtersModel.value.search.toLowerCase();
|
||||||
loading: {
|
matches = matches && props.displayName(resource).toLowerCase().includes(searchString);
|
||||||
type: Boolean,
|
}
|
||||||
required: false,
|
|
||||||
default: true,
|
if (props.additionalFiltersHandler) {
|
||||||
},
|
matches = props.additionalFiltersHandler(resource, filtersModel.value, matches);
|
||||||
|
}
|
||||||
|
|
||||||
|
return matches;
|
||||||
|
});
|
||||||
|
|
||||||
|
return filtered.sort((a, b) => {
|
||||||
|
switch (sortBy.value) {
|
||||||
|
case 'lastUpdated':
|
||||||
|
return props.sortFns.lastUpdated
|
||||||
|
? props.sortFns.lastUpdated(a, b)
|
||||||
|
: new Date(b.updatedAt ?? '').valueOf() - new Date(a.updatedAt ?? '').valueOf();
|
||||||
|
case 'lastCreated':
|
||||||
|
return props.sortFns.lastCreated
|
||||||
|
? props.sortFns.lastCreated(a, b)
|
||||||
|
: new Date(b.createdAt ?? '').valueOf() - new Date(a.createdAt ?? '').valueOf();
|
||||||
|
case 'nameAsc':
|
||||||
|
return props.sortFns.nameAsc
|
||||||
|
? props.sortFns.nameAsc(a, b)
|
||||||
|
: props.displayName(a).trim().localeCompare(props.displayName(b).trim());
|
||||||
|
case 'nameDesc':
|
||||||
|
return props.sortFns.nameDesc
|
||||||
|
? props.sortFns.nameDesc(a, b)
|
||||||
|
: props.displayName(b).trim().localeCompare(props.displayName(a).trim());
|
||||||
|
default:
|
||||||
|
return props.sortFns[sortBy.value] ? props.sortFns[sortBy.value](a, b) : 0;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
//methods
|
||||||
|
|
||||||
|
const focusSearchInput = () => {
|
||||||
|
if (search.value) {
|
||||||
|
search.value.focus();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const hasAppliedFilters = (): boolean => {
|
||||||
|
return !!filterKeys.value.find(
|
||||||
|
(key) =>
|
||||||
|
key !== 'search' &&
|
||||||
|
(Array.isArray(props.filters[key])
|
||||||
|
? props.filters[key].length > 0
|
||||||
|
: props.filters[key] !== ''),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const setRowsPerPage = (numberOfRowsPerPage: number) => {
|
||||||
|
rowsPerPage.value = numberOfRowsPerPage;
|
||||||
|
};
|
||||||
|
|
||||||
|
const setCurrentPage = (page: number) => {
|
||||||
|
currentPage.value = page;
|
||||||
|
};
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
currentPage,
|
||||||
|
setCurrentPage,
|
||||||
|
});
|
||||||
|
|
||||||
|
const sendFiltersTelemetry = (source: string) => {
|
||||||
|
// Prevent sending multiple telemetry events when resetting filters
|
||||||
|
// Timeout is required to wait for search debounce to be over
|
||||||
|
if (resettingFilters.value) {
|
||||||
|
if (source !== 'reset') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(() => (resettingFilters.value = false), 1500);
|
||||||
|
}
|
||||||
|
|
||||||
|
const filters = filtersModel.value as Record<string, string[] | string | boolean>;
|
||||||
|
const filtersSet: string[] = [];
|
||||||
|
const filterValues: Array<string[] | string | boolean | null> = [];
|
||||||
|
|
||||||
|
Object.keys(filters).forEach((key) => {
|
||||||
|
if (filters[key]) {
|
||||||
|
filtersSet.push(key);
|
||||||
|
filterValues.push(key === 'search' ? null : filters[key]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
telemetry.track(`User set filters in ${props.resourceKey} list`, {
|
||||||
|
filters_set: filtersSet,
|
||||||
|
filter_values: filterValues,
|
||||||
|
[`${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: IFilters) => {
|
||||||
|
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;
|
||||||
},
|
},
|
||||||
emits: ['update:filters', 'click:add', 'sort'],
|
);
|
||||||
setup(props, { emit }) {
|
|
||||||
const route = useRoute();
|
|
||||||
const i18n = useI18n();
|
|
||||||
const { callDebounced } = useDebounce();
|
|
||||||
const usersStore = useUsersStore();
|
|
||||||
const projectsStore = useProjectsStore();
|
|
||||||
const telemetry = useTelemetry();
|
|
||||||
|
|
||||||
const sortBy = ref(props.sortOptions[0]);
|
watch(
|
||||||
const hasFilters = ref(false);
|
() => filtersModel.value.homeProject,
|
||||||
const filtersModel = ref(props.filters);
|
() => {
|
||||||
const currentPage = ref(1);
|
sendFiltersTelemetry('homeProject');
|
||||||
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 (filtersModel.value.homeProject) {
|
|
||||||
matches =
|
|
||||||
matches &&
|
|
||||||
!!(resource.homeProject && resource.homeProject.id === filtersModel.value.homeProject);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (filtersModel.value.search) {
|
|
||||||
const searchString = filtersModel.value.search.toLowerCase();
|
|
||||||
matches = matches && props.displayName(resource).toLowerCase().includes(searchString);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (props.additionalFiltersHandler) {
|
|
||||||
matches = props.additionalFiltersHandler(resource, filtersModel.value, matches);
|
|
||||||
}
|
|
||||||
|
|
||||||
return matches;
|
|
||||||
});
|
|
||||||
|
|
||||||
return filtered.sort((a, b) => {
|
|
||||||
switch (sortBy.value) {
|
|
||||||
case 'lastUpdated':
|
|
||||||
return props.sortFns.lastUpdated
|
|
||||||
? props.sortFns.lastUpdated(a, b)
|
|
||||||
: new Date(b.updatedAt ?? '').valueOf() - new Date(a.updatedAt ?? '').valueOf();
|
|
||||||
case 'lastCreated':
|
|
||||||
return props.sortFns.lastCreated
|
|
||||||
? props.sortFns.lastCreated(a, b)
|
|
||||||
: new Date(b.createdAt ?? '').valueOf() - new Date(a.createdAt ?? '').valueOf();
|
|
||||||
case 'nameAsc':
|
|
||||||
return props.sortFns.nameAsc
|
|
||||||
? props.sortFns.nameAsc(a, b)
|
|
||||||
: props.displayName(a).trim().localeCompare(props.displayName(b).trim());
|
|
||||||
case 'nameDesc':
|
|
||||||
return props.sortFns.nameDesc
|
|
||||||
? props.sortFns.nameDesc(a, b)
|
|
||||||
: props.displayName(b).trim().localeCompare(props.displayName(a).trim());
|
|
||||||
default:
|
|
||||||
return props.sortFns[sortBy.value] ? props.sortFns[sortBy.value](a, b) : 0;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
//methods
|
|
||||||
|
|
||||||
const focusSearchInput = () => {
|
|
||||||
if (search.value) {
|
|
||||||
search.value.focus();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const hasAppliedFilters = (): boolean => {
|
|
||||||
return !!filterKeys.value.find(
|
|
||||||
(key) =>
|
|
||||||
key !== 'search' &&
|
|
||||||
(Array.isArray(props.filters[key])
|
|
||||||
? props.filters[key].length > 0
|
|
||||||
: props.filters[key] !== ''),
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
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 (resettingFilters.value) {
|
|
||||||
if (source !== 'reset') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setTimeout(() => (resettingFilters.value = false), 1500);
|
|
||||||
}
|
|
||||||
|
|
||||||
const filters = filtersModel.value as Record<string, string[] | string | boolean>;
|
|
||||||
const filtersSet: string[] = [];
|
|
||||||
const filterValues: Array<string[] | string | boolean | null> = [];
|
|
||||||
|
|
||||||
Object.keys(filters).forEach((key) => {
|
|
||||||
if (filters[key]) {
|
|
||||||
filtersSet.push(key);
|
|
||||||
filterValues.push(key === 'search' ? null : filters[key]);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
telemetry.track(`User set filters in ${props.resourceKey} list`, {
|
|
||||||
filters_set: filtersSet,
|
|
||||||
filter_values: filterValues,
|
|
||||||
[`${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;
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => filtersModel.value.homeProject,
|
|
||||||
() => {
|
|
||||||
sendFiltersTelemetry('homeProject');
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => filtersModel.value.tags,
|
|
||||||
() => {
|
|
||||||
sendFiltersTelemetry('tags');
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => filtersModel.value.type,
|
|
||||||
() => {
|
|
||||||
sendFiltersTelemetry('type');
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
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();
|
|
||||||
await nextTick();
|
|
||||||
|
|
||||||
focusSearchInput();
|
|
||||||
|
|
||||||
if (hasAppliedFilters()) {
|
|
||||||
hasFilters.value = true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const headerIcon = computed(() => {
|
|
||||||
if (projectsStore.currentProject?.type === ProjectTypes.Personal) {
|
|
||||||
return 'user';
|
|
||||||
} else if (projectsStore.currentProject?.name) {
|
|
||||||
return 'layer-group';
|
|
||||||
} else {
|
|
||||||
return 'home';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const projectName = computed(() => {
|
|
||||||
if (!projectsStore.currentProject) {
|
|
||||||
return i18n.baseText('projects.menu.home');
|
|
||||||
} else if (projectsStore.currentProject.type === ProjectTypes.Personal) {
|
|
||||||
return i18n.baseText('projects.menu.personal');
|
|
||||||
} else {
|
|
||||||
return projectsStore.currentProject.name;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
i18n,
|
|
||||||
search,
|
|
||||||
usersStore,
|
|
||||||
projectsStore,
|
|
||||||
filterKeys,
|
|
||||||
currentPage,
|
|
||||||
rowsPerPage,
|
|
||||||
filteredAndSortedResources,
|
|
||||||
hasFilters,
|
|
||||||
sortBy,
|
|
||||||
resettingFilters,
|
|
||||||
filtersModel,
|
|
||||||
sendFiltersTelemetry,
|
|
||||||
getColumns,
|
|
||||||
itemSize,
|
|
||||||
onAddButtonClick,
|
|
||||||
onUpdateFiltersLength,
|
|
||||||
onUpdateFilters,
|
|
||||||
resetFilters,
|
|
||||||
callDebounced,
|
|
||||||
setCurrentPage,
|
|
||||||
setRowsPerPage,
|
|
||||||
onSearch,
|
|
||||||
headerIcon,
|
|
||||||
projectName,
|
|
||||||
};
|
|
||||||
},
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => filtersModel.value.tags,
|
||||||
|
() => {
|
||||||
|
sendFiltersTelemetry('tags');
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => filtersModel.value.type,
|
||||||
|
() => {
|
||||||
|
sendFiltersTelemetry('type');
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
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();
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
focusSearchInput();
|
||||||
|
|
||||||
|
if (hasAppliedFilters()) {
|
||||||
|
hasFilters.value = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const headerIcon = computed(() => {
|
||||||
|
if (projectsStore.currentProject?.type === ProjectTypes.Personal) {
|
||||||
|
return 'user';
|
||||||
|
} else if (projectsStore.currentProject?.name) {
|
||||||
|
return 'layer-group';
|
||||||
|
} else {
|
||||||
|
return 'home';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const projectName = computed(() => {
|
||||||
|
if (!projectsStore.currentProject) {
|
||||||
|
return i18n.baseText('projects.menu.home');
|
||||||
|
} else if (projectsStore.currentProject.type === ProjectTypes.Personal) {
|
||||||
|
return i18n.baseText('projects.menu.personal');
|
||||||
|
} else {
|
||||||
|
return projectsStore.currentProject.name;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -2,8 +2,10 @@
|
||||||
import { ref, computed, onMounted, watch } from 'vue';
|
import { ref, computed, onMounted, watch } from 'vue';
|
||||||
import { useRoute, useRouter } from 'vue-router';
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
import type { ICredentialsResponse, ICredentialTypeMap } from '@/Interface';
|
import type { ICredentialsResponse, ICredentialTypeMap } from '@/Interface';
|
||||||
import type { IResource } from '@/components/layouts/ResourcesListLayout.vue';
|
import ResourcesListLayout, {
|
||||||
import ResourcesListLayout from '@/components/layouts/ResourcesListLayout.vue';
|
type IResource,
|
||||||
|
type IFilters,
|
||||||
|
} from '@/components/layouts/ResourcesListLayout.vue';
|
||||||
import CredentialCard from '@/components/CredentialCard.vue';
|
import CredentialCard from '@/components/CredentialCard.vue';
|
||||||
import type { ICredentialType } from 'n8n-workflow';
|
import type { ICredentialType } from 'n8n-workflow';
|
||||||
import {
|
import {
|
||||||
|
@ -43,10 +45,10 @@ const router = useRouter();
|
||||||
const telemetry = useTelemetry();
|
const telemetry = useTelemetry();
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
|
|
||||||
const filters = ref({
|
const filters = ref<IFilters>({
|
||||||
search: '',
|
search: '',
|
||||||
homeProject: '',
|
homeProject: '',
|
||||||
type: '',
|
type: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
|
@ -123,13 +125,11 @@ watch(
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const onFilter = (
|
const onFilter = (resource: IResource, newFilters: IFilters, matches: boolean): boolean => {
|
||||||
resource: ICredentialsResponse,
|
const iResource = resource as ICredentialsResponse;
|
||||||
filtersToApply: { type: string[]; search: string },
|
const filtersToApply = newFilters as IFilters & { type: string[] };
|
||||||
matches: boolean,
|
|
||||||
): boolean => {
|
|
||||||
if (filtersToApply.type.length > 0) {
|
if (filtersToApply.type.length > 0) {
|
||||||
matches = matches && filtersToApply.type.includes(resource.type);
|
matches = matches && filtersToApply.type.includes(iResource.type);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (filtersToApply.search) {
|
if (filtersToApply.search) {
|
||||||
|
@ -137,8 +137,8 @@ const onFilter = (
|
||||||
|
|
||||||
matches =
|
matches =
|
||||||
matches ||
|
matches ||
|
||||||
(credentialTypesById.value[resource.type] &&
|
(credentialTypesById.value[iResource.type] &&
|
||||||
credentialTypesById.value[resource.type].displayName.toLowerCase().includes(searchString));
|
credentialTypesById.value[iResource.type].displayName.toLowerCase().includes(searchString));
|
||||||
}
|
}
|
||||||
|
|
||||||
return matches;
|
return matches;
|
||||||
|
|
|
@ -1,10 +1,13 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, onMounted, watch, ref } from 'vue';
|
import { computed, onMounted, watch, ref } from 'vue';
|
||||||
import ResourcesListLayout, { type IResource } from '@/components/layouts/ResourcesListLayout.vue';
|
import ResourcesListLayout, {
|
||||||
|
type IResource,
|
||||||
|
type IFilters,
|
||||||
|
} from '@/components/layouts/ResourcesListLayout.vue';
|
||||||
import WorkflowCard from '@/components/WorkflowCard.vue';
|
import WorkflowCard from '@/components/WorkflowCard.vue';
|
||||||
import WorkflowTagsDropdown from '@/components/WorkflowTagsDropdown.vue';
|
import WorkflowTagsDropdown from '@/components/WorkflowTagsDropdown.vue';
|
||||||
import { EnterpriseEditionFeature, MORE_ONBOARDING_OPTIONS_EXPERIMENT, VIEWS } from '@/constants';
|
import { EnterpriseEditionFeature, MORE_ONBOARDING_OPTIONS_EXPERIMENT, VIEWS } from '@/constants';
|
||||||
import type { ITag, IUser, IWorkflowDb } from '@/Interface';
|
import type { IUser, IWorkflowDb } from '@/Interface';
|
||||||
import { useUIStore } from '@/stores/ui.store';
|
import { useUIStore } from '@/stores/ui.store';
|
||||||
import { useSettingsStore } from '@/stores/settings.store';
|
import { useSettingsStore } from '@/stores/settings.store';
|
||||||
import { useUsersStore } from '@/stores/users.store';
|
import { useUsersStore } from '@/stores/users.store';
|
||||||
|
@ -49,9 +52,7 @@ const uiStore = useUIStore();
|
||||||
const tagsStore = useTagsStore();
|
const tagsStore = useTagsStore();
|
||||||
const documentTitle = useDocumentTitle();
|
const documentTitle = useDocumentTitle();
|
||||||
|
|
||||||
interface Filters {
|
interface Filters extends IFilters {
|
||||||
search: string;
|
|
||||||
homeProject: string;
|
|
||||||
status: string | boolean;
|
status: string | boolean;
|
||||||
tags: string[];
|
tags: string[];
|
||||||
}
|
}
|
||||||
|
@ -137,16 +138,13 @@ const emptyListDescription = computed(() => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const onFilter = (
|
const onFilter = (resource: IResource, newFilters: IFilters, matches: boolean): boolean => {
|
||||||
resource: IWorkflowDb,
|
const iFilters = newFilters as Filters;
|
||||||
newFilters: { tags: string[]; search: string; status: string | boolean },
|
if (settingsStore.areTagsEnabled && iFilters.tags.length > 0) {
|
||||||
matches: boolean,
|
|
||||||
): boolean => {
|
|
||||||
if (settingsStore.areTagsEnabled && newFilters.tags.length > 0) {
|
|
||||||
matches =
|
matches =
|
||||||
matches &&
|
matches &&
|
||||||
newFilters.tags.every((tag) =>
|
iFilters.tags.every((tag) =>
|
||||||
(resource.tags as ITag[])?.find((resourceTag) =>
|
(resource as IWorkflowDb).tags?.find((resourceTag) =>
|
||||||
typeof resourceTag === 'object'
|
typeof resourceTag === 'object'
|
||||||
? `${resourceTag.id}` === `${tag}`
|
? `${resourceTag.id}` === `${tag}`
|
||||||
: `${resourceTag}` === `${tag}`,
|
: `${resourceTag}` === `${tag}`,
|
||||||
|
@ -155,14 +153,14 @@ const onFilter = (
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newFilters.status !== '') {
|
if (newFilters.status !== '') {
|
||||||
matches = matches && resource.active === newFilters.status;
|
matches = matches && (resource as IWorkflowDb).active === newFilters.status;
|
||||||
}
|
}
|
||||||
|
|
||||||
return matches;
|
return matches;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Methods
|
// Methods
|
||||||
const onFiltersUpdated = (newFilters: Filters) => {
|
const onFiltersUpdated = (newFilters: IFilters) => {
|
||||||
Object.assign(filters.value, newFilters);
|
Object.assign(filters.value, newFilters);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue