refactor(editor): ResourceListLayout to script setup (#11526)

This commit is contained in:
Raúl Gómez Morales 2024-11-04 13:07:21 +01:00 committed by GitHub
parent e10968b26f
commit 5f3deea60f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 336 additions and 380 deletions

View file

@ -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,108 +30,93 @@ 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,
default: '' as IResourceKeyType,
},
displayName: {
type: Function as PropType<(resource: IResource) => string>,
default: (resource: IResource) => resource.name,
},
resources: {
type: Array as PropType<IResource[]>,
default: (): IResource[] => [],
},
disabled: {
type: Boolean,
default: false,
},
initialize: {
type: Function as PropType<() => Promise<void>>,
default: () => async () => {},
},
filters: {
type: Object,
default: (): IFilters => ({ search: '', homeProject: '' }),
},
additionalFiltersHandler: {
type: Function,
required: false,
default: undefined,
},
shareable: {
type: Boolean,
default: true,
},
showFiltersDropdown: {
type: Boolean,
default: true,
},
sortFns: {
type: Object as PropType<Record<string, (a: IResource, b: IResource) => number>>,
default: (): Record<string, (a: IResource, b: IResource) => number> => ({}),
},
sortOptions: {
type: Array as PropType<string[]>,
default: () => ['lastUpdated', 'lastCreated', 'nameAsc', 'nameDesc'],
},
type: {
type: String as PropType<'datatable' | 'list'>,
default: 'list',
},
typeProps: {
type: Object as PropType<{ itemSize: number } | { columns: DatatableColumn[] }>,
default: () => ({
itemSize: 80,
}),
},
loading: {
type: Boolean,
required: false,
default: true,
},
},
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]); const emit = defineEmits<{
const hasFilters = ref(false); 'update:filters': [value: IFilters];
const filtersModel = ref(props.filters); 'click:add': [event: Event];
const currentPage = ref(1); sort: [value: string];
const rowsPerPage = ref<number>(10); }>();
const resettingFilters = ref(false);
const search = ref<HTMLElement | null>(null);
//computed defineSlots<{
header(): unknown;
empty(): unknown;
preamble(): unknown;
postamble(): unknown;
'add-button'(props: { disabled: boolean }): unknown;
callout(): unknown;
filters(props: {
filters: Record<string, boolean | string | string[]>;
setKeyValue: (key: string, value: unknown) => void;
}): unknown;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
default(props: { data: any; updateItemSize: (data: any) => void }): unknown;
}>();
const filterKeys = computed(() => { const route = useRoute();
const i18n = useI18n();
const { callDebounced } = useDebounce();
const usersStore = useUsersStore();
const projectsStore = useProjectsStore();
const telemetry = useTelemetry();
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); return Object.keys(filtersModel.value);
}); });
const filteredAndSortedResources = computed(() => { const filteredAndSortedResources = computed(() => {
const filtered = props.resources.filter((resource) => { const filtered = props.resources.filter((resource) => {
let matches = true; let matches = true;
@ -177,17 +160,17 @@ export default defineComponent({
return props.sortFns[sortBy.value] ? props.sortFns[sortBy.value](a, b) : 0; return props.sortFns[sortBy.value] ? props.sortFns[sortBy.value](a, b) : 0;
} }
}); });
}); });
//methods //methods
const focusSearchInput = () => { const focusSearchInput = () => {
if (search.value) { if (search.value) {
search.value.focus(); search.value.focus();
} }
}; };
const hasAppliedFilters = (): boolean => { const hasAppliedFilters = (): boolean => {
return !!filterKeys.value.find( return !!filterKeys.value.find(
(key) => (key) =>
key !== 'search' && key !== 'search' &&
@ -195,17 +178,22 @@ export default defineComponent({
? props.filters[key].length > 0 ? props.filters[key].length > 0
: props.filters[key] !== ''), : props.filters[key] !== ''),
); );
}; };
const setRowsPerPage = (numberOfRowsPerPage: number) => { const setRowsPerPage = (numberOfRowsPerPage: number) => {
rowsPerPage.value = numberOfRowsPerPage; rowsPerPage.value = numberOfRowsPerPage;
}; };
const setCurrentPage = (page: number) => { const setCurrentPage = (page: number) => {
currentPage.value = page; currentPage.value = page;
}; };
const sendFiltersTelemetry = (source: string) => { defineExpose({
currentPage,
setCurrentPage,
});
const sendFiltersTelemetry = (source: string) => {
// Prevent sending multiple telemetry events when resetting filters // Prevent sending multiple telemetry events when resetting filters
// Timeout is required to wait for search debounce to be over // Timeout is required to wait for search debounce to be over
if (resettingFilters.value) { if (resettingFilters.value) {
@ -233,17 +221,17 @@ export default defineComponent({
[`${props.resourceKey}_total_in_view`]: props.resources.length, [`${props.resourceKey}_total_in_view`]: props.resources.length,
[`${props.resourceKey}_after_filtering`]: filteredAndSortedResources.value.length, [`${props.resourceKey}_after_filtering`]: filteredAndSortedResources.value.length,
}); });
}; };
const onAddButtonClick = (e: Event) => { const onAddButtonClick = (e: Event) => {
emit('click:add', e); emit('click:add', e);
}; };
const onUpdateFilters = (e: Event) => { const onUpdateFilters = (e: IFilters) => {
emit('update:filters', e); emit('update:filters', e);
}; };
const resetFilters = () => { const resetFilters = () => {
Object.keys(filtersModel.value).forEach((key) => { Object.keys(filtersModel.value).forEach((key) => {
filtersModel.value[key] = Array.isArray(filtersModel.value[key]) ? [] : ''; filtersModel.value[key] = Array.isArray(filtersModel.value[key]) ? [] : '';
}); });
@ -251,88 +239,88 @@ export default defineComponent({
resettingFilters.value = true; resettingFilters.value = true;
sendFiltersTelemetry('reset'); sendFiltersTelemetry('reset');
emit('update:filters', filtersModel.value); emit('update:filters', filtersModel.value);
}; };
const itemSize = () => { const itemSize = () => {
if ('itemSize' in props.typeProps) { if ('itemSize' in props.typeProps) {
return props.typeProps.itemSize; return props.typeProps.itemSize;
} }
return 0; return 0;
}; };
const getColumns = () => { const getColumns = () => {
if ('columns' in props.typeProps) { if ('columns' in props.typeProps) {
return props.typeProps.columns; return props.typeProps.columns;
} }
return {}; return {};
}; };
const sendSortingTelemetry = () => { const sendSortingTelemetry = () => {
telemetry.track(`User changed sorting in ${props.resourceKey} list`, { telemetry.track(`User changed sorting in ${props.resourceKey} list`, {
sorting: sortBy.value, sorting: sortBy.value,
}); });
}; };
const onUpdateFiltersLength = (length: number) => { const onUpdateFiltersLength = (length: number) => {
hasFilters.value = length > 0; hasFilters.value = length > 0;
}; };
const onSearch = (s: string) => { const onSearch = (s: string) => {
filtersModel.value.search = s; filtersModel.value.search = s;
emit('update:filters', filtersModel.value); emit('update:filters', filtersModel.value);
}; };
//watchers //watchers
watch( watch(
() => props.filters, () => props.filters,
(value) => { (value) => {
filtersModel.value = value; filtersModel.value = value;
}, },
); );
watch( watch(
() => filtersModel.value.homeProject, () => filtersModel.value.homeProject,
() => { () => {
sendFiltersTelemetry('homeProject'); sendFiltersTelemetry('homeProject');
}, },
); );
watch( watch(
() => filtersModel.value.tags, () => filtersModel.value.tags,
() => { () => {
sendFiltersTelemetry('tags'); sendFiltersTelemetry('tags');
}, },
); );
watch( watch(
() => filtersModel.value.type, () => filtersModel.value.type,
() => { () => {
sendFiltersTelemetry('type'); sendFiltersTelemetry('type');
}, },
); );
watch( watch(
() => filtersModel.value.search, () => filtersModel.value.search,
() => callDebounced(sendFiltersTelemetry, { debounceTime: 1000, trailing: true }, 'search'), () => callDebounced(sendFiltersTelemetry, { debounceTime: 1000, trailing: true }, 'search'),
); );
watch( watch(
() => sortBy.value, () => sortBy.value,
(newValue) => { (newValue) => {
emit('sort', newValue); emit('sort', newValue);
sendSortingTelemetry(); sendSortingTelemetry();
}, },
); );
watch( watch(
() => route?.params?.projectId, () => route?.params?.projectId,
() => { () => {
resetFilters(); resetFilters();
}, },
); );
onMounted(async () => { onMounted(async () => {
await props.initialize(); await props.initialize();
await nextTick(); await nextTick();
@ -341,9 +329,9 @@ export default defineComponent({
if (hasAppliedFilters()) { if (hasAppliedFilters()) {
hasFilters.value = true; hasFilters.value = true;
} }
}); });
const headerIcon = computed(() => { const headerIcon = computed(() => {
if (projectsStore.currentProject?.type === ProjectTypes.Personal) { if (projectsStore.currentProject?.type === ProjectTypes.Personal) {
return 'user'; return 'user';
} else if (projectsStore.currentProject?.name) { } else if (projectsStore.currentProject?.name) {
@ -351,9 +339,9 @@ export default defineComponent({
} else { } else {
return 'home'; return 'home';
} }
}); });
const projectName = computed(() => { const projectName = computed(() => {
if (!projectsStore.currentProject) { if (!projectsStore.currentProject) {
return i18n.baseText('projects.menu.home'); return i18n.baseText('projects.menu.home');
} else if (projectsStore.currentProject.type === ProjectTypes.Personal) { } else if (projectsStore.currentProject.type === ProjectTypes.Personal) {
@ -361,36 +349,6 @@ export default defineComponent({
} else { } else {
return projectsStore.currentProject.name; 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,
};
},
}); });
</script> </script>

View file

@ -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;

View file

@ -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);
}; };