refactor(editor): Migrate small components to composition API (#11509)

This commit is contained in:
Ricardo Espinoza
2024-11-04 08:06:00 -05:00
committed by GitHub
parent 5e2e205394
commit 23677062d9
12 changed files with 372 additions and 470 deletions

View File

@@ -1,7 +1,5 @@
<script lang="ts">
import type { PropType } from 'vue';
import { defineComponent } from 'vue';
import { mapStores } from 'pinia';
<script setup lang="ts">
import { computed, ref } from 'vue';
import type { IN8nPromptResponse, ModalKey } from '@/Interface';
import { VALID_EMAIL_REGEX } from '@/constants';
import Modal from '@/components/Modal.vue';
@@ -10,78 +8,69 @@ import { useRootStore } from '@/stores/root.store';
import { createEventBus } from 'n8n-design-system/utils';
import { useToast } from '@/composables/useToast';
import { useNpsSurveyStore } from '@/stores/npsSurvey.store';
import { useTelemetry } from '@/composables/useTelemetry';
export default defineComponent({
name: 'ContactPromptModal',
components: { Modal },
props: {
modalName: {
type: String as PropType<ModalKey>,
required: true,
},
},
setup() {
return {
...useToast(),
};
},
data() {
return {
email: '',
modalBus: createEventBus(),
};
},
computed: {
...mapStores(useRootStore, useSettingsStore, useNpsSurveyStore),
title(): string {
if (this.npsSurveyStore.promptsData?.title) {
return this.npsSurveyStore.promptsData.title;
defineProps<{
modalName: ModalKey;
}>();
const email = ref('');
const modalBus = createEventBus();
const npsSurveyStore = useNpsSurveyStore();
const rootStore = useRootStore();
const settingsStore = useSettingsStore();
const toast = useToast();
const telemetry = useTelemetry();
const title = computed(() => {
if (npsSurveyStore.promptsData?.title) {
return npsSurveyStore.promptsData.title;
}
return 'Youre a power user 💪';
},
description(): string {
if (this.npsSurveyStore.promptsData?.message) {
return this.npsSurveyStore.promptsData.message;
});
const description = computed(() => {
if (npsSurveyStore.promptsData?.message) {
return npsSurveyStore.promptsData.message;
}
return 'Your experience with n8n can help us improve — for you and our entire community.';
},
isEmailValid(): boolean {
return VALID_EMAIL_REGEX.test(String(this.email).toLowerCase());
},
},
methods: {
closeDialog(): void {
if (!this.isEmailValid) {
this.$telemetry.track('User closed email modal', {
instance_id: this.rootStore.instanceId,
});
const isEmailValid = computed(() => {
return VALID_EMAIL_REGEX.test(String(email.value).toLowerCase());
});
const closeDialog = () => {
if (!isEmailValid.value) {
telemetry.track('User closed email modal', {
instance_id: rootStore.instanceId,
email: null,
});
}
},
async send() {
if (this.isEmailValid) {
const response = (await this.settingsStore.submitContactInfo(
this.email,
)) as IN8nPromptResponse;
};
const send = async () => {
if (isEmailValid.value) {
const response = (await settingsStore.submitContactInfo(email.value)) as IN8nPromptResponse;
if (response.updated) {
this.$telemetry.track('User closed email modal', {
instance_id: this.rootStore.instanceId,
email: this.email,
telemetry.track('User closed email modal', {
instance_id: rootStore.instanceId,
email: email.value,
});
this.showMessage({
toast.showMessage({
title: 'Thanks!',
message: "It's people like you that help make n8n better",
type: 'success',
});
}
this.modalBus.emit('close');
modalBus.emit('close');
}
},
},
});
};
</script>
<template>

View File

@@ -1,34 +1,36 @@
<script lang="ts">
import type { PropType } from 'vue';
import { defineComponent } from 'vue';
<script setup lang="ts">
import { nextTick } from 'vue';
import { onBeforeUnmount, onMounted, ref } from 'vue';
import type { EventBus } from 'n8n-design-system/utils';
import { createEventBus } from 'n8n-design-system/utils';
export default defineComponent({
name: 'IntersectionObserved',
props: {
enabled: {
type: Boolean,
default: false,
},
eventBus: {
type: Object as PropType<EventBus>,
const props = withDefaults(
defineProps<{
enabled: boolean;
eventBus: EventBus;
}>(),
{
enabled: false,
default: () => createEventBus(),
},
},
async mounted() {
if (!this.enabled) {
);
const observed = ref<IntersectionObserver | null>(null);
onMounted(async () => {
if (!props.enabled) {
return;
}
await this.$nextTick();
this.eventBus.emit('observe', this.$refs.observed);
},
beforeUnmount() {
if (this.enabled) {
this.eventBus.emit('unobserve', this.$refs.observed);
await nextTick();
props.eventBus.emit('observe', observed.value);
});
onBeforeUnmount(() => {
if (props.enabled) {
props.eventBus.emit('unobserve', observed.value);
}
},
});
</script>

View File

@@ -1,67 +1,65 @@
<script lang="ts">
import type { PropType } from 'vue';
import { defineComponent } from 'vue';
<script setup lang="ts">
import { onBeforeUnmount, onMounted, ref } from 'vue';
import type { EventBus } from 'n8n-design-system/utils';
import { createEventBus } from 'n8n-design-system/utils';
export default defineComponent({
name: 'IntersectionObserver',
props: {
threshold: {
type: Number,
default: 0,
},
enabled: {
type: Boolean,
default: false,
},
eventBus: {
type: Object as PropType<EventBus>,
const props = withDefaults(
defineProps<{
threshold: number;
enabled: boolean;
eventBus: EventBus;
}>(),
{
threshold: 0,
enabled: false,
default: () => createEventBus(),
},
},
data() {
return {
observer: null as IntersectionObserver | null,
};
},
mounted() {
if (!this.enabled) {
);
const emit = defineEmits<{
observed: [{ el: HTMLElement; isIntersecting: boolean }];
}>();
const observer = ref<IntersectionObserver | null>(null);
const root = ref<HTMLElement | null>(null);
onBeforeUnmount(() => {
if (props.enabled && observer.value) {
observer.value.disconnect();
}
});
onMounted(() => {
if (!props.enabled) {
return;
}
const options = {
root: this.$refs.root as Element,
root: root.value,
rootMargin: '0px',
threshold: this.threshold,
threshold: props.threshold,
};
const observer = new IntersectionObserver((entries) => {
const intersectionObserver = new IntersectionObserver((entries) => {
entries.forEach(({ target, isIntersecting }) => {
this.$emit('observed', {
el: target,
emit('observed', {
el: target as HTMLElement,
isIntersecting,
});
});
}, options);
this.observer = observer;
observer.value = intersectionObserver;
this.eventBus.on('observe', (observed: Element) => {
props.eventBus.on('observe', (observed: Element) => {
if (observed) {
observer.observe(observed);
intersectionObserver.observe(observed);
}
});
this.eventBus.on('unobserve', (observed: Element) => {
observer.unobserve(observed);
props.eventBus.on('unobserve', (observed: Element) => {
intersectionObserver.unobserve(observed);
});
},
beforeUnmount() {
if (this.enabled && this.observer) {
this.observer.disconnect();
}
},
});
</script>

View File

@@ -1,20 +1,14 @@
<script lang="ts">
import { defineComponent } from 'vue';
import { mapStores } from 'pinia';
<script setup lang="ts">
import { computed } from 'vue';
import { useRootStore } from '@/stores/root.store';
import { useUIStore } from '@/stores/ui.store';
export default defineComponent({
computed: {
...mapStores(useRootStore, useUIStore),
basePath(): string {
return this.rootStore.baseUrl;
},
logoPath(): string {
return this.basePath + this.uiStore.logo;
},
},
});
const rootStore = useRootStore();
const uiStore = useUIStore();
const basePath = computed(() => rootStore.baseUrl);
const logoPath = computed(() => basePath.value + uiStore.logo);
</script>
<template>

View File

@@ -1,85 +1,71 @@
<script lang="ts">
<script setup lang="ts">
import { useUIStore } from '@/stores/ui.store';
import { mapStores } from 'pinia';
import { defineComponent } from 'vue';
import type { PropType } from 'vue';
import { onBeforeUnmount, onMounted } from 'vue';
import type { EventBus } from 'n8n-design-system';
import { ElDrawer } from 'element-plus';
export default defineComponent({
name: 'ModalDrawer',
components: {
ElDrawer,
const props = withDefaults(
defineProps<{
name: string;
beforeClose?: Function;
eventBus?: EventBus;
direction: 'ltr' | 'rtl' | 'ttb' | 'btt';
modal?: boolean;
width: string;
wrapperClosable?: boolean;
}>(),
{
modal: true,
wrapperClosable: true,
},
props: {
name: {
type: String,
required: true,
},
beforeClose: {
type: Function,
},
eventBus: {
type: Object as PropType<EventBus>,
},
direction: {
type: String as PropType<'ltr' | 'rtl' | 'ttb' | 'btt'>,
required: true,
},
modal: {
type: Boolean,
default: true,
},
width: {
type: String,
},
wrapperClosable: {
type: Boolean,
default: true,
},
},
mounted() {
window.addEventListener('keydown', this.onWindowKeydown);
this.eventBus?.on('close', this.close);
);
const activeElement = document.activeElement as HTMLElement;
if (activeElement) {
activeElement.blur();
const emit = defineEmits<{
enter: [];
}>();
const uiStore = useUIStore();
const handleEnter = () => {
if (uiStore.isModalActiveById[props.name]) {
emit('enter');
}
},
beforeUnmount() {
this.eventBus?.off('close', this.close);
window.removeEventListener('keydown', this.onWindowKeydown);
},
computed: {
...mapStores(useUIStore),
},
methods: {
onWindowKeydown(event: KeyboardEvent) {
if (!this.uiStore.isModalActiveById[this.name]) {
};
const onWindowKeydown = (event: KeyboardEvent) => {
if (!uiStore.isModalActiveById[props.name]) {
return;
}
if (event && event.keyCode === 13) {
this.handleEnter();
handleEnter();
}
},
handleEnter() {
if (this.uiStore.isModalActiveById[this.name]) {
this.$emit('enter');
}
},
async close() {
if (this.beforeClose) {
const shouldClose = await this.beforeClose();
};
const close = async () => {
if (props.beforeClose) {
const shouldClose = await props.beforeClose();
if (shouldClose === false) {
// must be strictly false to stop modal from closing
return;
}
}
this.uiStore.closeModal(this.name);
},
},
uiStore.closeModal(props.name);
};
onMounted(() => {
window.addEventListener('keydown', onWindowKeydown);
props.eventBus?.on('close', close);
const activeElement = document.activeElement as HTMLElement;
if (activeElement) {
activeElement.blur();
}
});
onBeforeUnmount(() => {
props.eventBus?.off('close', close);
window.removeEventListener('keydown', onWindowKeydown);
});
</script>

View File

@@ -1,25 +1,23 @@
<script lang="ts">
import type { PropType } from 'vue';
import { defineComponent } from 'vue';
<script setup lang="ts">
import { useUIStore } from '@/stores/ui.store';
import { mapStores } from 'pinia';
import type { ModalKey } from '@/Interface';
export default defineComponent({
name: 'ModalRoot',
props: {
name: {
type: String as PropType<ModalKey>,
required: true,
},
keepAlive: {
type: Boolean,
},
},
computed: {
...mapStores(useUIStore),
},
});
defineProps<{
name: string;
keepAlive?: boolean;
}>();
defineSlots<{
default: {
modalName: string;
active: boolean;
open: boolean;
activeId: string;
mode: string;
data: Record<string, unknown>;
};
}>();
const uiStore = useUIStore();
</script>
<template>

View File

@@ -69,6 +69,7 @@ import ProjectMoveResourceModal from '@/components/Projects/ProjectMoveResourceM
import NewAssistantSessionModal from '@/components/AskAssistant/NewAssistantSessionModal.vue';
import PromptMfaCodeModal from './PromptMfaCodeModal/PromptMfaCodeModal.vue';
import CommunityPlusEnrollmentModal from '@/components/CommunityPlusEnrollmentModal.vue';
import type { EventBus } from 'n8n-design-system';
</script>
<template>
@@ -183,7 +184,15 @@ import CommunityPlusEnrollmentModal from '@/components/CommunityPlusEnrollmentMo
</ModalRoot>
<ModalRoot :name="LOG_STREAM_MODAL_KEY">
<template #default="{ modalName, data }">
<template
#default="{
modalName,
data,
}: {
modalName: string;
data: { destination: Object; isNew: boolean; eventBus: EventBus };
}"
>
<EventDestinationSettingsModal
:modal-name="modalName"
:destination="data.destination"

View File

@@ -1,48 +1,40 @@
<script lang="ts">
import { defineComponent } from 'vue';
<script setup lang="ts">
import { computed } from 'vue';
import NodeIcon from '@/components/NodeIcon.vue';
import type { ITemplatesNode } from '@/Interface';
import { filterTemplateNodes } from '@/utils/nodeTypesUtils';
export default defineComponent({
name: 'NodeList',
components: {
NodeIcon,
},
props: {
nodes: {
type: Array,
},
limit: {
type: Number,
default: 4,
},
size: {
type: String,
default: 'sm',
},
},
computed: {
filteredCoreNodes() {
return filterTemplateNodes(this.nodes as ITemplatesNode[]);
},
hiddenNodes(): number {
return this.filteredCoreNodes.length - this.countNodesToBeSliced(this.filteredCoreNodes);
},
slicedNodes(): ITemplatesNode[] {
return this.filteredCoreNodes.slice(0, this.countNodesToBeSliced(this.filteredCoreNodes));
},
},
methods: {
countNodesToBeSliced(nodes: ITemplatesNode[]): number {
if (nodes.length > this.limit) {
return this.limit - 1;
} else {
return this.limit;
}
},
const props = withDefaults(
defineProps<{
nodes: ITemplatesNode[];
limit?: number;
size?: string;
}>(),
{
limit: 4,
size: 'sm',
},
);
const filteredCoreNodes = computed(() => {
return filterTemplateNodes(props.nodes);
});
const hiddenNodes = computed(() => {
return filteredCoreNodes.value.length - countNodesToBeSliced(filteredCoreNodes.value);
});
const slicedNodes = computed(() => {
return filteredCoreNodes.value.slice(0, countNodesToBeSliced(filteredCoreNodes.value));
});
const countNodesToBeSliced = (nodes: ITemplatesNode[]): number => {
if (nodes.length > props.limit) {
return props.limit - 1;
} else {
return props.limit;
}
};
</script>
<template>

View File

@@ -1,32 +1,29 @@
<script lang="ts">
import { defineComponent } from 'vue';
<script setup lang="ts">
import Draggable from './Draggable.vue';
import type { XYPosition } from '@/Interface';
export default defineComponent({
components: {
Draggable,
},
props: {
canMoveRight: {
type: Boolean,
},
canMoveLeft: {
type: Boolean,
},
},
methods: {
onDrag(e: XYPosition) {
this.$emit('drag', e);
},
onDragStart() {
this.$emit('dragstart');
},
onDragEnd() {
this.$emit('dragend');
},
},
});
defineProps<{
canMoveRight: boolean;
canMoveLeft: boolean;
}>();
const emit = defineEmits<{
drag: [e: XYPosition];
dragstart: [];
dragend: [];
}>();
const onDrag = (e: XYPosition) => {
emit('drag', e);
};
const onDragEnd = () => {
emit('dragend');
};
const onDragStart = () => {
emit('dragstart');
};
</script>
<template>

View File

@@ -1,14 +1,9 @@
<script lang="ts">
import { defineComponent } from 'vue';
<script setup lang="ts">
import TitledList from '@/components/TitledList.vue';
export default defineComponent({
name: 'ParameterIssues',
components: {
TitledList,
},
props: ['issues'],
});
defineProps<{
issues: string[];
}>();
</script>
<template>

View File

@@ -1,6 +1,4 @@
<script lang="ts">
import { defineComponent } from 'vue';
import type { PropType } from 'vue';
<script setup lang="ts">
import TemplateDetailsBlock from '@/components/TemplateDetailsBlock.vue';
import NodeIcon from '@/components/NodeIcon.vue';
import { filterTemplateNodes } from '@/utils/nodeTypesUtils';
@@ -11,51 +9,32 @@ import type {
ITemplatesNode,
ITemplatesWorkflow,
} from '@/Interface';
import { mapStores } from 'pinia';
import { useTemplatesStore } from '@/stores/templates.store';
import TimeAgo from '@/components/TimeAgo.vue';
import { isFullTemplatesCollection, isTemplatesWorkflow } from '@/utils/templates/typeGuards';
import { useRouter } from 'vue-router';
import { useI18n } from '@/composables/useI18n';
export default defineComponent({
name: 'TemplateDetails',
components: {
NodeIcon,
TemplateDetailsBlock,
TimeAgo,
},
props: {
template: {
type: Object as PropType<
ITemplatesWorkflow | ITemplatesCollection | ITemplatesCollectionFull | null
>,
required: true,
},
blockTitle: {
type: String,
required: true,
},
loading: {
type: Boolean,
},
},
computed: {
...mapStores(useTemplatesStore),
},
methods: {
abbreviateNumber,
filterTemplateNodes,
redirectToCategory(id: string) {
this.templatesStore.resetSessionId();
void this.$router.push(`/templates?categories=${id}`);
},
redirectToSearchPage(node: ITemplatesNode) {
this.templatesStore.resetSessionId();
void this.$router.push(`/templates?search=${node.displayName}`);
},
isFullTemplatesCollection,
isTemplatesWorkflow,
},
});
defineProps<{
template: ITemplatesWorkflow | ITemplatesCollection | ITemplatesCollectionFull | null;
blockTitle: string;
loading: boolean;
}>();
const router = useRouter();
const i18n = useI18n();
const templatesStore = useTemplatesStore();
const redirectToCategory = (id: string) => {
templatesStore.resetSessionId();
void router.push(`/templates?categories=${id}`);
};
const redirectToSearchPage = (node: ITemplatesNode) => {
templatesStore.resetSessionId();
void router.push(`/templates?search=${node.displayName}`);
};
</script>
<template>
@@ -91,13 +70,13 @@ export default defineComponent({
<TemplateDetailsBlock
v-if="!loading && template"
:title="$locale.baseText('template.details.details')"
:title="i18n.baseText('template.details.details')"
>
<div :class="$style.text">
<n8n-text v-if="isTemplatesWorkflow(template)" size="small" color="text-base">
{{ $locale.baseText('template.details.created') }}
{{ i18n.baseText('template.details.created') }}
<TimeAgo :date="template.createdAt" />
{{ $locale.baseText('template.details.by') }}
{{ i18n.baseText('template.details.by') }}
{{ template.user ? template.user.username : 'n8n team' }}
</n8n-text>
</div>
@@ -107,9 +86,9 @@ export default defineComponent({
size="small"
color="text-base"
>
{{ $locale.baseText('template.details.viewed') }}
{{ i18n.baseText('template.details.viewed') }}
{{ abbreviateNumber(template.totalViews) }}
{{ $locale.baseText('template.details.times') }}
{{ i18n.baseText('template.details.times') }}
</n8n-text>
</div>
</TemplateDetailsBlock>

View File

@@ -1,103 +1,66 @@
<script lang="ts">
import { defineComponent } from 'vue';
<script setup lang="ts">
import { computed, onBeforeMount, onBeforeUnmount, onMounted } from 'vue';
import { useRouter } from 'vue-router';
import { mapStores } from 'pinia';
import type { WorkerStatus } from '@n8n/api-types';
import type { ExecutionStatus } from 'n8n-workflow';
import PushConnectionTracker from '@/components/PushConnectionTracker.vue';
import { useI18n } from '@/composables/useI18n';
import { useToast } from '@/composables/useToast';
import { useUIStore } from '@/stores/ui.store';
import { useOrchestrationStore } from '@/stores/orchestration.store';
import { useDocumentTitle } from '@/composables/useDocumentTitle';
import WorkerCard from './Workers/WorkerCard.ee.vue';
import { usePushConnection } from '@/composables/usePushConnection';
import { usePushConnectionStore } from '@/stores/pushConnection.store';
import { useRootStore } from '@/stores/root.store';
import { useTelemetry } from '@/composables/useTelemetry';
// eslint-disable-next-line import/no-default-export
export default defineComponent({
name: 'WorkerList',
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/naming-convention
components: { PushConnectionTracker, WorkerCard },
props: {
autoRefreshEnabled: {
type: Boolean,
default: true,
withDefaults(
defineProps<{
autoRefreshEnabled?: boolean;
}>(),
{
autoRefreshEnabled: true,
},
},
setup() {
);
const router = useRouter();
const i18n = useI18n();
const pushConnection = usePushConnection({ router });
const documentTitle = useDocumentTitle();
const telemetry = useTelemetry();
return {
i18n,
pushConnection,
...useToast(),
documentTitle: useDocumentTitle(),
};
},
computed: {
...mapStores(useRootStore, useUIStore, usePushConnectionStore, useOrchestrationStore),
combinedWorkers(): WorkerStatus[] {
const returnData: WorkerStatus[] = [];
for (const workerId in this.orchestrationManagerStore.workers) {
returnData.push(this.orchestrationManagerStore.workers[workerId]);
}
return returnData;
},
initialStatusReceived(): boolean {
return this.orchestrationManagerStore.initialStatusReceived;
},
workerIds(): string[] {
return Object.keys(this.orchestrationManagerStore.workers);
},
pageTitle() {
return this.i18n.baseText('workerList.pageTitle');
},
},
mounted() {
this.documentTitle.set(this.pageTitle);
const orchestrationManagerStore = useOrchestrationStore();
const rootStore = useRootStore();
const pushStore = usePushConnectionStore();
this.$telemetry.track('User viewed worker view', {
instance_id: this.rootStore.instanceId,
const initialStatusReceived = computed(() => orchestrationManagerStore.initialStatusReceived);
const workerIds = computed(() => Object.keys(orchestrationManagerStore.workers));
const pageTitle = computed(() => i18n.baseText('workerList.pageTitle'));
onMounted(() => {
documentTitle.set(pageTitle.value);
telemetry.track('User viewed worker view', {
instance_id: rootStore.instanceId,
});
},
beforeMount() {
});
onBeforeMount(() => {
if (window.Cypress !== undefined) {
return;
}
this.pushConnection.initialize();
this.pushStore.pushConnect();
this.orchestrationManagerStore.startWorkerStatusPolling();
},
beforeUnmount() {
pushConnection.initialize();
pushStore.pushConnect();
orchestrationManagerStore.startWorkerStatusPolling();
});
onBeforeUnmount(() => {
if (window.Cypress !== undefined) {
return;
}
this.orchestrationManagerStore.stopWorkerStatusPolling();
this.pushStore.pushDisconnect();
this.pushConnection.terminate();
},
methods: {
averageLoadAvg(loads: number[]) {
return (loads.reduce((prev, curr) => prev + curr, 0) / loads.length).toFixed(2);
},
getStatus(payload: WorkerStatus): ExecutionStatus {
if (payload.runningJobsSummary.length > 0) {
return 'running';
} else {
return 'success';
}
},
getRowClass(payload: WorkerStatus): string {
return [this.$style.execRow, this.$style[this.getStatus(payload)]].join(' ');
},
},
orchestrationManagerStore.stopWorkerStatusPolling();
pushStore.pushDisconnect();
pushConnection.terminate();
});
</script>
@@ -111,7 +74,7 @@ export default defineComponent({
<n8n-spinner />
</div>
<div v-else>
<div v-if="workerIds.length === 0">{{ $locale.baseText('workerList.empty') }}</div>
<div v-if="workerIds.length === 0">{{ i18n.baseText('workerList.empty') }}</div>
<div v-else>
<div v-for="workerId in workerIds" :key="workerId" :class="$style.card">
<WorkerCard :worker-id="workerId" data-test-id="worker-card" />