test: fix most unit tests

This commit is contained in:
Alex Grozav 2023-07-21 11:36:05 +03:00
parent 964811243c
commit 206289a7b0
103 changed files with 2152 additions and 2070 deletions

View file

@ -1,6 +1,6 @@
<script lang="ts" setup>
import { computed, ref } from 'vue';
import { uid } from '@/utils';
import { uid } from '../../utils';
import { ElColorPicker } from 'element-plus';
import N8nInput from '../N8nInput';

View file

@ -10,7 +10,7 @@
<slot name="prepend" />
</div>
<el-select
v-bind="{ ...$props, ...$attrs }"
v-bind="{ ...$props, ...listeners }"
:modelValue="modelValue"
:size="computedSize"
:class="$style[classes]"
@ -92,6 +92,15 @@ export default defineComponent({
},
},
computed: {
listeners() {
return Object.entries(this.$attrs).reduce<Record<string, () => {}>>((acc, [key, value]) => {
if (/^on[A-Z]/.test(key)) {
acc[key] = value;
}
return acc;
}, {});
},
computedSize(): string | undefined {
if (this.size === 'medium') {
return 'default';

View file

@ -0,0 +1,85 @@
import type { Plugin } from 'vue';
import { render } from '@testing-library/vue';
import { i18nInstance, I18nPlugin } from '@/plugins/i18n';
import { GlobalComponentsPlugin } from '@/plugins/components';
import { GlobalDirectivesPlugin } from '@/plugins/directives';
import { FontAwesomePlugin } from '@/plugins/icons';
import { PiniaVuePlugin } from 'pinia';
import type { Telemetry } from '@/plugins/telemetry';
import vueJsonPretty from 'vue-json-pretty';
import { merge } from 'lodash-es';
import type { TestingPinia } from '@pinia/testing';
export type RenderComponent = Parameters<typeof render>[0];
export type RenderOptions = Parameters<typeof render>[1] & {
pinia?: TestingPinia;
};
const TelemetryPlugin: Plugin<{}> = {
install(app) {
app.config.globalProperties.$telemetry = {
track(event: string, properties?: object) {},
} as Telemetry;
},
};
const defaultOptions = {
global: {
stubs: {
'router-link': true,
'vue-json-pretty': vueJsonPretty,
},
plugins: [
I18nPlugin,
i18nInstance,
PiniaVuePlugin,
FontAwesomePlugin,
GlobalComponentsPlugin,
GlobalDirectivesPlugin,
TelemetryPlugin,
],
},
};
export function renderComponent(component: RenderComponent, options: RenderOptions = {}) {
const { pinia, ...renderOptions } = options;
return render(component, {
...defaultOptions,
...renderOptions,
global: {
...defaultOptions.global,
...renderOptions.global,
stubs: { ...defaultOptions.global.stubs, ...(renderOptions.global?.stubs ?? {}) },
plugins: [
...defaultOptions.global.plugins,
...(renderOptions.global?.plugins ?? []),
...(pinia ? [pinia] : []),
],
},
});
}
export function createComponentRenderer(
component: RenderComponent,
defaultOptions: RenderOptions = {},
) {
return (options: RenderOptions = {}, rendererOptions: { merge?: boolean } = {}) =>
renderComponent(
component,
rendererOptions.merge
? merge(defaultOptions, options)
: {
...defaultOptions,
...options,
props: {
...defaultOptions.props,
...options.props,
},
global: {
...defaultOptions.global,
...options.global,
},
},
);
}

View file

@ -1,27 +1,8 @@
import '@testing-library/jest-dom';
import { configure } from '@testing-library/vue';
import '../plugins';
import { I18nPlugin } from '@/plugins/i18n';
import { config } from '@vue/test-utils';
import { GlobalComponentsPlugin } from '@/plugins/components';
import { GlobalDirectivesPlugin } from '@/plugins/directives';
import { FontAwesomePlugin } from '@/plugins/icons';
configure({ testIdAttribute: 'data-test-id' });
// Vue.config.productionTip = false;
// Vue.config.devtools = false;
config.plugins.VueWrapper.install(I18nPlugin);
config.plugins.VueWrapper.install(FontAwesomePlugin);
config.plugins.VueWrapper.install(GlobalComponentsPlugin);
config.plugins.VueWrapper.install(GlobalDirectivesPlugin);
// TODO: Investigate why this is needed
// Without having this 3rd party library imported like this, any component test using 'vue-json-pretty' fail with:
// [Vue warn]: Failed to mount component: template or render function not defined.
config.stubs['vue-json-pretty'] = require('vue-json-pretty').default;
window.ResizeObserver =
window.ResizeObserver ||
vi.fn().mockImplementation(() => ({

View file

@ -1,7 +1,6 @@
import type { ISettingsState } from '@/Interface';
import { UserManagementAuthenticationMethod } from '@/Interface';
import { render } from '@testing-library/vue';
import { PiniaVuePlugin } from 'pinia';
export const retry = async (assertion: () => any, { interval = 20, timeout = 1000 } = {}) => {
return new Promise((resolve, reject) => {
@ -23,9 +22,7 @@ export const retry = async (assertion: () => any, { interval = 20, timeout = 100
type RenderParams = Parameters<typeof render>;
export const renderComponent = (Component: RenderParams[0], renderOptions: RenderParams[1] = {}) =>
render(Component, renderOptions, (vue) => {
vue.use(PiniaVuePlugin);
});
render(Component, renderOptions);
export const waitAllPromises = async () => new Promise((resolve) => setTimeout(resolve));

View file

@ -56,7 +56,7 @@
<script lang="ts">
import { defineComponent } from 'vue';
import { mapStores } from 'pinia';
import { createEventBus } from 'n8n-design-system';
import { createEventBus } from 'n8n-design-system/utils';
import Modal from './Modal.vue';
import { ABOUT_MODAL_KEY } from '../constants';
import { useSettingsStore } from '@/stores/settings.store';

View file

@ -37,7 +37,7 @@
<script lang="ts">
import { defineComponent } from 'vue';
import { mapStores } from 'pinia';
import { createEventBus } from 'n8n-design-system';
import { createEventBus } from 'n8n-design-system/utils';
import Modal from '@/components/Modal.vue';
import {

View file

@ -26,7 +26,7 @@
import { defineComponent } from 'vue';
import Modal from './Modal.vue';
import { ASK_AI_MODAL_KEY, ASK_AI_WAITLIST_URL } from '../constants';
import { createEventBus } from 'n8n-design-system';
import { createEventBus } from 'n8n-design-system/utils';
export default defineComponent({
name: 'AskAI',

View file

@ -37,7 +37,7 @@ import type { IFormInputs } from '@/Interface';
import { CHANGE_PASSWORD_MODAL_KEY } from '@/constants';
import { mapStores } from 'pinia';
import { useUsersStore } from '@/stores/users.store';
import { createEventBus } from 'n8n-design-system';
import { createEventBus } from 'n8n-design-system/utils';
export default defineComponent({
name: 'ChangePasswordModal',

View file

@ -91,7 +91,7 @@
<script lang="ts">
import { defineComponent } from 'vue';
import { mapStores } from 'pinia';
import { createEventBus } from 'n8n-design-system';
import { createEventBus } from 'n8n-design-system/utils';
import Modal from '@/components/Modal.vue';
import {
COMMUNITY_PACKAGE_INSTALL_MODAL_KEY,

View file

@ -39,7 +39,7 @@ import { COMMUNITY_PACKAGE_CONFIRM_MODAL_KEY, COMMUNITY_PACKAGE_MANAGE_ACTIONS }
import { useToast } from '@/composables';
import { mapStores } from 'pinia';
import { useCommunityNodesStore } from '@/stores/communityNodes.store';
import { createEventBus } from 'n8n-design-system';
import { createEventBus } from 'n8n-design-system/utils';
export default defineComponent({
name: 'CommunityPackageManageConfirmModal',

View file

@ -41,7 +41,7 @@ import { workflowHelpers } from '@/mixins/workflowHelpers';
import Modal from '@/components/Modal.vue';
import { useSettingsStore } from '@/stores/settings.store';
import { useRootStore } from '@/stores/n8nRoot.store';
import { createEventBus } from 'n8n-design-system';
import { createEventBus } from 'n8n-design-system/utils';
import { useToast } from '@/composables';
export default defineComponent({

View file

@ -144,7 +144,7 @@ import FeatureComingSoon from '@/components/FeatureComingSoon.vue';
import type { IPermissions } from '@/permissions';
import { getCredentialPermissions } from '@/permissions';
import type { IMenuItem } from 'n8n-design-system';
import { createEventBus } from 'n8n-design-system';
import { createEventBus } from 'n8n-design-system/utils';
import { useUIStore } from '@/stores/ui.store';
import { useSettingsStore } from '@/stores/settings.store';
import { useUsersStore } from '@/stores/users.store';

View file

@ -66,7 +66,7 @@ import { mapStores } from 'pinia';
import { useUIStore } from '@/stores/ui.store';
import { useWorkflowsStore } from '@/stores/workflows.store';
import { useCredentialsStore } from '@/stores/credentials.store';
import { createEventBus } from 'n8n-design-system';
import { createEventBus } from 'n8n-design-system/utils';
export default defineComponent({
name: 'CredentialsSelectModal',

View file

@ -84,7 +84,7 @@ import Modal from '@/components/Modal.vue';
import type { IUser } from '@/Interface';
import { mapStores } from 'pinia';
import { useUsersStore } from '@/stores/users.store';
import { createEventBus } from 'n8n-design-system';
import { createEventBus } from 'n8n-design-system/utils';
export default defineComponent({
name: 'DeleteUserModal',

View file

@ -62,7 +62,7 @@ import type { IWorkflowDataUpdate } from '@/Interface';
import type { IPermissions } from '@/permissions';
import { getWorkflowPermissions } from '@/permissions';
import { useUsersStore } from '@/stores/users.store';
import { createEventBus } from 'n8n-design-system';
import { createEventBus } from 'n8n-design-system/utils';
import { useCredentialsStore } from '@/stores';
export default defineComponent({

View file

@ -19,6 +19,7 @@ import type { Placement } from '@floating-ui/core';
export type ExecutionFilterProps = {
workflows?: IWorkflowShortResponse[];
popoverPlacement?: Placement;
teleported?: boolean;
};
const DATE_TIME_MASK = 'YYYY-MM-DD HH:mm';
@ -31,6 +32,7 @@ const telemetry = useTelemetry();
const props = withDefaults(defineProps<ExecutionFilterProps>(), {
popoverPlacement: 'bottom' as Placement,
teleported: true,
});
const emit = defineEmits<{
(event: 'filterChanged', value: ExecutionFilterType): void;
@ -165,7 +167,7 @@ onBeforeMount(() => {
</n8n-button>
</template>
<div data-test-id="execution-filter-form">
<div v-if="workflows?.length" :class="$style.group">
<div v-if="workflows && workflows.length > 0" :class="$style.group">
<label for="execution-filter-workflows">{{ locale.baseText('workflows.heading') }}</label>
<n8n-select
id="execution-filter-workflows"
@ -173,6 +175,7 @@ onBeforeMount(() => {
:placeholder="locale.baseText('executionsFilter.selectWorkflow')"
filterable
data-test-id="executions-filter-workflows-select"
:teleported="teleported"
>
<div class="ph-no-capture">
<n8n-option
@ -205,6 +208,7 @@ onBeforeMount(() => {
:placeholder="locale.baseText('executionsFilter.selectStatus')"
filterable
data-test-id="executions-filter-status-select"
:teleported="teleported"
>
<n8n-option
v-for="(item, idx) in statuses"

View file

@ -12,7 +12,7 @@
@update:modelValue="handleAutoRefreshToggle"
data-test-id="execution-auto-refresh-checkbox"
>
{{ $locale.baseText('executionsList.autoRefresh') }}
{{ i18n.baseText('executionsList.autoRefresh') }}
</el-checkbox>
<execution-filter
v-show="!isMounting"
@ -26,7 +26,7 @@
v-if="allVisibleSelected && finishedExecutionsCount > 0"
:class="$style.selectAll"
:label="
$locale.baseText('executionsList.selectAll', {
i18n.baseText('executionsList.selectAll', {
adjustToNumber: finishedExecutionsCount,
interpolate: { executionNum: finishedExecutionsCount },
})
@ -53,10 +53,10 @@
data-test-id="select-visible-executions-checkbox"
/>
</th>
<th>{{ $locale.baseText('executionsList.name') }}</th>
<th>{{ $locale.baseText('executionsList.startedAt') }}</th>
<th>{{ $locale.baseText('executionsList.status') }}</th>
<th>{{ $locale.baseText('executionsList.id') }}</th>
<th>{{ i18n.baseText('executionsList.name') }}</th>
<th>{{ i18n.baseText('executionsList.startedAt') }}</th>
<th>{{ i18n.baseText('executionsList.status') }}</th>
<th>{{ i18n.baseText('executionsList.id') }}</th>
<th></th>
<th></th>
<th></th>
@ -81,7 +81,7 @@
<td>
<span class="ph-no-capture" @click.stop="displayExecution(execution)"
><a href="#" :class="$style.link">{{
execution.workflowName || $locale.baseText('executionsList.unsavedWorkflow')
execution.workflowName || i18n.baseText('executionsList.unsavedWorkflow')
}}</a></span
>
</td>
@ -130,13 +130,13 @@
<span v-if="execution.retryOf">
<br />
<small>
({{ $locale.baseText('executionsList.retryOf') }} #{{ execution.retryOf }})
({{ i18n.baseText('executionsList.retryOf') }} #{{ execution.retryOf }})
</small>
</span>
<span v-else-if="execution.retrySuccessId">
<br />
<small>
({{ $locale.baseText('executionsList.successRetry') }} #{{
({{ i18n.baseText('executionsList.successRetry') }} #{{
execution.retrySuccessId
}})
</small>
@ -145,7 +145,7 @@
<td>
<n8n-tooltip v-if="execution.mode === 'manual'" placement="top">
<template #content>
<span>{{ $locale.baseText('executionsList.test') }}</span>
<span>{{ i18n.baseText('executionsList.test') }}</span>
</template>
<font-awesome-icon icon="flask" />
</n8n-tooltip>
@ -156,7 +156,7 @@
v-if="execution.stoppedAt !== undefined && execution.id"
size="small"
outline
:label="$locale.baseText('executionsList.view')"
:label="i18n.baseText('executionsList.view')"
@click.stop="displayExecution(execution)"
/>
</div>
@ -167,7 +167,7 @@
v-if="execution.stoppedAt === undefined || execution.waitTill"
size="small"
outline
:label="$locale.baseText('executionsList.stop')"
:label="i18n.baseText('executionsList.stop')"
@click.stop="stopExecution(execution.id)"
:loading="stoppingExecutions.includes(execution.id)"
/>
@ -184,7 +184,7 @@
text
type="tertiary"
size="mini"
:title="$locale.baseText('executionsList.retryExecution')"
:title="i18n.baseText('executionsList.retryExecution')"
icon="ellipsis-v"
/>
</span>
@ -200,20 +200,20 @@
:class="$style.retryAction"
:command="{ command: 'currentlySaved', execution }"
>
{{ $locale.baseText('executionsList.retryWithCurrentlySavedWorkflow') }}
{{ i18n.baseText('executionsList.retryWithCurrentlySavedWorkflow') }}
</el-dropdown-item>
<el-dropdown-item
v-if="isExecutionRetriable(execution)"
:class="$style.retryAction"
:command="{ command: 'original', execution }"
>
{{ $locale.baseText('executionsList.retryWithOriginalWorkflow') }}
{{ i18n.baseText('executionsList.retryWithOriginalWorkflow') }}
</el-dropdown-item>
<el-dropdown-item
:class="$style.deleteAction"
:command="{ command: 'delete', execution }"
>
{{ $locale.baseText('generic.delete') }}
{{ i18n.baseText('generic.delete') }}
</el-dropdown-item>
</el-dropdown-menu>
</template>
@ -228,7 +228,7 @@
:class="$style.loadedAll"
data-test-id="execution-list-empty"
>
{{ $locale.baseText('executionsList.empty') }}
{{ i18n.baseText('executionsList.empty') }}
</div>
<div
:class="$style.loadMore"
@ -238,8 +238,8 @@
>
<n8n-button
icon="sync"
:title="$locale.baseText('executionsList.loadMore')"
:label="$locale.baseText('executionsList.loadMore')"
:title="i18n.baseText('executionsList.loadMore')"
:label="i18n.baseText('executionsList.loadMore')"
@click="loadMore()"
:loading="isDataLoading"
data-test-id="load-more-button"
@ -250,7 +250,7 @@
:class="$style.loadedAll"
data-test-id="execution-all-loaded"
>
{{ $locale.baseText('executionsList.loadedAll') }}
{{ i18n.baseText('executionsList.loadedAll') }}
</div>
</div>
<div
@ -260,20 +260,20 @@
>
<span>
{{
$locale.baseText('executionsList.selected', {
i18n.baseText('executionsList.selected', {
adjustToNumber: numSelected,
interpolate: { numSelected },
})
}}
</span>
<n8n-button
:label="$locale.baseText('generic.delete')"
:label="i18n.baseText('generic.delete')"
type="tertiary"
@click="handleDeleteSelected"
data-test-id="delete-selected-button"
/>
<n8n-button
:label="$locale.baseText('executionsList.clearSelection')"
:label="i18n.baseText('executionsList.clearSelection')"
type="tertiary"
@click="handleClearSelection"
data-test-id="clear-selection-button"
@ -291,7 +291,7 @@ import { externalHooks } from '@/mixins/externalHooks';
import { MODAL_CONFIRM, VIEWS, WAIT_TIME_UNLIMITED } from '@/constants';
import { genericHelpers } from '@/mixins/genericHelpers';
import { executionHelpers } from '@/mixins/executionsHelpers';
import { useToast, useMessage } from '@/composables';
import { useToast, useMessage, useI18n, useTelemetry } from '@/composables';
import type {
IExecutionsCurrentSummaryExtended,
IExecutionDeleteFilter,
@ -321,7 +321,12 @@ export default defineComponent({
},
},
setup() {
const i18n = useI18n();
const telemetry = useTelemetry();
return {
i18n,
telemetry,
...useToast(),
...useMessage(),
};
@ -360,7 +365,7 @@ export default defineComponent({
await this.loadWorkflows();
void this.$externalHooks().run('executionsList.openDialog');
this.$telemetry.track('User opened Executions log', {
this.telemetry.track('User opened Executions log', {
workflow_id: this.workflowsStore.workflowId,
});
},
@ -406,7 +411,7 @@ export default defineComponent({
return executionFilterToQueryFilter(this.filter);
},
pageTitle() {
return this.$locale.baseText('executionsList.workflowExecutions');
return this.i18n.baseText('executionsList.workflowExecutions');
},
},
methods: {
@ -456,16 +461,14 @@ export default defineComponent({
},
async handleDeleteSelected() {
const deleteExecutions = await this.confirm(
this.$locale.baseText('executionsList.confirmMessage.message', {
this.i18n.baseText('executionsList.confirmMessage.message', {
interpolate: { numSelected: this.numSelected.toString() },
}),
this.$locale.baseText('executionsList.confirmMessage.headline'),
this.i18n.baseText('executionsList.confirmMessage.headline'),
{
type: 'warning',
confirmButtonText: this.$locale.baseText(
'executionsList.confirmMessage.confirmButtonText',
),
cancelButtonText: this.$locale.baseText('executionsList.confirmMessage.cancelButtonText'),
confirmButtonText: this.i18n.baseText('executionsList.confirmMessage.confirmButtonText'),
cancelButtonText: this.i18n.baseText('executionsList.confirmMessage.cancelButtonText'),
},
);
@ -490,7 +493,7 @@ export default defineComponent({
this.isDataLoading = false;
this.showError(
error,
this.$locale.baseText('executionsList.showError.handleDeleteSelected.title'),
this.i18n.baseText('executionsList.showError.handleDeleteSelected.title'),
);
return;
@ -498,7 +501,7 @@ export default defineComponent({
this.isDataLoading = false;
this.showMessage({
title: this.$locale.baseText('executionsList.showMessage.handleDeleteSelected.title'),
title: this.i18n.baseText('executionsList.showMessage.handleDeleteSelected.title'),
type: 'success',
});
@ -525,7 +528,7 @@ export default defineComponent({
await this.retryExecution(commandData.execution, loadWorkflow);
this.$telemetry.track('User clicked retry execution button', {
this.telemetry.track('User clicked retry execution button', {
workflow_id: this.workflowsStore.workflowId,
execution_id: commandData.execution.id,
retry_type: loadWorkflow ? 'current' : 'original',
@ -691,7 +694,7 @@ export default defineComponent({
);
} catch (error) {
this.isDataLoading = false;
this.showError(error, this.$locale.baseText('executionsList.showError.loadMore.title'));
this.showError(error, this.i18n.baseText('executionsList.showError.loadMore.title'));
return;
}
@ -726,15 +729,12 @@ export default defineComponent({
workflows.unshift({
id: 'all',
name: this.$locale.baseText('executionsList.allWorkflows'),
name: this.i18n.baseText('executionsList.allWorkflows'),
} as IWorkflowShortResponse);
this.workflows = workflows;
} catch (error) {
this.showError(
error,
this.$locale.baseText('executionsList.showError.loadWorkflows.title'),
);
this.showError(error, this.i18n.baseText('executionsList.showError.loadWorkflows.title'));
}
},
async retryExecution(execution: IExecutionsSummary, loadWorkflow?: boolean) {
@ -748,22 +748,19 @@ export default defineComponent({
if (retrySuccessful) {
this.showMessage({
title: this.$locale.baseText('executionsList.showMessage.retrySuccessfulTrue.title'),
title: this.i18n.baseText('executionsList.showMessage.retrySuccessfulTrue.title'),
type: 'success',
});
} else {
this.showMessage({
title: this.$locale.baseText('executionsList.showMessage.retrySuccessfulFalse.title'),
title: this.i18n.baseText('executionsList.showMessage.retrySuccessfulFalse.title'),
type: 'error',
});
}
this.isDataLoading = false;
} catch (error) {
this.showError(
error,
this.$locale.baseText('executionsList.showError.retryExecution.title'),
);
this.showError(error, this.i18n.baseText('executionsList.showError.retryExecution.title'));
this.isDataLoading = false;
}
@ -774,7 +771,7 @@ export default defineComponent({
try {
await Promise.all([this.loadActiveExecutions(), this.loadFinishedExecutions()]);
} catch (error) {
this.showError(error, this.$locale.baseText('executionsList.showError.refreshData.title'));
this.showError(error, this.i18n.baseText('executionsList.showError.refreshData.title'));
}
this.isDataLoading = false;
@ -807,21 +804,21 @@ export default defineComponent({
let text = '';
if (status === 'waiting') {
text = this.$locale.baseText('executionsList.waiting');
text = this.i18n.baseText('executionsList.waiting');
} else if (status === 'canceled') {
text = this.$locale.baseText('executionsList.canceled');
text = this.i18n.baseText('executionsList.canceled');
} else if (status === 'crashed') {
text = this.$locale.baseText('executionsList.error');
text = this.i18n.baseText('executionsList.error');
} else if (status === 'new') {
text = this.$locale.baseText('executionsList.running');
text = this.i18n.baseText('executionsList.running');
} else if (status === 'running') {
text = this.$locale.baseText('executionsList.running');
text = this.i18n.baseText('executionsList.running');
} else if (status === 'success') {
text = this.$locale.baseText('executionsList.succeeded');
text = this.i18n.baseText('executionsList.succeeded');
} else if (status === 'failed') {
text = this.$locale.baseText('executionsList.error');
text = this.i18n.baseText('executionsList.error');
} else {
text = this.$locale.baseText('executionsList.unknown');
text = this.i18n.baseText('executionsList.unknown');
}
return text;
@ -855,7 +852,7 @@ export default defineComponent({
let text = '';
if (status === 'waiting' && this.isWaitTillIndefinite(entry)) {
text = this.$locale.baseText(
text = this.i18n.baseText(
'executionsList.statusTooltipText.theWorkflowIsWaitingIndefinitely',
);
}
@ -875,8 +872,8 @@ export default defineComponent({
this.stoppingExecutions.splice(index, 1);
this.showMessage({
title: this.$locale.baseText('executionsList.showMessage.stopExecution.title'),
message: this.$locale.baseText('executionsList.showMessage.stopExecution.message', {
title: this.i18n.baseText('executionsList.showMessage.stopExecution.title'),
message: this.i18n.baseText('executionsList.showMessage.stopExecution.message', {
interpolate: { activeExecutionId },
}),
type: 'success',
@ -884,10 +881,7 @@ export default defineComponent({
await this.refreshData();
} catch (error) {
this.showError(
error,
this.$locale.baseText('executionsList.showError.stopExecution.title'),
);
this.showError(error, this.i18n.baseText('executionsList.showError.stopExecution.title'));
}
},
isExecutionRetriable(execution: IExecutionsSummary): boolean {
@ -912,7 +906,7 @@ export default defineComponent({
} catch (error) {
this.showError(
error,
this.$locale.baseText('executionsList.showError.handleDeleteSelected.title'),
this.i18n.baseText('executionsList.showError.handleDeleteSelected.title'),
);
}
this.isDataLoading = true;

View file

@ -11,7 +11,7 @@ import { defineComponent } from 'vue';
import ExecutionsList from '@/components/ExecutionsList.vue';
import Modal from '@/components/Modal.vue';
import { EXECUTIONS_MODAL_KEY } from '@/constants';
import { createEventBus } from 'n8n-design-system';
import { createEventBus } from 'n8n-design-system/utils';
export default defineComponent({
name: 'ExecutionsModal',

View file

@ -52,7 +52,7 @@ import type { INodeUi } from '@/Interface';
import { mapStores } from 'pinia';
import { useUIStore } from '@/stores/ui.store';
import { useNDVStore } from '@/stores/ndv.store';
import { createEventBus } from 'n8n-design-system';
import { createEventBus } from 'n8n-design-system/utils';
export default defineComponent({
name: 'ImportCurlModal',

View file

@ -1,18 +1,18 @@
<template>
<div :class="visible ? $style.dropdown : $style.hidden">
<n8n-text size="small" compact :class="$style.header">
{{ $locale.baseText('parameterInput.resultForItem') }} {{ hoveringItemNumber }}
{{ i18n.baseText('parameterInput.resultForItem') }} {{ hoveringItemNumber }}
</n8n-text>
<n8n-text :class="$style.body">
<div ref="root" class="ph-no-capture" data-test-id="inline-expression-editor-output"></div>
</n8n-text>
<div :class="$style.footer">
<n8n-text size="small" compact>
{{ $locale.baseText('parameterInput.anythingInside') }}
{{ i18n.baseText('parameterInput.anythingInside') }}
</n8n-text>
<div :class="$style['expression-syntax-example']" v-text="`{{ }}`"></div>
<n8n-text size="small" compact>
{{ $locale.baseText('parameterInput.isJavaScript') }}
{{ i18n.baseText('parameterInput.isJavaScript') }}
</n8n-text>
{{ ' ' }}
<n8n-link
@ -22,7 +22,7 @@
theme="text"
:to="expressionsDocsUrl"
>
{{ $locale.baseText('parameterInput.learnMore') }}
{{ i18n.baseText('parameterInput.learnMore') }}
</n8n-link>
</div>
</div>
@ -40,6 +40,7 @@ import { outputTheme } from './theme';
import type { Plaintext, Resolved, Segment } from '@/types/expressions';
import { EXPRESSIONS_DOCS_URL } from '@/constants';
import { useI18n } from '@/composables';
export default defineComponent({
name: 'InlineExpressionEditorOutput',
@ -73,6 +74,13 @@ export default defineComponent({
highlighter.removeColor(this.editor, this.plaintextSegments);
},
},
setup() {
const i18n = useI18n();
return {
i18n,
};
},
data() {
return {
editor: null as EditorView | null,

View file

@ -24,7 +24,7 @@
import { defineComponent } from 'vue';
import ExpandableInputEdit from '@/components/ExpandableInput/ExpandableInputEdit.vue';
import ExpandableInputPreview from '@/components/ExpandableInput/ExpandableInputPreview.vue';
import { createEventBus } from 'n8n-design-system';
import { createEventBus } from 'n8n-design-system/utils';
export default defineComponent({
name: 'InlineTextEdit',

View file

@ -62,7 +62,7 @@ import { VALID_EMAIL_REGEX, INVITE_USER_MODAL_KEY } from '@/constants';
import { ROLE } from '@/utils';
import { useUsersStore } from '@/stores/users.store';
import { useSettingsStore } from '@/stores/settings.store';
import { createEventBus } from 'n8n-design-system';
import { createEventBus } from 'n8n-design-system/utils';
const NAME_EMAIL_FORMAT_REGEX = /^.* <(.*)>$/;

View file

@ -164,7 +164,7 @@ import {
} from '@/stores';
import type { IPermissions } from '@/permissions';
import { getWorkflowPermissions } from '@/permissions';
import { createEventBus } from 'n8n-design-system';
import { createEventBus } from 'n8n-design-system/utils';
import { useCloudPlanStore } from '@/stores';
import { nodeViewEventBus } from '@/event-bus';

View file

@ -3,7 +3,8 @@ import { computed, ref } from 'vue';
import { useRouter } from 'vue-router';
import { createEventBus } from 'n8n-design-system/utils';
import { useI18n, useLoadingService, useMessage, useToast } from '@/composables';
import { useUIStore, useSourceControlStore } from '@/stores';
import { useUIStore } from '@/stores/ui.store';
import { useSourceControlStore } from '@/stores/sourceControl.store';
import { SOURCE_CONTROL_PULL_MODAL_KEY, SOURCE_CONTROL_PUSH_MODAL_KEY, VIEWS } from '@/constants';
const props = defineProps<{
@ -20,7 +21,7 @@ const uiStore = useUIStore();
const sourceControlStore = useSourceControlStore();
const message = useMessage();
const toast = useToast();
const { i18n } = useI18n();
const i18n = useI18n();
const eventBus = createEventBus();
const tooltipOpenDelay = ref(300);

View file

@ -18,7 +18,7 @@
<p
:class="$style.communityNodeIcon"
v-html="
$locale.baseText('generic.communityNode.tooltip', {
i18n.baseText('generic.communityNode.tooltip', {
interpolate: {
packageName: nodeType.name.split('.')[0],
docURL: COMMUNITY_NODES_INSTALLATION_DOCS_URL,
@ -38,7 +38,7 @@
</template>
<script setup lang="ts">
import { computed, ref, getCurrentInstance } from 'vue';
import { computed, ref } from 'vue';
import type { SimplifiedNodeType } from '@/Interface';
import { COMMUNITY_NODES_INSTALLATION_DOCS_URL, DEFAULT_SUBCATEGORY } from '@/constants';
@ -48,6 +48,7 @@ import { useNodeCreatorStore } from '@/stores/nodeCreator.store';
import NodeIcon from '@/components/NodeIcon.vue';
import { useActions } from '../composables/useActions';
import { useI18n, useTelemetry } from '@/composables';
export interface Props {
nodeType: SimplifiedNodeType;
@ -59,16 +60,18 @@ const props = withDefaults(defineProps<Props>(), {
active: false,
});
const i18n = useI18n();
const telemetry = useTelemetry();
const { actions } = useNodeCreatorStore();
const { getNodeTypesWithManualTrigger } = useActions();
const instance = getCurrentInstance();
const dragging = ref(false);
const draggablePosition = ref({ x: -100, y: -100 });
const draggableDataTransfer = ref(null as Element | null);
const description = computed<string>(() => {
return instance?.proxy.$locale.headerText({
return i18n.headerText({
key: `headers.${shortNodeType.value}.description`,
fallback: props.nodeType.description,
}) as string;
@ -84,9 +87,7 @@ const nodeActions = computed(() => {
return nodeActions;
});
const shortNodeType = computed<string>(
() => instance?.proxy.$locale.shortNodeType(props.nodeType.name) || '',
);
const shortNodeType = computed<string>(() => i18n.shortNodeType(props.nodeType.name) || '');
const draggableStyle = computed<{ top: string; left: string }>(() => ({
top: `${draggablePosition.value.y}px`,
@ -99,7 +100,7 @@ const isCommunityNode = computed<boolean>(() => isCommunityPackageName(props.nod
const displayName = computed<any>(() => {
const displayName = props.nodeType.displayName.trimEnd();
return instance?.proxy.$locale.headerText({
return i18n.headerText({
key: `headers.${shortNodeType.value}.displayName`,
fallback: hasActions.value ? displayName.replace('Trigger', '') : displayName,
});
@ -153,7 +154,7 @@ function onDragEnd(event: DragEvent): void {
function onCommunityNodeTooltipClick(event: MouseEvent) {
if ((event.target as Element).localName === 'a') {
instance?.proxy.$telemetry.track('user clicked cnr docs link', { source: 'nodes panel node' });
telemetry.track('user clicked cnr docs link', { source: 'nodes panel node' });
}
}
</script>

View file

@ -1,9 +1,9 @@
<template>
<n8n-node-creator-node
:class="$style.subCategory"
:title="$locale.baseText(`nodeCreator.subcategoryNames.${subcategoryName}`)"
:title="i18n.baseText(`nodeCreator.subcategoryNames.${subcategoryName}`)"
:isTrigger="false"
:description="$locale.baseText(`nodeCreator.subcategoryDescriptions.${subcategoryName}`)"
:description="i18n.baseText(`nodeCreator.subcategoryDescriptions.${subcategoryName}`)"
:showActionArrow="true"
>
<template #icon>
@ -16,11 +16,14 @@
import type { SubcategoryItemProps } from '@/Interface';
import { camelCase } from 'lodash-es';
import { computed } from 'vue';
import { useI18n } from '@/composables';
export interface Props {
item: SubcategoryItemProps;
}
const props = defineProps<Props>();
const i18n = useI18n();
const subcategoryName = computed(() => camelCase(props.item.subcategory || props.item.title));
</script>

View file

@ -1,6 +1,6 @@
<script setup lang="ts">
import { camelCase } from 'lodash-es';
import { getCurrentInstance, computed } from 'vue';
import { computed } from 'vue';
import type { INodeCreateElement, NodeFilterType } from '@/Interface';
import { TRIGGER_NODE_CREATOR_VIEW, HTTP_REQUEST_NODE_TYPE, WEBHOOK_NODE_TYPE } from '@/constants';
@ -16,6 +16,7 @@ import { useKeyboardNavigation } from '../composables/useKeyboardNavigation';
import ItemsRenderer from '../Renderers/ItemsRenderer.vue';
import CategorizedItemsRenderer from '../Renderers/CategorizedItemsRenderer.vue';
import NoResults from '../Panel/NoResults.vue';
import { useI18n, useTelemetry } from '@/composables';
export interface Props {
rootView: 'trigger' | 'action';
@ -25,7 +26,9 @@ const emit = defineEmits({
nodeTypeSelected: (nodeTypes: string[]) => true,
});
const instance = getCurrentInstance();
const i18n = useI18n();
const telemetry = useTelemetry();
const { mergedNodes, actions } = useNodeCreatorStore();
const { baseUrl } = useRootStore();
const { getNodeTypesWithManualTrigger } = useActions();
@ -45,7 +48,7 @@ function selectNodeType(nodeTypes: string[]) {
function onSelected(item: INodeCreateElement) {
if (item.type === 'subcategory') {
const title = instance?.proxy.$locale.baseText(
const title = i18n.baseText(
`nodeCreator.subcategoryNames.${camelCase(item.properties.title)}` as BaseTextKey,
);
@ -59,7 +62,7 @@ function onSelected(item: INodeCreateElement) {
itemsMapper: subcategoriesMapper,
});
instance?.proxy.$telemetry.trackNodesPanel('nodeCreateList.onSubcategorySelected', {
telemetry.trackNodesPanel('nodeCreateList.onSubcategorySelected', {
subcategory: item.key,
});
}
@ -96,10 +99,7 @@ function onSelected(item: INodeCreateElement) {
}
if (item.type === 'view') {
const view =
item.key === TRIGGER_NODE_CREATOR_VIEW
? TriggerView(instance?.proxy?.$locale)
: RegularView(instance?.proxy?.$locale);
const view = item.key === TRIGGER_NODE_CREATOR_VIEW ? TriggerView() : RegularView();
pushViewStack({
title: view.title,

View file

@ -1,5 +1,5 @@
<script setup lang="ts">
import { getCurrentInstance, computed, onMounted, onUnmounted, watch } from 'vue';
import { computed, onMounted, onUnmounted, watch } from 'vue';
import type { INodeCreateElement } from '@/Interface';
import { TRIGGER_NODE_CREATOR_VIEW } from '@/constants';
@ -11,8 +11,9 @@ import { useKeyboardNavigation } from '../composables/useKeyboardNavigation';
import SearchBar from './SearchBar.vue';
import ActionsRenderer from '../Modes/ActionsMode.vue';
import NodesRenderer from '../Modes/NodesMode.vue';
import { useI18n } from '@/composables';
const instance = getCurrentInstance();
const i18n = useI18n();
const { mergedNodes } = useNodeCreatorStore();
const { pushViewStack, popViewStack, updateCurrentViewStack } = useViewStacks();
@ -25,10 +26,10 @@ const viewStacks = computed(() => useViewStacks().viewStacks);
const isActionsMode = computed(() => useViewStacks().activeViewStackMode === 'actions');
const searchPlaceholder = computed(() =>
isActionsMode.value
? instance?.proxy?.$locale.baseText('nodeCreator.actionsCategory.searchActions', {
? i18n.baseText('nodeCreator.actionsCategory.searchActions', {
interpolate: { node: activeViewStack.value.title as string },
})
: instance?.proxy?.$locale.baseText('nodeCreator.searchBar.searchNodes'),
: i18n.baseText('nodeCreator.searchBar.searchNodes'),
);
const nodeCreatorView = computed(() => useNodeCreatorStore().selectedView);
@ -58,10 +59,7 @@ onUnmounted(() => {
watch(
() => nodeCreatorView.value,
(selectedView) => {
const view =
selectedView === TRIGGER_NODE_CREATOR_VIEW
? TriggerView(instance?.proxy?.$locale)
: RegularView(instance?.proxy?.$locale);
const view = selectedView === TRIGGER_NODE_CREATOR_VIEW ? TriggerView() : RegularView();
pushViewStack({
title: view.title,

View file

@ -1,5 +1,4 @@
import Vue from 'vue';
import { PiniaVuePlugin } from 'pinia';
import { nextTick } from 'vue';
import { createTestingPinia } from '@pinia/testing';
import { render, fireEvent } from '@testing-library/vue';
import {
@ -24,18 +23,16 @@ describe('ItemsRenderer', () => {
mockNodeCreateElement('subcategory', { displayName: 'Node 3', name: 'node3' }),
mockSubcategoryCreateElement({ title: 'Subcategory 2' }),
];
const { container } = render(
ItemsRenderer,
{
pinia: createTestingPinia(),
props: { elements: items },
const { container } = render(ItemsRenderer, {
store: createTestingPinia(),
props: { elements: items },
global: {
stubs: ['n8n-loading'],
},
(vue) => {
vue.use(PiniaVuePlugin);
},
);
//
await Vue.nextTick();
});
await nextTick();
const nodeItems = container.querySelectorAll('.iteratorItem .nodeItem');
const labels = container.querySelectorAll('.iteratorItem .label');
@ -53,18 +50,12 @@ describe('ItemsRenderer', () => {
mockActionCreateElement(),
mockViewCreateElement(),
];
const { container, emitted } = render(
ItemsRenderer,
{
pinia: createTestingPinia(),
props: { elements: items },
},
(vue) => {
vue.use(PiniaVuePlugin);
},
);
const { container, emitted } = render(ItemsRenderer, {
pinia: createTestingPinia(),
props: { elements: items },
});
//
await Vue.nextTick();
await nextTick();
const itemTypes = {
node: container.querySelector('.iteratorItem .nodeItem'),

View file

@ -1,6 +1,6 @@
import Vue, { defineComponent, watch } from 'vue';
import { defineComponent, nextTick, watch } from 'vue';
import type { PropType } from 'vue';
import { PiniaVuePlugin, createPinia } from 'pinia';
import { createPinia } from 'pinia';
import { render, screen, fireEvent } from '@testing-library/vue';
import type { INodeTypeDescription } from 'n8n-workflow';
import { useNodeCreatorStore } from '@/stores/nodeCreator.store';
@ -9,23 +9,6 @@ import NodesListPanel from '../Panel/NodesListPanel.vue';
import { REGULAR_NODE_CREATOR_VIEW } from '@/constants';
import type { NodeFilterType } from '@/Interface';
function TelemetryPlugin(vue: typeof Vue): void {
Object.defineProperty(vue, '$telemetry', {
get() {
return {
trackNodesPanel: () => {},
};
},
});
Object.defineProperty(vue.prototype, '$telemetry', {
get() {
return {
trackNodesPanel: () => {},
};
},
});
}
function getWrapperComponent(setup: () => void) {
const wrapperComponent = defineComponent({
props: {
@ -41,16 +24,11 @@ function getWrapperComponent(setup: () => void) {
template: '<NodesListPanel @nodeTypeSelected="e => $emit(\'nodeTypeSelected\', e)" />',
});
return render(
wrapperComponent,
{
pinia: createPinia(),
return render(wrapperComponent, {
global: {
plugins: [createPinia()],
},
(vue) => {
vue.use(PiniaVuePlugin);
vue.use(TelemetryPlugin);
},
);
});
}
describe('NodesListPanel', () => {
@ -78,11 +56,11 @@ describe('NodesListPanel', () => {
return {};
});
await Vue.nextTick();
await nextTick();
expect(screen.getByText('Select a trigger')).toBeInTheDocument();
expect(screen.queryByTestId('node-creator-search-bar')).toBeInTheDocument();
screen.getByText('On app event').click();
await Vue.nextTick();
await nextTick();
expect(screen.queryByTestId('node-creator-search-bar')).not.toBeInTheDocument();
mockedTriggerNodes.forEach((n) => {
expect(screen.queryByText(n.name)).toBeInTheDocument();
@ -95,7 +73,7 @@ describe('NodesListPanel', () => {
expect(container.querySelector('.backButton')).toBeInTheDocument();
await fireEvent.click(container.querySelector('.backButton')!);
await Vue.nextTick();
await nextTick();
expect(screen.queryAllByTestId('item-iterator-item')).toHaveLength(6);
});
@ -145,27 +123,20 @@ describe('NodesListPanel', () => {
template: '<NodesListPanel @nodeTypeSelected="e => $emit(\'nodeTypeSelected\', e)" />',
});
render(
wrapperComponent,
{
pinia: createPinia(),
props: {
nodeTypes: mockedNodes,
selectedView: REGULAR_NODE_CREATOR_VIEW,
},
render(wrapperComponent, {
pinia: createPinia(),
props: {
nodeTypes: mockedNodes,
selectedView: REGULAR_NODE_CREATOR_VIEW,
},
(vue) => {
vue.use(PiniaVuePlugin);
vue.use(TelemetryPlugin);
},
);
});
await Vue.nextTick();
await nextTick();
expect(screen.getByText('What happens next?')).toBeInTheDocument();
expect(screen.queryAllByTestId('item-iterator-item')).toHaveLength(6);
screen.getByText('Action in an app').click();
await Vue.nextTick();
await nextTick();
mockedNodes.forEach((n) => {
expect(screen.queryByText(n.displayName)).toBeInTheDocument();
});
@ -206,39 +177,32 @@ describe('NodesListPanel', () => {
});
function renderComponent() {
return render(
wrapperComponent,
{
pinia: createPinia(),
props: {
nodeTypes: mockedNodes,
},
return render(wrapperComponent, {
pinia: createPinia(),
props: {
nodeTypes: mockedNodes,
},
(vue) => {
vue.use(PiniaVuePlugin);
vue.use(TelemetryPlugin);
},
);
});
}
it('should be visible in the root view', async () => {
renderComponent();
await Vue.nextTick();
await nextTick();
expect(screen.queryByTestId('node-creator-search-bar')).toBeInTheDocument();
});
it('should not be visible if subcategory contains less than 9 items', async () => {
renderComponent();
await Vue.nextTick();
await nextTick();
screen.getByText('On app event').click();
await Vue.nextTick();
await nextTick();
expect(screen.queryByTestId('node-creator-search-bar')).not.toBeInTheDocument();
expect(screen.queryAllByTestId('item-iterator-item')).toHaveLength(8);
});
it('should be visible if subcategory contains 9 or more items', async () => {
const { updateProps } = renderComponent();
await Vue.nextTick();
await nextTick();
mockedNodes.push(
mockSimplifiedNodeType({
@ -249,10 +213,10 @@ describe('NodesListPanel', () => {
);
await updateProps({ nodeTypes: [...mockedNodes] });
await Vue.nextTick();
await nextTick();
screen.getByText('On app event').click();
await Vue.nextTick();
await nextTick();
expect(screen.queryAllByTestId('item-iterator-item')).toHaveLength(9);
expect(screen.queryByTestId('node-creator-search-bar')).toBeInTheDocument();
@ -260,26 +224,26 @@ describe('NodesListPanel', () => {
it('should correctly handle search', async () => {
const { container } = renderComponent();
await Vue.nextTick();
await nextTick();
screen.getByText('On app event').click();
await Vue.nextTick();
await nextTick();
await fireEvent.input(screen.getByTestId('node-creator-search-bar'), {
target: { value: 'Ninth' },
});
await Vue.nextTick();
await nextTick();
expect(screen.queryAllByTestId('item-iterator-item')).toHaveLength(1);
await fireEvent.input(screen.getByTestId('node-creator-search-bar'), {
target: { value: 'Non sense' },
});
await Vue.nextTick();
await nextTick();
expect(screen.queryAllByTestId('item-iterator-item')).toHaveLength(0);
expect(screen.queryByText("We didn't make that... yet")).toBeInTheDocument();
await fireEvent.click(container.querySelector('.clear')!);
await Vue.nextTick();
await nextTick();
expect(screen.queryAllByTestId('item-iterator-item')).toHaveLength(9);
});
});

View file

@ -2,7 +2,7 @@ import { render } from '@testing-library/vue';
import userEvent from '@testing-library/user-event';
import { defineComponent, computed } from 'vue';
import { useKeyboardNavigation } from '../composables/useKeyboardNavigation';
import { PiniaVuePlugin, createPinia } from 'pinia';
import { createPinia } from 'pinia';
const eventHookSpy = vi.fn();
describe('useKeyboardNavigation', () => {
@ -39,9 +39,7 @@ describe('useKeyboardNavigation', () => {
});
const renderTestComponent = () => {
return render(TestComponent, { pinia: createPinia() }, (vue) => {
vue.use(PiniaVuePlugin);
});
return render(TestComponent, { pinia: createPinia() });
};
afterAll(() => {

View file

@ -14,12 +14,15 @@ import {
EMAIL_IMAP_NODE_TYPE,
DEFAULT_SUBCATEGORY,
} from '@/constants';
import { useI18n } from '@/composables';
export function TriggerView() {
const i18n = useI18n();
export function TriggerView($locale: any) {
return {
value: TRIGGER_NODE_CREATOR_VIEW,
title: $locale.baseText('nodeCreator.triggerHelperPanel.selectATrigger'),
subtitle: $locale.baseText('nodeCreator.triggerHelperPanel.selectATriggerDescription'),
title: i18n.baseText('nodeCreator.triggerHelperPanel.selectATrigger'),
subtitle: i18n.baseText('nodeCreator.triggerHelperPanel.selectATriggerDescription'),
items: [
{
key: DEFAULT_SUBCATEGORY,
@ -37,12 +40,8 @@ export function TriggerView($locale: any) {
properties: {
group: [],
name: SCHEDULE_TRIGGER_NODE_TYPE,
displayName: $locale.baseText(
'nodeCreator.triggerHelperPanel.scheduleTriggerDisplayName',
),
description: $locale.baseText(
'nodeCreator.triggerHelperPanel.scheduleTriggerDescription',
),
displayName: i18n.baseText('nodeCreator.triggerHelperPanel.scheduleTriggerDisplayName'),
description: i18n.baseText('nodeCreator.triggerHelperPanel.scheduleTriggerDescription'),
icon: 'fa:clock',
},
},
@ -53,8 +52,8 @@ export function TriggerView($locale: any) {
properties: {
group: [],
name: WEBHOOK_NODE_TYPE,
displayName: $locale.baseText('nodeCreator.triggerHelperPanel.webhookTriggerDisplayName'),
description: $locale.baseText('nodeCreator.triggerHelperPanel.webhookTriggerDescription'),
displayName: i18n.baseText('nodeCreator.triggerHelperPanel.webhookTriggerDisplayName'),
description: i18n.baseText('nodeCreator.triggerHelperPanel.webhookTriggerDescription'),
iconData: {
type: 'file',
icon: 'webhook',
@ -69,8 +68,8 @@ export function TriggerView($locale: any) {
properties: {
group: [],
name: MANUAL_TRIGGER_NODE_TYPE,
displayName: $locale.baseText('nodeCreator.triggerHelperPanel.manualTriggerDisplayName'),
description: $locale.baseText('nodeCreator.triggerHelperPanel.manualTriggerDescription'),
displayName: i18n.baseText('nodeCreator.triggerHelperPanel.manualTriggerDisplayName'),
description: i18n.baseText('nodeCreator.triggerHelperPanel.manualTriggerDescription'),
icon: 'fa:mouse-pointer',
},
},
@ -81,12 +80,8 @@ export function TriggerView($locale: any) {
properties: {
group: [],
name: EXECUTE_WORKFLOW_TRIGGER_NODE_TYPE,
displayName: $locale.baseText(
'nodeCreator.triggerHelperPanel.workflowTriggerDisplayName',
),
description: $locale.baseText(
'nodeCreator.triggerHelperPanel.workflowTriggerDescription',
),
displayName: i18n.baseText('nodeCreator.triggerHelperPanel.workflowTriggerDisplayName'),
description: i18n.baseText('nodeCreator.triggerHelperPanel.workflowTriggerDescription'),
icon: 'fa:sign-out-alt',
},
},
@ -103,10 +98,12 @@ export function TriggerView($locale: any) {
};
}
export function RegularView($locale: any) {
export function RegularView() {
const i18n = useI18n();
return {
value: REGULAR_NODE_CREATOR_VIEW,
title: $locale.baseText('nodeCreator.triggerHelperPanel.whatHappensNext'),
title: i18n.baseText('nodeCreator.triggerHelperPanel.whatHappensNext'),
items: [
{
key: DEFAULT_SUBCATEGORY,
@ -156,11 +153,9 @@ export function RegularView($locale: any) {
key: TRIGGER_NODE_CREATOR_VIEW,
type: 'view',
properties: {
title: $locale.baseText('nodeCreator.triggerHelperPanel.addAnotherTrigger'),
title: i18n.baseText('nodeCreator.triggerHelperPanel.addAnotherTrigger'),
icon: 'bolt',
description: $locale.baseText(
'nodeCreator.triggerHelperPanel.addAnotherTriggerDescription',
),
description: i18n.baseText('nodeCreator.triggerHelperPanel.addAnotherTriggerDescription'),
},
},
],

View file

@ -126,7 +126,7 @@
<script lang="ts">
import { defineComponent } from 'vue';
import { mapStores } from 'pinia';
import { createEventBus } from 'n8n-design-system';
import { createEventBus } from 'n8n-design-system/utils';
import type {
INodeConnections,
INodeTypeDescription,

View file

@ -525,7 +525,7 @@ export default defineComponent({
}
}
// Set the value via Vue.set that everything updates correctly in the UI
// Set the value so that everything updates correctly in the UI
if (nameParts.length === 0) {
// Data is on top level
if (value === null) {

View file

@ -53,7 +53,7 @@ import { defineComponent } from 'vue';
import { useToast } from '@/composables';
import { mapStores } from 'pinia';
import { useUIStore } from '@/stores/ui.store';
import { createEventBus } from 'n8n-design-system';
import { createEventBus } from 'n8n-design-system/utils';
export default defineComponent({
name: 'OnboardingCallSignupModal',

View file

@ -63,7 +63,7 @@
append-to-body
:close-on-click-modal="false"
width="80%"
:title="`${$locale.baseText('codeEdit.edit')} ${$locale
:title="`${i18n.baseText('codeEdit.edit')} ${$locale
.nodeText()
.inputLabelDisplayName(parameter, path)}`"
:before-close="closeCodeEditDialog"
@ -157,7 +157,7 @@
focused: isFocused,
invalid: !isFocused && getIssues.length > 0 && !isValueExpression,
}"
:title="$locale.baseText('parameterInput.openEditWindow')"
:title="i18n.baseText('parameterInput.openEditWindow')"
@click="displayEditDialog()"
@focus="setFocus"
/>
@ -201,7 +201,7 @@
:placeholder="
parameter.placeholder
? getPlaceholder()
: $locale.baseText('parameterInput.selectDateAndTime')
: i18n.baseText('parameterInput.selectDateAndTime')
"
:picker-options="dateTimePickerOptions"
@update:modelValue="valueChanged"
@ -255,7 +255,7 @@
filterable
:modelValue="displayValue"
:placeholder="
parameter.placeholder ? getPlaceholder() : $locale.baseText('parameterInput.select')
parameter.placeholder ? getPlaceholder() : i18n.baseText('parameterInput.select')
"
:loading="remoteParameterOptionsLoading"
:disabled="isReadOnly || remoteParameterOptionsLoading"
@ -297,7 +297,7 @@
:loading="remoteParameterOptionsLoading"
:disabled="isReadOnly || remoteParameterOptionsLoading"
:title="displayTitle"
:placeholder="$locale.baseText('parameterInput.select')"
:placeholder="i18n.baseText('parameterInput.select')"
@update:modelValue="valueChanged"
@keydown.stop
@focus="setFocus"
@ -392,6 +392,7 @@ import { useSettingsStore } from '@/stores/settings.store';
import { htmlEditorEventBus } from '@/event-bus';
import type { EventBus } from 'n8n-design-system/utils';
import { createEventBus } from 'n8n-design-system/utils';
import { useI18n } from '@/composables';
export default defineComponent({
name: 'parameter-input',
@ -467,6 +468,13 @@ export default defineComponent({
default: () => createEventBus(),
},
},
setup() {
const i18n = useI18n();
return {
i18n,
};
},
data() {
return {
codeEditDialogVisible: false,
@ -586,17 +594,14 @@ export default defineComponent({
const interpolation = { interpolate: { shortPath: this.shortPath } };
if (this.getIssues.length && this.isValueExpression) {
return this.$locale.baseText(
'parameterInput.parameterHasIssuesAndExpression',
interpolation,
);
return this.i18n.baseText('parameterInput.parameterHasIssuesAndExpression', interpolation);
} else if (this.getIssues.length && !this.isValueExpression) {
return this.$locale.baseText('parameterInput.parameterHasIssues', interpolation);
return this.i18n.baseText('parameterInput.parameterHasIssues', interpolation);
} else if (!this.getIssues.length && this.isValueExpression) {
return this.$locale.baseText('parameterInput.parameterHasExpression', interpolation);
return this.i18n.baseText('parameterInput.parameterHasExpression', interpolation);
}
return this.$locale.baseText('parameterInput.parameter', interpolation);
return this.i18n.baseText('parameterInput.parameter', interpolation);
},
displayValue(): string | number | boolean | null {
if (this.remoteParameterOptionsLoading === true) {
@ -604,7 +609,7 @@ export default defineComponent({
// to user that the data is loading. If not it would
// display the user the key instead of the value it
// represents
return this.$locale.baseText('parameterInput.loadingOptions');
return this.i18n.baseText('parameterInput.loadingOptions');
}
// if the value is marked as empty return empty string, to prevent displaying the asterisks
@ -690,7 +695,7 @@ export default defineComponent({
if (this.parameter.type === 'credentialsSelect' && this.displayValue === '') {
issues.parameters = issues.parameters || {};
const issue = this.$locale.baseText('parameterInput.selectACredentialTypeFromTheDropdown');
const issue = this.i18n.baseText('parameterInput.selectACredentialTypeFromTheDropdown');
issues.parameters[this.parameter.name] = [issue];
} else if (
@ -723,7 +728,7 @@ export default defineComponent({
issues.parameters = {};
}
const issue = this.$locale.baseText('parameterInput.theValueIsNotSupported', {
const issue = this.i18n.baseText('parameterInput.theValueIsNotSupported', {
interpolate: { checkValue },
});
@ -856,18 +861,18 @@ export default defineComponent({
},
getPlaceholder(): string {
return this.isForCredential
? this.$locale.credText().placeholder(this.parameter)
: this.$locale.nodeText().placeholder(this.parameter, this.path);
? this.i18n.credText().placeholder(this.parameter)
: this.i18n.nodeText().placeholder(this.parameter, this.path);
},
getOptionsOptionDisplayName(option: INodePropertyOptions): string {
return this.isForCredential
? this.$locale.credText().optionsOptionDisplayName(this.parameter, option)
: this.$locale.nodeText().optionsOptionDisplayName(this.parameter, option, this.path);
? this.i18n.credText().optionsOptionDisplayName(this.parameter, option)
: this.i18n.nodeText().optionsOptionDisplayName(this.parameter, option, this.path);
},
getOptionsOptionDescription(option: INodePropertyOptions): string {
return this.isForCredential
? this.$locale.credText().optionsOptionDescription(this.parameter, option)
: this.$locale.nodeText().optionsOptionDescription(this.parameter, option, this.path);
? this.i18n.credText().optionsOptionDescription(this.parameter, option)
: this.i18n.nodeText().optionsOptionDescription(this.parameter, option, this.path);
},
async loadRemoteParameterOptions() {

View file

@ -1,7 +1,7 @@
<template>
<n8n-input-label
:label="hideLabel ? '' : $locale.nodeText().inputLabelDisplayName(parameter, path)"
:tooltipText="hideLabel ? '' : $locale.nodeText().inputLabelDescription(parameter, path)"
:label="hideLabel ? '' : i18n.nodeText().inputLabelDisplayName(parameter, path)"
:tooltipText="hideLabel ? '' : i18n.nodeText().inputLabelDescription(parameter, path)"
:showTooltip="focused"
:showOptions="menuExpanded || focused || forceShowExpression"
:bold="false"
@ -36,7 +36,7 @@
<template #content>
<span
v-html="
$locale.baseText(`dataMapping.${displayMode}Hint`, {
i18n.baseText(`dataMapping.${displayMode}Hint`, {
interpolate: { name: parameter.displayName },
})
"
@ -77,7 +77,7 @@ import type { IN8nButton, INodeUi, IRunDataDisplayMode, IUpdateInformation } fro
import ParameterOptions from '@/components/ParameterOptions.vue';
import DraggableTarget from '@/components/DraggableTarget.vue';
import { useToast } from '@/composables';
import { useI18n, useToast } from '@/composables';
import {
hasExpressionMapping,
isResourceLocatorValue,
@ -110,8 +110,10 @@ export default defineComponent({
},
setup() {
const eventBus = createEventBus();
const i18n = useI18n();
return {
i18n,
eventBus,
...useToast(),
};
@ -158,12 +160,12 @@ export default defineComponent({
}),
},
},
created() {
mounted() {
const mappingTooltipDismissHandler = this.onMappingTooltipDismissed.bind(this);
this.dataMappingTooltipButtons = [
{
attrs: {
label: this.$locale.baseText('_reusableBaseText.dismiss' as BaseTextKey),
label: this.i18n.baseText('_reusableBaseText.dismiss' as BaseTextKey),
'data-test-id': 'dismiss-mapping-tooltip',
},
listeners: {
@ -178,7 +180,7 @@ export default defineComponent({
return this.ndvStore.activeNode;
},
hint(): string | null {
return this.$locale.nodeText().hint(this.parameter, this.path);
return this.i18n.nodeText().hint(this.parameter, this.path);
},
isInputTypeString(): boolean {
return this.parameter.type === 'string';
@ -300,8 +302,8 @@ export default defineComponent({
if (!this.ndvStore.isMappingOnboarded) {
this.showMessage({
title: this.$locale.baseText('dataMapping.success.title'),
message: this.$locale.baseText('dataMapping.success.moreInfo'),
title: this.i18n.baseText('dataMapping.success.title'),
message: this.i18n.baseText('dataMapping.success.moreInfo'),
type: 'success',
dangerouslyUseHTMLString: true,
});

View file

@ -135,7 +135,7 @@ import { useUIStore } from '@/stores/ui.store';
import { useSettingsStore } from '@/stores/settings.store';
import { useRootStore } from '@/stores/n8nRoot.store';
import { useUsersStore } from '@/stores/users.store';
import { createEventBus } from 'n8n-design-system';
import { createEventBus } from 'n8n-design-system/utils';
export default defineComponent({
name: 'PersonalizationModal',

View file

@ -13,6 +13,7 @@ interface Props {
serviceName: string;
loading: boolean;
loadingError: boolean;
teleported?: boolean;
}
const props = defineProps<Props>();
@ -112,6 +113,7 @@ defineExpose({
<div class="mt-5xs">
<n8n-select
:modelValue="selected"
:teleported="teleported"
:size="props.inputSize"
@update:modelValue="onModeChanged"
>

View file

@ -16,6 +16,7 @@ interface Props {
inputSize: string;
loading: boolean;
serviceName: string;
teleported?: boolean;
}
const props = defineProps<Props>();
@ -134,6 +135,7 @@ defineExpose({
:modelValue="state.selected"
:size="props.inputSize"
:disabled="loading"
:teleported="teleported"
@update:modelValue="onSelectionChange"
>
<n8n-option v-for="field in availableMatchingFields" :key="field.id" :value="field.id">

View file

@ -26,6 +26,7 @@ type Props = {
inputSize: string;
labelSize: string;
dependentParametersValues?: string | null;
teleported: boolean;
};
const nodeTypesStore = useNodeTypesStore();
@ -445,6 +446,7 @@ defineExpose({
:loading="state.loading"
:loadingError="state.loadingError"
:fieldsToMap="state.paramValue.schema"
:teleported="teleported"
@modeChanged="onModeChanged"
@retryFetch="initFetching"
/>
@ -457,6 +459,7 @@ defineExpose({
:loading="state.loading"
:initialValue="matchingColumns"
:serviceName="nodeType?.displayName || locale.baseText('generic.service')"
:teleported="teleported"
@matchingColumnsChanged="onMatchingColumnsChanged"
/>
<n8n-text v-if="!showMappingModeSelect && state.loading" size="small">

View file

@ -69,7 +69,7 @@
</template>
<script lang="ts">
import { defineAsyncComponent, defineComponent } from 'vue';
import { defineAsyncComponent, defineComponent, ref } from 'vue';
import type { PropType } from 'vue';
import VueJsonPretty from 'vue-json-pretty';
import type { IDataObject, INodeExecutionData } from 'n8n-workflow';
@ -82,7 +82,7 @@ import { useNDVStore } from '@/stores/ndv.store';
import MappingPill from './MappingPill.vue';
import { getMappedExpression } from '@/utils/mappingUtils';
import { useWorkflowsStore } from '@/stores/workflows.store';
import { nonExistingJsonPath } from '@/components/RunDataJsonActions.vue';
import { nonExistingJsonPath } from '@/constants';
const RunDataJsonActions = defineAsyncComponent(
async () => import('@/components/RunDataJsonActions.vue'),
@ -126,11 +126,15 @@ export default defineComponent({
type: Number,
},
},
data() {
setup() {
const selectedJsonPath = ref(nonExistingJsonPath);
const draggingPath = ref<null | string>(null);
const displayMode = ref('json');
return {
selectedJsonPath: nonExistingJsonPath,
draggingPath: null as null | string,
displayMode: 'json',
selectedJsonPath,
draggingPath,
displayMode,
};
},
computed: {

View file

@ -2,7 +2,7 @@
<div :class="$style.actionsGroup">
<n8n-icon-button
v-if="noSelection"
:title="$locale.baseText('runData.copyToClipboard')"
:title="i18n.baseText('runData.copyToClipboard')"
icon="copy"
type="tertiary"
:circle="false"
@ -11,7 +11,7 @@
<el-dropdown v-else trigger="click" @command="handleCopyClick">
<span class="el-dropdown-link">
<n8n-icon-button
:title="$locale.baseText('runData.copyToClipboard')"
:title="i18n.baseText('runData.copyToClipboard')"
icon="copy"
type="tertiary"
:circle="false"
@ -20,13 +20,13 @@
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item :command="{ command: 'value' }">
{{ $locale.baseText('runData.copyValue') }}
{{ i18n.baseText('runData.copyValue') }}
</el-dropdown-item>
<el-dropdown-item :command="{ command: 'itemPath' }" divided>
{{ $locale.baseText('runData.copyItemPath') }}
{{ i18n.baseText('runData.copyItemPath') }}
</el-dropdown-item>
<el-dropdown-item :command="{ command: 'parameterPath' }">
{{ $locale.baseText('runData.copyParameterPath') }}
{{ i18n.baseText('runData.copyParameterPath') }}
</el-dropdown-item>
</el-dropdown-menu>
</template>
@ -48,16 +48,14 @@ import { genericHelpers } from '@/mixins/genericHelpers';
import { clearJsonKey, convertPath, executionDataToJson } from '@/utils';
import { useWorkflowsStore } from '@/stores/workflows.store';
import { useNDVStore } from '@/stores/ndv.store';
import { useToast } from '@/composables';
import { useI18n, useToast } from '@/composables';
import { nonExistingJsonPath } from '@/constants';
type JsonPathData = {
path: string;
startPath: string;
};
// A path that does not exist so that nothing is selected by default
export const nonExistingJsonPath = '_!^&*';
export default defineComponent({
name: 'run-data-json-actions',
mixins: [genericHelpers, nodeHelpers, pinData, copyPaste],
@ -94,7 +92,10 @@ export default defineComponent({
},
},
setup() {
const i18n = useI18n();
return {
i18n,
...useToast(),
};
},
@ -161,7 +162,7 @@ export default defineComponent({
value = this.getJsonValue();
this.showToast({
title: this.$locale.baseText('runData.copyValue.toast'),
title: this.i18n.baseText('runData.copyValue.toast'),
message: '',
type: 'success',
duration: 2000,
@ -175,7 +176,7 @@ export default defineComponent({
path = jsonItemPath.path;
this.showToast({
title: this.$locale.baseText('runData.copyItemPath.toast'),
title: this.i18n.baseText('runData.copyItemPath.toast'),
message: '',
type: 'success',
duration: 2000,
@ -186,7 +187,7 @@ export default defineComponent({
path = jsonParameterPath.path;
this.showToast({
title: this.$locale.baseText('runData.copyParameterPath.toast'),
title: this.i18n.baseText('runData.copyParameterPath.toast'),
message: '',
type: 'success',
duration: 2000,

View file

@ -1,7 +1,8 @@
<script lang="ts" setup>
import { useSSOStore } from '@/stores/sso.store';
import { useToast } from '@/composables';
import { useI18n, useToast } from '@/composables';
const i18n = useI18n();
const ssoStore = useSSOStore();
const toast = useToast();
@ -17,14 +18,14 @@ const onSSOLogin = async () => {
<template>
<div v-if="ssoStore.showSsoLoginButton" :class="$style.ssoLogin">
<div :class="$style.divider">
<span>{{ $locale.baseText('sso.login.divider') }}</span>
<span>{{ i18n.baseText('sso.login.divider') }}</span>
</div>
<n8n-button
@click="onSSOLogin"
size="large"
type="primary"
outline
:label="$locale.baseText('sso.login.button')"
:label="i18n.baseText('sso.login.button')"
/>
</div>
</template>

View file

@ -206,7 +206,7 @@ import InlineNameEdit from '@/components/InlineNameEdit.vue';
import SaveButton from '@/components/SaveButton.vue';
import EventSelection from '@/components/SettingsLogStreaming/EventSelection.ee.vue';
import type { EventBus } from 'n8n-design-system';
import { createEventBus } from 'n8n-design-system';
import { createEventBus } from 'n8n-design-system/utils';
export default defineComponent({
name: 'event-destination-settings-modal',

View file

@ -21,7 +21,7 @@ const defaultStagedFileTypes = ['tags', 'variables', 'credential'];
const loadingService = useLoadingService();
const uiStore = useUIStore();
const toast = useToast();
const { i18n } = useI18n();
const i18n = useI18n();
const sourceControlStore = useSourceControlStore();
const route = useRoute();

View file

@ -23,7 +23,7 @@ const defaultStagedFileTypes = ['tags', 'variables', 'credential'];
const loadingService = useLoadingService();
const uiStore = useUIStore();
const toast = useToast();
const { i18n } = useI18n();
const i18n = useI18n();
const sourceControlStore = useSourceControlStore();
const route = useRoute();

View file

@ -1,5 +1,5 @@
<template>
<div :class="$style.sqlEditor" v-click-outside="onBlur">
<div :class="$style.sqlEditor" v-on-click-outside="onBlur">
<div ref="sqlEditor" data-test-id="sql-editor-container" class="ph-no-capture"></div>
<InlineExpressionEditorOutput
:segments="segments"

View file

@ -39,7 +39,7 @@ import Modal from '@/components/Modal.vue';
import { TAGS_MANAGER_MODAL_KEY } from '@/constants';
import { mapStores } from 'pinia';
import { useTagsStore } from '@/stores/tags.store';
import { createEventBus } from 'n8n-design-system';
import { createEventBus } from 'n8n-design-system/utils';
export default defineComponent({
name: 'TagsManager',

View file

@ -119,7 +119,7 @@ import { useWorkflowsStore } from '@/stores/workflows.store';
import { useNDVStore } from '@/stores/ndv.store';
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
import type { N8nInfoAccordion } from 'n8n-design-system';
import { createEventBus } from 'n8n-design-system';
import { createEventBus } from 'n8n-design-system/utils';
type HelpRef = InstanceType<typeof N8nInfoAccordion>;

View file

@ -65,7 +65,7 @@ import ModalDrawer from '@/components/ModalDrawer.vue';
import { workflowHelpers } from '@/mixins/workflowHelpers';
import { useSettingsStore } from '@/stores/settings.store';
import { useRootStore } from '@/stores/n8nRoot.store';
import { createEventBus } from 'n8n-design-system';
import { createEventBus } from 'n8n-design-system/utils';
import { useToast } from '@/composables';
const DEFAULT_TITLE = 'How likely are you to recommend n8n to a friend or colleague?';

View file

@ -7,7 +7,7 @@ import { EnterpriseEditionFeature } from '@/constants';
import { useSettingsStore, useUsersStore } from '@/stores';
import { getVariablesPermissions } from '@/permissions';
const { i18n } = useI18n();
const i18n = useI18n();
const copyToClipboard = useCopyToClipboard();
const { showMessage } = useToast();
const settingsStore = useSettingsStore();

View file

@ -381,7 +381,7 @@ import {
useWorkflowsEEStore,
useUsersStore,
} from '@/stores';
import { createEventBus } from 'n8n-design-system';
import { createEventBus } from 'n8n-design-system/utils';
export default defineComponent({
name: 'WorkflowSettings',

View file

@ -124,7 +124,7 @@
<script lang="ts">
import { defineComponent } from 'vue';
import { mapStores } from 'pinia';
import { createEventBus } from 'n8n-design-system';
import { createEventBus } from 'n8n-design-system/utils';
import Modal from './Modal.vue';
import {

View file

@ -1,5 +1,4 @@
import { PiniaVuePlugin } from 'pinia';
import { render, within } from '@testing-library/vue';
import { within } from '@testing-library/vue';
import { merge } from 'lodash-es';
import userEvent from '@testing-library/user-event';
@ -10,53 +9,51 @@ import { createTestingPinia } from '@pinia/testing';
import BannerStack from '@/components/banners/BannerStack.vue';
import { useUIStore } from '@/stores/ui.store';
import { useUsersStore } from '@/stores/users.store';
import { createComponentRenderer, RenderOptions } from '@/__tests__/render';
let uiStore: ReturnType<typeof useUIStore>;
let usersStore: ReturnType<typeof useUsersStore>;
const DEFAULT_SETUP = {
pinia: createTestingPinia({
initialState: {
[STORES.SETTINGS]: {
settings: merge({}, SETTINGS_STORE_DEFAULT_STATE.settings),
},
[STORES.UI]: {
banners: {
V1: { dismissed: false },
TRIAL: { dismissed: false },
TRIAL_OVER: { dismissed: false },
const initialState = {
[STORES.SETTINGS]: {
settings: merge({}, SETTINGS_STORE_DEFAULT_STATE.settings),
},
[STORES.UI]: {
banners: {
V1: { dismissed: false },
TRIAL: { dismissed: false },
TRIAL_OVER: { dismissed: false },
},
},
[STORES.USERS]: {
currentUserId: 'aaa-bbb',
users: {
'aaa-bbb': {
id: 'aaa-bbb',
globalRole: {
id: '1',
name: 'owner',
scope: 'global',
},
},
[STORES.USERS]: {
currentUserId: 'aaa-bbb',
users: {
'aaa-bbb': {
id: 'aaa-bbb',
globalRole: {
id: '1',
name: 'owner',
scope: 'global',
},
},
'bbb-bbb': {
id: 'bbb-bbb',
globalRoleId: 2,
globalRole: {
id: '2',
name: 'member',
scope: 'global',
},
},
'bbb-bbb': {
id: 'bbb-bbb',
globalRoleId: 2,
globalRole: {
id: '2',
name: 'member',
scope: 'global',
},
},
},
}),
},
};
const renderComponent = (renderOptions: Parameters<typeof render>[1] = {}) =>
render(BannerStack, merge(DEFAULT_SETUP, renderOptions), (vue) => {
vue.use(PiniaVuePlugin);
});
const defaultRenderOptions: RenderOptions = {
pinia: createTestingPinia({ initialState }),
};
const renderComponent = createComponentRenderer(BannerStack, defaultRenderOptions);
describe('BannerStack', () => {
beforeEach(() => {
@ -82,19 +79,17 @@ describe('BannerStack', () => {
it('should not render dismissed banners', async () => {
const { getByTestId } = renderComponent({
pinia: createTestingPinia({
initialState: merge(
{
[STORES.UI]: {
banners: {
V1: { dismissed: true },
TRIAL: { dismissed: true },
},
initialState: merge(initialState, {
[STORES.UI]: {
banners: {
V1: { dismissed: true },
TRIAL: { dismissed: true },
},
},
DEFAULT_SETUP.pinia,
),
}),
}),
});
const bannerStack = getByTestId('banner-stack');
expect(bannerStack).toBeInTheDocument();
@ -117,7 +112,7 @@ describe('BannerStack', () => {
it('should permanently dismiss banner on click', async () => {
const { getByTestId } = renderComponent({
pinia: createTestingPinia({
initialState: merge(DEFAULT_SETUP.pinia, {
initialState: merge(initialState, {
[STORES.UI]: {
banners: {
V1: { dismissed: false },
@ -139,7 +134,7 @@ describe('BannerStack', () => {
it('should not render permanent dismiss link if user is not owner', async () => {
const { queryByTestId } = renderComponent({
pinia: createTestingPinia({
initialState: merge(DEFAULT_SETUP.pinia, {
initialState: merge(initialState, {
[STORES.USERS]: {
currentUserId: 'bbb-bbb',
},

View file

@ -1,29 +1,15 @@
import { describe, test, expect } from 'vitest';
import Vue from 'vue';
import { PiniaVuePlugin } from 'pinia';
import { createTestingPinia } from '@pinia/testing';
import type { RenderOptions } from '@testing-library/vue';
import { render } from '@testing-library/vue';
import userEvent from '@testing-library/user-event';
import { faker } from '@faker-js/faker';
import ExecutionFilter from '@/components/ExecutionFilter.vue';
import { STORES } from '@/constants';
import { i18nInstance } from '@/plugins/i18n';
import type { IWorkflowShortResponse, ExecutionFilterType } from '@/Interface';
import { useTelemetry } from '@/composables';
vi.mock('@/composables', () => {
const track = vi.fn();
return {
useTelemetry: () => ({
track,
}),
};
});
let telemetry: ReturnType<typeof useTelemetry>;
Vue.use(PiniaVuePlugin);
import { createComponentRenderer } from '@/__tests__/render';
import * as telemetryModule from '@/composables/useTelemetry';
import type { Telemetry } from '@/plugins/telemetry';
import { merge } from 'lodash-es';
import { nextTick } from 'vue';
const defaultFilterState: ExecutionFilterType = {
status: 'all',
@ -65,13 +51,36 @@ const initialState = {
},
};
const renderOptions: RenderOptions<ExecutionFilter> = {
i18n: i18nInstance,
};
const renderComponent = createComponentRenderer(ExecutionFilter, {
props: {
teleported: false,
},
});
describe('ExecutionFilter', () => {
beforeEach(() => {
telemetry = useTelemetry();
afterAll(() => {
vi.clearAllMocks();
});
test('telemetry sent only once after component is mounted', async () => {
const track = vi.fn();
const spy = vi.spyOn(telemetryModule, 'useTelemetry');
spy.mockImplementation(
() =>
({
track,
} as unknown as Telemetry),
);
const { getByTestId } = renderComponent({
pinia: createTestingPinia({ initialState }),
});
const customDataKeyInput = getByTestId('execution-filter-saved-data-key-input');
await userEvent.type(customDataKeyInput, 'test');
await userEvent.type(customDataKeyInput, 'key');
expect(track).toHaveBeenCalledTimes(1);
});
test.each([
@ -86,30 +95,49 @@ describe('ExecutionFilter', () => {
])(
'renders in %s environment on %s deployment with advancedExecutionFilters %s and workflows %s',
async (environment, deployment, advancedExecutionFilters, workflows) => {
initialState[STORES.SETTINGS].settings.license.environment = environment;
initialState[STORES.SETTINGS].settings.deployment.type = deployment;
initialState[STORES.SETTINGS].settings.enterprise.advancedExecutionFilters =
advancedExecutionFilters;
renderOptions.pinia = createTestingPinia({ initialState });
renderOptions.props = { workflows };
const { getByTestId, queryByTestId } = render(ExecutionFilter, renderOptions);
const { html, getByTestId, queryByTestId, queryAllByTestId } = renderComponent({
props: { workflows },
pinia: createTestingPinia({
initialState: merge(initialState, {
[STORES.SETTINGS]: {
settings: {
license: {
environment,
},
deployment: {
type: deployment,
},
enterprise: {
advancedExecutionFilters,
},
},
},
}),
}),
});
await userEvent.click(getByTestId('executions-filter-button'));
await userEvent.hover(getByTestId('execution-filter-saved-data-key-input'));
if (advancedExecutionFilters) {
expect(queryByTestId('executions-filter-view-plans-link')).not.toBeInTheDocument();
expect(queryByTestId('executions-filter-view-plans-link')).not.toBeVisible();
}
expect(queryByTestId('executions-filter-reset-button')).not.toBeInTheDocument();
expect(!!queryByTestId('executions-filter-workflows-select')).toBe(!!workflows?.length);
const select = queryByTestId('executions-filter-workflows-select');
if (workflows && workflows.length > 0) {
expect(select).toBeVisible();
} else {
expect(select).not.toBeInTheDocument();
}
},
);
test('state change', async () => {
const { getByTestId, queryByTestId, emitted } = render(ExecutionFilter, renderOptions);
const { html, getByTestId, queryByTestId, emitted } = renderComponent({
pinia: createTestingPinia({ initialState }),
});
const filterChangedEvent = emitted().filterChanged;
expect(filterChangedEvent).toHaveLength(1);
@ -123,6 +151,7 @@ describe('ExecutionFilter', () => {
expect(getByTestId('execution-filter-form')).toBeVisible();
await userEvent.click(getByTestId('executions-filter-status-select'));
await userEvent.click(getByTestId('executions-filter-status-select').querySelectorAll('li')[1]);
expect(emitted().filterChanged).toHaveLength(2);
@ -136,14 +165,4 @@ describe('ExecutionFilter', () => {
expect(queryByTestId('executions-filter-reset-button')).not.toBeInTheDocument();
expect(queryByTestId('execution-filter-badge')).not.toBeInTheDocument();
});
test('telemetry sent only once after component is mounted', async () => {
const { getByTestId } = render(ExecutionFilter, renderOptions);
const customDataKeyInput = getByTestId('execution-filter-saved-data-key-input');
await userEvent.type(customDataKeyInput, 'test');
await userEvent.type(customDataKeyInput, 'key');
expect(telemetry.track).toHaveBeenCalledTimes(1);
});
});

View file

@ -1,17 +1,15 @@
import { vi, describe, it, expect } from 'vitest';
import { merge } from 'lodash-es';
import { PiniaVuePlugin } from 'pinia';
import { createTestingPinia } from '@pinia/testing';
import { render } from '@testing-library/vue';
import userEvent from '@testing-library/user-event';
import { faker } from '@faker-js/faker';
import { STORES } from '@/constants';
import ExecutionsList from '@/components/ExecutionsList.vue';
import { i18nInstance } from '@/plugins/i18n';
import type { IWorkflowDb } from '@/Interface';
import type { IExecutionsSummary } from 'n8n-workflow';
import { retry, SETTINGS_STORE_DEFAULT_STATE, waitAllPromises } from '@/__tests__/utils';
import { useWorkflowsStore } from '@/stores';
import { RenderOptions, createComponentRenderer } from '@/__tests__/render';
let pinia: ReturnType<typeof createTestingPinia>;
@ -65,28 +63,19 @@ const generateExecutionsData = () =>
estimated: false,
}));
const renderComponent = async () => {
const renderResult = render(
ExecutionsList,
{
pinia,
propsData: {
autoRefreshEnabled: false,
},
i18n: i18nInstance,
const defaultRenderOptions: RenderOptions = {
props: {
autoRefreshEnabled: false,
},
global: {
stubs: {
stubs: ['font-awesome-icon'],
},
(vue) => {
vue.use(PiniaVuePlugin);
vue.prototype.$telemetry = {
track: () => {},
};
},
);
await waitAllPromises();
return renderResult;
},
};
const renderComponent = createComponentRenderer(ExecutionsList, defaultRenderOptions);
describe('ExecutionsList.vue', () => {
let workflowsStore: ReturnType<typeof useWorkflowsStore>;
let workflowsData: IWorkflowDb[];
@ -123,7 +112,13 @@ describe('ExecutionsList.vue', () => {
results: [],
estimated: false,
});
const { queryAllByTestId, queryByTestId, getByTestId } = await renderComponent();
const { queryAllByTestId, queryByTestId, getByTestId } = renderComponent({
global: {
plugins: [pinia],
},
});
await waitAllPromises();
await userEvent.click(getByTestId('execution-auto-refresh-checkbox'));
expect(queryAllByTestId('select-execution-checkbox').length).toBe(0);
@ -138,7 +133,12 @@ describe('ExecutionsList.vue', () => {
.mockResolvedValueOnce(executionsData[0])
.mockResolvedValueOnce(executionsData[1]);
const { getByTestId, getAllByTestId, queryByTestId } = await renderComponent();
const { getByTestId, getAllByTestId, queryByTestId } = renderComponent({
global: {
plugins: [pinia],
},
});
await waitAllPromises();
expect(storeSpy).toHaveBeenCalledTimes(1);
@ -192,7 +192,12 @@ describe('ExecutionsList.vue', () => {
(execution) => !execution.retryOf && execution.retrySuccessId,
);
const { queryAllByText } = await renderComponent();
const { queryAllByText } = renderComponent({
global: {
plugins: [pinia],
},
});
await waitAllPromises();
expect(queryAllByText(/Retry of/).length).toBe(retryOf.length);
expect(queryAllByText(/Success retry/).length).toBe(retrySuccessId.length);

View file

@ -1,32 +1,20 @@
import { describe, it, expect, vi } from 'vitest';
import { render, waitFor } from '@testing-library/vue';
import { waitFor } from '@testing-library/vue';
import userEvent from '@testing-library/user-event';
import { PiniaVuePlugin } from 'pinia';
import { createTestingPinia } from '@pinia/testing';
import { merge } from 'lodash-es';
import { SOURCE_CONTROL_PULL_MODAL_KEY, STORES } from '@/constants';
import { i18nInstance } from '@/plugins/i18n';
import { SETTINGS_STORE_DEFAULT_STATE } from '@/__tests__/utils';
import MainSidebarSourceControl from '@/components/MainSidebarSourceControl.vue';
import { useSourceControlStore, useUIStore } from '@/stores';
import { useSourceControlStore } from '@/stores/sourceControl.store';
import { useUIStore } from '@/stores/ui.store';
import { createComponentRenderer } from '@/__tests__/render';
let pinia: ReturnType<typeof createTestingPinia>;
let sourceControlStore: ReturnType<typeof useSourceControlStore>;
let uiStore: ReturnType<typeof useUIStore>;
const renderComponent = (renderOptions: Parameters<typeof render>[1] = {}) => {
return render(
MainSidebarSourceControl,
{
pinia,
i18n: i18nInstance,
...renderOptions,
},
(vue) => {
vue.use(PiniaVuePlugin);
},
);
};
const renderComponent = createComponentRenderer(MainSidebarSourceControl);
describe('MainSidebarSourceControl', () => {
const getItemSpy = vi.spyOn(Storage.prototype, 'getItem');
@ -47,12 +35,22 @@ describe('MainSidebarSourceControl', () => {
it('should render nothing', async () => {
getItemSpy.mockReturnValue(null);
const { container } = renderComponent({ props: { isCollapsed: false } });
const { container } = renderComponent({
props: { isCollapsed: false },
global: {
plugins: [pinia],
},
});
expect(container).toBeEmptyDOMElement();
});
it('should render empty content', async () => {
const { getByTestId } = renderComponent({ props: { isCollapsed: false } });
const { getByTestId } = renderComponent({
props: { isCollapsed: false },
global: {
plugins: [pinia],
},
});
expect(getByTestId('main-sidebar-source-control')).toBeInTheDocument();
expect(getByTestId('main-sidebar-source-control')).toBeEmptyDOMElement();
});
@ -73,7 +71,12 @@ describe('MainSidebarSourceControl', () => {
});
it('should render the appropriate content', async () => {
const { getByTestId, queryByTestId } = renderComponent({ props: { isCollapsed: false } });
const { getByTestId, queryByTestId } = renderComponent({
props: { isCollapsed: false },
global: {
plugins: [pinia],
},
});
expect(getByTestId('main-sidebar-source-control-connected')).toBeInTheDocument();
expect(queryByTestId('main-sidebar-source-control-setup')).not.toBeInTheDocument();
});
@ -82,7 +85,12 @@ describe('MainSidebarSourceControl', () => {
vi.spyOn(sourceControlStore, 'pullWorkfolder').mockRejectedValueOnce({
response: { status: 400 },
});
const { getAllByRole, getByRole } = renderComponent({ props: { isCollapsed: false } });
const { getAllByRole, getByRole } = renderComponent({
props: { isCollapsed: false },
global: {
plugins: [pinia],
},
});
await userEvent.click(getAllByRole('button')[0]);
await waitFor(() => expect(getByRole('alert')).toBeInTheDocument());
@ -95,7 +103,12 @@ describe('MainSidebarSourceControl', () => {
});
const openModalSpy = vi.spyOn(uiStore, 'openModalWithData');
const { getAllByRole, getByRole } = renderComponent({ props: { isCollapsed: false } });
const { getAllByRole, getByRole } = renderComponent({
props: { isCollapsed: false },
global: {
plugins: [pinia],
},
});
await userEvent.click(getAllByRole('button')[0]);
await waitFor(() =>

View file

@ -1,12 +1,12 @@
import { PiniaVuePlugin } from 'pinia';
import { createLocalVue, mount } from '@vue/test-utils';
import PersonalizationModal from '@/components/PersonalizationModal.vue';
import { createTestingPinia } from '@pinia/testing';
import { PERSONALIZATION_MODAL_KEY } from '@/constants';
import { retry } from '@/__tests__/utils';
import { createComponentRenderer } from '@/__tests__/render';
import { fireEvent } from '@testing-library/vue';
describe('PersonalizationModal.vue', () => {
const pinia = createTestingPinia({
const renderComponent = createComponentRenderer(PersonalizationModal, {
pinia: createTestingPinia({
initialState: {
ui: {
modals: {
@ -21,41 +21,42 @@ describe('PersonalizationModal.vue', () => {
},
},
},
});
const localVue = createLocalVue();
localVue.use(PiniaVuePlugin);
}),
});
describe('PersonalizationModal.vue', () => {
it('should render correctly', async () => {
const wrapper = mount(PersonalizationModal, {
localVue,
pinia,
});
const wrapper = renderComponent();
await retry(() => expect(wrapper.find('.modal-content').exists()).toBe(true));
await retry(() =>
expect(wrapper.container.querySelector('.modal-content')).toBeInTheDocument(),
);
expect(wrapper.findAll('.n8n-select').length).toEqual(5);
expect(wrapper.container.querySelectorAll('.n8n-select').length).toEqual(5);
expect(wrapper.html()).toMatchSnapshot();
});
it('should display new option when role is "Devops", "Engineering", "IT", or "Sales and marketing"', async () => {
const wrapper = mount(PersonalizationModal, {
localVue,
pinia,
});
const wrapper = renderComponent();
await retry(() => expect(wrapper.find('.modal-content').exists()).toBe(true));
await retry(() =>
expect(wrapper.container.querySelector('.modal-content')).toBeInTheDocument(),
);
for (const index of [3, 4, 5, 6]) {
await wrapper.find('.n8n-select[name="role"]').trigger('click');
await wrapper
.find('.n8n-select[name="role"]')
.findAll('.el-select-dropdown__item')
.at(index)
.trigger('click');
const select = wrapper.container.querySelector('.n8n-select[name="role"]')!;
await fireEvent.click(select);
const item = select.querySelectorAll('.el-select-dropdown__item')[index];
await fireEvent.click(item);
await retry(() => {
expect(wrapper.findAll('.n8n-select').length).toEqual(6);
expect(wrapper.find('.n8n-select[name^="automationGoal"]').exists()).toBe(true);
expect(wrapper.container.querySelectorAll('.n8n-select').length).toEqual(6);
expect(
wrapper.container.querySelector('.n8n-select[name^="automationGoal"]'),
).toBeInTheDocument();
});
}
});

View file

@ -1,6 +1,4 @@
import { PiniaVuePlugin } from 'pinia';
import { render, within } from '@testing-library/vue';
import { merge } from 'lodash-es';
import {
DEFAULT_SETUP,
MAPPING_COLUMNS_RESPONSE,
@ -12,13 +10,11 @@ import { waitAllPromises } from '@/__tests__/utils';
import * as workflowHelpers from '@/mixins/workflowHelpers';
import ResourceMapper from '@/components/ResourceMapper/ResourceMapper.vue';
import userEvent from '@testing-library/user-event';
import { createComponentRenderer } from '@/__tests__/render';
let nodeTypeStore: ReturnType<typeof useNodeTypesStore>;
const renderComponent = (renderOptions: Parameters<typeof render>[1] = {}) =>
render(ResourceMapper, merge(DEFAULT_SETUP, renderOptions), (vue) => {
vue.use(PiniaVuePlugin);
});
const renderComponent = createComponentRenderer(ResourceMapper, DEFAULT_SETUP);
describe('ResourceMapper.vue', () => {
beforeEach(() => {
@ -44,18 +40,21 @@ describe('ResourceMapper.vue', () => {
).toBe(MAPPING_COLUMNS_RESPONSE.fields.length);
});
it('renders add mode properly', async () => {
const { getByTestId, queryByTestId } = renderComponent({
props: {
parameter: {
typeOptions: {
resourceMapper: {
mode: 'add',
it('renders add mode properly', async () => {
const { getByTestId, queryByTestId } = renderComponent(
{
props: {
parameter: {
typeOptions: {
resourceMapper: {
mode: 'add',
},
},
},
},
},
});
{ merge: true },
);
await waitAllPromises();
expect(getByTestId('resource-mapper-container')).toBeInTheDocument();
// This mode doesn't render matching column selector
@ -63,54 +62,63 @@ describe('ResourceMapper.vue', () => {
});
it('renders multi-key match selector properly', async () => {
const { container, getByTestId } = renderComponent({
props: {
parameter: {
typeOptions: {
resourceMapper: {
mode: 'upsert',
multiKeyMatch: true,
const { container, getByTestId } = renderComponent(
{
props: {
parameter: {
typeOptions: {
resourceMapper: {
mode: 'upsert',
multiKeyMatch: true,
},
},
},
},
},
});
{ merge: true },
);
await waitAllPromises();
expect(getByTestId('resource-mapper-container')).toBeInTheDocument();
expect(container.querySelector('.el-select__tags')).toBeInTheDocument();
});
it('does not render mapping mode selector if it is disabled', async () => {
const { getByTestId, queryByTestId } = renderComponent({
props: {
parameter: {
typeOptions: {
resourceMapper: {
supportAutoMap: false,
const { getByTestId, queryByTestId } = renderComponent(
{
props: {
parameter: {
typeOptions: {
resourceMapper: {
supportAutoMap: false,
},
},
},
},
},
});
{ merge: true },
);
await waitAllPromises();
expect(getByTestId('resource-mapper-container')).toBeInTheDocument();
expect(queryByTestId('mapping-mode-select')).not.toBeInTheDocument();
});
it('renders field on top of the list when they are selected for matching', async () => {
const { container, getByTestId } = renderComponent({
props: {
parameter: {
typeOptions: {
resourceMapper: {
supportAutoMap: true,
mode: 'upsert',
multiKeyMatch: false,
const { container, getByTestId } = renderComponent(
{
props: {
parameter: {
typeOptions: {
resourceMapper: {
supportAutoMap: true,
mode: 'upsert',
multiKeyMatch: false,
},
},
},
},
},
});
{ merge: true },
);
await waitAllPromises();
expect(getByTestId('resource-mapper-container')).toBeInTheDocument();
// Id should be the first field in the list
@ -124,19 +132,22 @@ describe('ResourceMapper.vue', () => {
});
it('renders selected matching columns properly when multiple key matching is enabled', async () => {
const { getByTestId, getByText, queryByText } = renderComponent({
props: {
parameter: {
typeOptions: {
resourceMapper: {
supportAutoMap: true,
mode: 'upsert',
multiKeyMatch: true,
const { getByTestId, getByText, queryByText } = renderComponent(
{
props: {
parameter: {
typeOptions: {
resourceMapper: {
supportAutoMap: true,
mode: 'upsert',
multiKeyMatch: true,
},
},
},
},
},
});
{ merge: true },
);
await waitAllPromises();
expect(getByTestId('resource-mapper-container')).toBeInTheDocument();
const matchingColumnDropdown = getByTestId('matching-column-select');
@ -156,20 +167,23 @@ describe('ResourceMapper.vue', () => {
});
it('uses field words defined in node definition', async () => {
const { getByText } = renderComponent({
props: {
parameter: {
typeOptions: {
resourceMapper: {
fieldWords: {
singular: 'foo',
plural: 'foos',
const { getByText } = renderComponent(
{
props: {
parameter: {
typeOptions: {
resourceMapper: {
fieldWords: {
singular: 'foo',
plural: 'foos',
},
},
},
},
},
},
});
{ merge: true },
);
await waitAllPromises();
expect(getByText('Set the value for each foo')).toBeInTheDocument();
expect(
@ -180,24 +194,27 @@ describe('ResourceMapper.vue', () => {
});
it('should render correct fields based on saved schema', async () => {
const { getByTestId, queryAllByTestId } = renderComponent({
props: {
node: {
parameters: {
columns: {
schema: UPDATED_SCHEMA,
const { getByTestId, queryAllByTestId } = renderComponent(
{
props: {
node: {
parameters: {
columns: {
schema: UPDATED_SCHEMA,
},
},
},
},
parameter: {
typeOptions: {
resourceMapper: {
mode: 'add',
parameter: {
typeOptions: {
resourceMapper: {
mode: 'add',
},
},
},
},
},
});
{ merge: true },
);
await waitAllPromises();
// There should be 5 fields rendered and only 2 of them should have remove button
expect(
@ -207,24 +224,27 @@ describe('ResourceMapper.vue', () => {
});
it('should render correct options based on saved schema', async () => {
const { getByTestId } = renderComponent({
props: {
node: {
parameters: {
columns: {
schema: UPDATED_SCHEMA,
const { getByTestId } = renderComponent(
{
props: {
node: {
parameters: {
columns: {
schema: UPDATED_SCHEMA,
},
},
},
},
parameter: {
typeOptions: {
resourceMapper: {
mode: 'add',
parameter: {
typeOptions: {
resourceMapper: {
mode: 'add',
},
},
},
},
},
});
{ merge: true },
);
await waitAllPromises();
// Should have one option in the bottom dropdown for one removed field
expect(getByTestId('add-fields-select').querySelectorAll('li').length).toBe(1);

View file

@ -1,7 +1,5 @@
import type Vue from 'vue';
import { defineComponent } from 'vue';
import { PiniaVuePlugin } from 'pinia';
import { render, waitFor } from '@testing-library/vue';
import { waitFor } from '@testing-library/vue';
import userEvent from '@testing-library/user-event';
import { createTestingPinia } from '@pinia/testing';
import { merge } from 'lodash-es';
@ -9,142 +7,41 @@ import RunData from '@/components/RunData.vue';
import { STORES, VIEWS } from '@/constants';
import { useSSOStore } from '@/stores/sso.store';
import { SETTINGS_STORE_DEFAULT_STATE } from '@/__tests__/utils';
import { externalHooks } from '@/mixins/externalHooks';
import { genericHelpers } from '@/mixins/genericHelpers';
import { pinData } from '@/mixins/pinData';
import { useNDVStore, useWorkflowsStore } from '@/stores';
import { createComponentRenderer } from '@/__tests__/render';
let pinia: ReturnType<typeof createTestingPinia>;
let ssoStore: ReturnType<typeof useSSOStore>;
let workflowsStore: ReturnType<typeof useWorkflowsStore>;
let ndvStore: ReturnType<typeof useNDVStore>;
function TelemetryPlugin(vue: typeof Vue): void {
Object.defineProperty(vue, '$telemetry', {
get() {
return {
track: () => {},
};
},
});
Object.defineProperty(vue.prototype, '$telemetry', {
get() {
return {
track: () => {},
};
},
});
}
// const nodeHelpers = defineComponent({
// methods: {
// getNodeInputData: vi.fn().mockReturnValue(),
// },
// });
const nodeHelpers = defineComponent({
methods: {
getNodeInputData: vi.fn().mockReturnValue([
{
json: {
id: 1,
name: 'Test 1',
json: {
data: 'Json data 1',
},
},
const renderComponent = createComponentRenderer(RunData, {
props: {
nodeUi: {
name: 'Test Node',
},
},
global: {
mocks: {
$route: {
name: VIEWS.WORKFLOW,
},
{
json: {
id: 2,
name: 'Test 2',
json: {
data: 'Json data 2',
},
},
},
]),
},
},
});
const renderComponent = (renderOptions: Parameters<typeof render>[1] = {}) =>
render(
RunData,
merge(
{
pinia,
mocks: {
$route: {
name: VIEWS.WORKFLOW,
},
},
mixins: [externalHooks, genericHelpers, nodeHelpers, pinData],
},
renderOptions,
),
(vue) => {
vue.use(TelemetryPlugin);
vue.use(PiniaVuePlugin);
},
);
describe('RunData', () => {
beforeEach(() => {
pinia = createTestingPinia({
initialState: {
[STORES.SETTINGS]: {
settings: merge({}, SETTINGS_STORE_DEFAULT_STATE.settings),
},
},
});
ssoStore = useSSOStore();
workflowsStore = useWorkflowsStore();
ndvStore = useNDVStore();
vi.spyOn(workflowsStore, 'getWorkflowExecution', 'get').mockReturnValue({
id: '1',
finished: true,
mode: 'trigger',
startedAt: new Date(),
workflowData: {
id: '1',
name: 'Test Workflow',
versionId: '1',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
active: false,
nodes: [],
connections: {},
},
data: {
resultData: {
runData: {
'Test Node': [
{
startTime: new Date().getTime(),
executionTime: new Date().getTime(),
data: {},
source: [null],
},
],
},
},
},
});
});
afterEach(() => {
vi.clearAllMocks();
});
it('should render data correctly even when "item.json" has another "json" key', async () => {
vi.spyOn(ndvStore, 'getPanelDisplayMode').mockReturnValue('schema');
vi.spyOn(ndvStore, 'activeNode', 'get').mockReturnValue({
id: '1',
typeVersion: 1,
name: 'Test Node',
position: [0, 0],
type: 'test',
parameters: {},
});
const { getByText, getAllByTestId, getByTestId } = renderComponent({
const { html, getByText, getAllByTestId, getByTestId } = renderComponent({
props: {
nodeUi: {
id: '1',
name: 'Test Node',
position: [0, 0],
},
@ -154,10 +51,90 @@ describe('RunData', () => {
mappingEnabled: true,
distanceFromActive: 0,
},
pinia: createTestingPinia({
initialState: {
[STORES.SETTINGS]: {
settings: merge({}, SETTINGS_STORE_DEFAULT_STATE.settings),
},
[STORES.NDV]: {
output: {
displayMode: 'schema',
},
activeNodeName: 'Test Node',
},
[STORES.WORKFLOWS]: {
workflow: {
nodes: [
{
id: '1',
typeVersion: 1,
name: 'Test Node',
position: [0, 0],
type: 'test',
parameters: {},
},
],
},
workflowExecutionData: {
id: '1',
finished: true,
mode: 'trigger',
startedAt: new Date(),
workflowData: {
id: '1',
name: 'Test Workflow',
versionId: '1',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
active: false,
nodes: [],
connections: {},
},
data: {
resultData: {
runData: {
'Test Node': [
{
startTime: new Date().getTime(),
executionTime: new Date().getTime(),
data: {
main: [
[
{
json: {
id: 1,
name: 'Test 1',
json: {
data: 'Json data 1',
},
},
},
{
json: {
id: 2,
name: 'Test 2',
json: {
data: 'Json data 2',
},
},
},
],
],
},
source: [null],
},
],
},
},
},
},
},
},
}),
});
await userEvent.click(getByTestId('ndv-pin-data'));
await waitFor(() => getAllByTestId('run-data-schema-item'));
await waitFor(() => getAllByTestId('run-data-schema-item'), { timeout: 1000 });
expect(getByText('Test 1')).toBeInTheDocument();
expect(getByText('Json data 1')).toBeInTheDocument();
});

View file

@ -1,44 +1,40 @@
import Vue from 'vue';
import { PiniaVuePlugin } from 'pinia';
import { createTestingPinia } from '@pinia/testing';
import { render, screen, cleanup } from '@testing-library/vue';
import { screen, cleanup } from '@testing-library/vue';
import RunDataJson from '@/components/RunDataJson.vue';
import { createComponentRenderer } from '@/__tests__/render';
Vue.use(PiniaVuePlugin);
describe('RunDataJson.vue', () => {
const DEFAULT_SETUP = {
pinia: createTestingPinia(),
props: {
mappingEnabled: true,
editMode: { enabled: false },
inputData: [
{
json: {
list: [1, 2, 3],
record: { name: 'Joe' },
myNumber: 123,
myStringNumber: '456',
myStringText: 'abc',
nil: null,
d: undefined,
},
const renderComponent = createComponentRenderer(RunDataJson, {
props: {
mappingEnabled: true,
editMode: { enabled: false },
inputData: [
{
json: {
list: [1, 2, 3],
record: { name: 'Joe' },
myNumber: 123,
myStringNumber: '456',
myStringText: 'abc',
nil: null,
d: undefined,
},
],
node: {
parameters: {
keepOnlySet: false,
values: {},
options: {},
},
id: '820ea733-d8a6-4379-8e73-88a2347ea003',
name: 'Set',
type: 'n8n-nodes-base.set',
typeVersion: 1,
position: [380, 1060],
disabled: false,
},
],
node: {
parameters: {
keepOnlySet: false,
values: {},
options: {},
},
id: '820ea733-d8a6-4379-8e73-88a2347ea003',
name: 'Set',
type: 'n8n-nodes-base.set',
typeVersion: 1,
position: [380, 1060],
disabled: false,
},
},
global: {
mocks: {
$locale: {
baseText() {
@ -49,13 +45,17 @@ describe('RunDataJson.vue', () => {
getters: {},
},
},
};
},
});
describe('RunDataJson.vue', () => {
beforeEach(cleanup);
it('renders json values properly', () => {
const { container } = render(RunDataJson, DEFAULT_SETUP, (vue) => {
vue.use(PiniaVuePlugin);
const { container } = renderComponent({
global: {
plugins: [createTestingPinia()],
},
});
expect(container).toMatchSnapshot();
@ -67,7 +67,11 @@ describe('RunDataJson.vue', () => {
});
it('sets ph-no-capture class correctly', () => {
render(RunDataJson, DEFAULT_SETUP);
renderComponent({
global: {
plugins: [createTestingPinia()],
},
});
expect(screen.getByText('"list"')).not.toHaveClass('ph-no-capture');
expect(screen.getByText('"record"')).not.toHaveClass('ph-no-capture');

View file

@ -1,82 +1,86 @@
import { PiniaVuePlugin } from 'pinia';
import { createTestingPinia } from '@pinia/testing';
import { render, cleanup } from '@testing-library/vue';
import RunDataJsonSchema from '@/components/RunDataSchema.vue';
import { STORES } from '@/constants';
import { createComponentRenderer } from '@/__tests__/render';
describe('RunDataJsonSchema.vue', () => {
const renderOptions = {
pinia: createTestingPinia({
initialState: {
[STORES.SETTINGS]: {
settings: {
templates: {
enabled: true,
host: 'https://api.n8n.io/api/',
const renderComponent = createComponentRenderer(RunDataJsonSchema, {
global: {
stubs: ['font-awesome-icon'],
plugins: [
createTestingPinia({
initialState: {
[STORES.SETTINGS]: {
settings: {
templates: {
enabled: true,
host: 'https://api.n8n.io/api/',
},
},
},
},
}),
],
},
props: {
mappingEnabled: true,
distanceFromActive: 1,
runIndex: 1,
totalRuns: 2,
paneType: 'input',
node: {
parameters: {
keepOnlySet: false,
values: {},
options: {},
},
}),
stubs: ['font-awesome-icon'],
props: {
mappingEnabled: true,
distanceFromActive: 1,
runIndex: 1,
totalRuns: 2,
paneType: 'input',
node: {
parameters: {
keepOnlySet: false,
values: {},
options: {},
},
id: '820ea733-d8a6-4379-8e73-88a2347ea003',
name: 'Set',
type: 'n8n-nodes-base.set',
typeVersion: 1,
position: [380, 1060],
disabled: false,
},
data: [{}],
id: '820ea733-d8a6-4379-8e73-88a2347ea003',
name: 'Set',
type: 'n8n-nodes-base.set',
typeVersion: 1,
position: [380, 1060],
disabled: false,
},
};
data: [{}],
},
});
describe('RunDataJsonSchema.vue', () => {
beforeEach(cleanup);
it('renders schema for empty data', () => {
const { container } = render(RunDataJsonSchema, renderOptions, (vue) => {
vue.use(PiniaVuePlugin);
});
const { container } = renderComponent();
expect(container).toMatchSnapshot();
});
it('renders schema for data', () => {
renderOptions.props.data = [
{ name: 'John', age: 22, hobbies: ['surfing', 'traveling'] },
{ name: 'Joe', age: 33, hobbies: ['skateboarding', 'gaming'] },
];
const { container } = render(RunDataJsonSchema, renderOptions, (vue) => {
vue.use(PiniaVuePlugin);
const { container } = renderComponent({
props: {
data: [
{ name: 'John', age: 22, hobbies: ['surfing', 'traveling'] },
{ name: 'Joe', age: 33, hobbies: ['skateboarding', 'gaming'] },
],
},
});
expect(container).toMatchSnapshot();
});
it('renders schema with spaces and dots', () => {
renderOptions.props.data = [
{
'hello world': [
const { container } = renderComponent({
props: {
data: [
{
test: {
'more to think about': 1,
},
'test.how': 'ignore',
'hello world': [
{
test: {
'more to think about': 1,
},
'test.how': 'ignore',
},
],
},
],
},
];
const { container } = render(RunDataJsonSchema, renderOptions, (vue) => {
vue.use(PiniaVuePlugin);
});
expect(container).toMatchSnapshot();
});

View file

@ -1,5 +1,3 @@
import { render } from '@testing-library/vue';
import { PiniaVuePlugin } from 'pinia';
import { SETTINGS_STORE_DEFAULT_STATE, waitAllPromises } from '@/__tests__/utils';
import { STORES } from '@/constants';
import { createTestingPinia } from '@pinia/testing';
@ -7,6 +5,10 @@ import { createTestingPinia } from '@pinia/testing';
import SqlEditor from '@/components/SqlEditor/SqlEditor.vue';
import { expressionManager } from '@/mixins/expressionManager';
import type { TargetItem } from '@/Interface';
import { N8nPlugin } from 'n8n-design-system/plugin';
import { PiniaVuePlugin } from 'pinia';
import { GlobalDirectivesPlugin } from '@/plugins/directives';
import { renderComponent } from '@/__tests__/render';
const EXPRESSION_OUTPUT_TEST_ID = 'inline-expression-editor-output';
@ -19,24 +21,23 @@ const RESOLVABLES: { [key: string]: string | number | boolean } = {
};
const DEFAULT_SETUP = {
pinia: createTestingPinia({
initialState: {
[STORES.SETTINGS]: {
settings: SETTINGS_STORE_DEFAULT_STATE.settings,
},
},
}),
props: {
dialect: 'PostgreSQL',
isReadOnly: false,
},
global: {
plugins: [
createTestingPinia({
initialState: {
[STORES.SETTINGS]: {
settings: SETTINGS_STORE_DEFAULT_STATE.settings,
},
},
}),
],
},
};
const renderComponent = (renderOptions: Parameters<typeof render>[1] = {}) =>
render(SqlEditor, { ...DEFAULT_SETUP, ...renderOptions }, (vue) => {
vue.use(PiniaVuePlugin);
});
describe('SQL Editor Preview Tests', () => {
beforeEach(() => {
vi.spyOn(expressionManager.methods, 'resolve').mockImplementation(
@ -51,9 +52,11 @@ describe('SQL Editor Preview Tests', () => {
});
it('renders basic query', async () => {
const { getByTestId } = renderComponent({
const { getByTestId } = renderComponent(SqlEditor, {
...DEFAULT_SETUP,
props: {
query: 'SELECT * FROM users',
...DEFAULT_SETUP.props,
modelValue: 'SELECT * FROM users',
},
});
await waitAllPromises();
@ -61,9 +64,11 @@ describe('SQL Editor Preview Tests', () => {
});
it('renders basic query with expression', async () => {
const { getByTestId } = renderComponent({
const { getByTestId } = renderComponent(SqlEditor, {
...DEFAULT_SETUP,
props: {
query: 'SELECT * FROM {{ $json.table }}',
...DEFAULT_SETUP.props,
modelValue: 'SELECT * FROM {{ $json.table }}',
},
});
await waitAllPromises();
@ -71,9 +76,11 @@ describe('SQL Editor Preview Tests', () => {
});
it('renders resolved expressions with dot between resolvables', async () => {
const { getByTestId } = renderComponent({
const { getByTestId } = renderComponent(SqlEditor, {
...DEFAULT_SETUP,
props: {
query: 'SELECT * FROM {{ $json.schema }}.{{ $json.table }}',
...DEFAULT_SETUP.props,
modelValue: 'SELECT * FROM {{ $json.schema }}.{{ $json.table }}',
},
});
await waitAllPromises();
@ -81,9 +88,11 @@ describe('SQL Editor Preview Tests', () => {
});
it('renders resolved expressions which resolve to 0', async () => {
const { getByTestId } = renderComponent({
const { getByTestId } = renderComponent(SqlEditor, {
...DEFAULT_SETUP,
props: {
query:
...DEFAULT_SETUP.props,
modelValue:
'SELECT * FROM {{ $json.schema }}.{{ $json.table }} WHERE {{ $json.id }} > {{ $json.limit - 10 }}',
},
});
@ -94,9 +103,11 @@ describe('SQL Editor Preview Tests', () => {
});
it('keeps query formatting in rendered output', async () => {
const { getByTestId } = renderComponent({
const { getByTestId } = renderComponent(SqlEditor, {
...DEFAULT_SETUP,
props: {
query:
...DEFAULT_SETUP.props,
modelValue:
'SELECT * FROM {{ $json.schema }}.{{ $json.table }}\n WHERE id > {{ $json.limit - 10 }}\n AND active = {{ $json.active }};',
},
});

View file

@ -1,4 +1,3 @@
import { PiniaVuePlugin } from 'pinia';
import { render } from '@testing-library/vue';
import { createTestingPinia } from '@pinia/testing';
import { merge } from 'lodash-es';
@ -16,18 +15,17 @@ const renderComponent = (renderOptions: Parameters<typeof render>[1] = {}) =>
SSOLogin,
merge(
{
pinia,
stubs: {
'n8n-button': {
template: '<button data-test-id="sso-button"></button>',
store: pinia,
global: {
stubs: {
'n8n-button': {
template: '<button data-test-id="sso-button"></button>',
},
},
},
},
renderOptions,
),
(vue) => {
vue.use(PiniaVuePlugin);
},
);
describe('SSOLogin', () => {

View file

@ -1,24 +1,38 @@
import VariablesRow from '../VariablesRow.vue';
import type { EnvironmentVariable } from '@/Interface';
import { fireEvent } from '@testing-library/vue';
import { createPinia, setActivePinia } from 'pinia';
import { setupServer } from '@/__tests__/server';
import { afterAll, beforeAll } from 'vitest';
import { useSettingsStore, useUsersStore } from '@/stores';
import { renderComponent } from '@/__tests__/utils';
import { createComponentRenderer } from '@/__tests__/render';
import { createTestingPinia } from '@pinia/testing';
import { STORES } from '@/constants';
const renderComponent = createComponentRenderer(VariablesRow, {
pinia: createTestingPinia({
initialState: {
[STORES.SETTINGS]: {
settings: {
enterprise: {
variables: true,
},
},
},
},
}),
global: {
stubs: ['n8n-tooltip'],
},
});
describe('VariablesRow', () => {
let server: ReturnType<typeof setupServer>;
let pinia: ReturnType<typeof createPinia>;
beforeAll(() => {
server = setupServer();
});
beforeEach(async () => {
pinia = createPinia();
setActivePinia(pinia);
await useSettingsStore().getSettings();
await useUsersStore().loginWithCookie();
});
@ -27,8 +41,6 @@ describe('VariablesRow', () => {
server.shutdown();
});
const stubs = ['n8n-tooltip'];
const environmentVariable: EnvironmentVariable = {
id: 1,
key: 'key',
@ -36,12 +48,10 @@ describe('VariablesRow', () => {
};
it('should render correctly', () => {
const wrapper = renderComponent(VariablesRow, {
const wrapper = renderComponent({
props: {
data: environmentVariable,
},
stubs,
pinia,
});
expect(wrapper.html()).toMatchSnapshot();
@ -49,12 +59,10 @@ describe('VariablesRow', () => {
});
it('should show edit and delete buttons on hover', async () => {
const wrapper = renderComponent(VariablesRow, {
const wrapper = renderComponent({
props: {
data: environmentVariable,
},
stubs,
pinia,
});
await fireEvent.mouseEnter(wrapper.container);
@ -64,13 +72,11 @@ describe('VariablesRow', () => {
});
it('should show key and value inputs in edit mode', async () => {
const wrapper = renderComponent(VariablesRow, {
const wrapper = renderComponent({
props: {
data: environmentVariable,
editing: true,
},
stubs,
pinia,
});
await fireEvent.mouseEnter(wrapper.container);
@ -88,13 +94,11 @@ describe('VariablesRow', () => {
});
it('should show cancel and save buttons in edit mode', async () => {
const wrapper = renderComponent(VariablesRow, {
const wrapper = renderComponent({
props: {
data: environmentVariable,
editing: true,
},
stubs,
pinia,
});
await fireEvent.mouseEnter(wrapper.container);

View file

@ -1,284 +0,0 @@
// Vitest Snapshot v1
exports[`PersonalizationModal.vue > should render correctly 1`] = `
"<transition-stub name=\\"dialog-fade\\" data-test-id=\\"personalization-form\\" class=\\"dialog-wrapper\\" style=\\"z-index: 2001;\\">
<div class=\\"el-dialog__wrapper\\">
<div role=\\"dialog\\" aria-modal=\\"true\\" aria-label=\\"dialog\\" class=\\"el-dialog\\" style=\\"margin-top: 15vh; width: 460px;\\">
<div class=\\"el-dialog__header\\">
<div class=\\"centerTitle\\">
<div>
<h1 class=\\"n8n-heading size-xlarge regular\\">Customize n8n to you</h1>
</div>
<div class=\\"subtitle\\">
<h3 class=\\"n8n-heading text-light size-small regular\\">These questions help us tailor n8n to you</h3>
</div>
</div>
<!---->
</div>
<div class=\\"el-dialog__body\\">
<div class=\\"modal-content\\">
<div class=\\"container\\">
<div>
<div class=\\"grid\\">
<div class=\\"\\">
<div class=\\"container\\" data-test-id=\\"companyType\\"><label for=\\"companyType\\" class=\\"n8n-input-label inputLabel heading medium\\">
<div class=\\"title\\"><span class=\\"n8n-text size-medium bold\\"> What best describes your company? <!----></span></div>
<!---->
<!---->
<!---->
</label>
<div class=\\"\\">
<div class=\\"n8n-select container\\" name=\\"companyType\\">
<!---->
<div class=\\"el-select el-select--large\\">
<!---->
<div class=\\"el-input el-input--large el-input--suffix\\">
<!----><input type=\\"text\\" readonly=\\"readonly\\" autocomplete=\\"off\\" placeholder=\\"Select...\\" class=\\"el-input__inner\\">
<!----><span class=\\"el-input__suffix\\"><span class=\\"el-input__suffix-inner\\"><i class=\\"el-select__caret el-input__icon el-icon-arrow-up\\"></i><!----><!----><!----><!----><!----></span>
<!----></span>
<!---->
<!---->
</div>
<transition-stub name=\\"el-zoom-in-top\\">
<div class=\\"el-select-dropdown el-popper\\" style=\\"display: none;\\">
<div class=\\"el-scrollbar\\" style=\\"\\">
<div class=\\"el-select-dropdown__wrap el-scrollbar__wrap el-scrollbar__wrap--hidden-default\\">
<ul class=\\"el-scrollbar__view el-select-dropdown__list\\">
<!---->
<li class=\\"el-select-dropdown__item\\"><span>Software as a service</span></li>
<li class=\\"el-select-dropdown__item\\"><span>Ecommerce</span></li>
<li class=\\"el-select-dropdown__item\\"><span>Marketing agency / consultancy</span></li>
<li class=\\"el-select-dropdown__item\\"><span>Systems integrator / Automation agency</span></li>
<li class=\\"el-select-dropdown__item\\"><span>Education</span></li>
<li class=\\"el-select-dropdown__item\\"><span>Other</span></li>
<li class=\\"el-select-dropdown__item\\"><span>I'm not using n8n for work</span></li>
</ul>
</div>
<div class=\\"el-scrollbar__bar is-horizontal\\">
<div class=\\"el-scrollbar__thumb\\" style=\\"transform: translateX(0%); webkit-transform: translateX(0%);\\"></div>
</div>
<div class=\\"el-scrollbar__bar is-vertical\\">
<div class=\\"el-scrollbar__thumb\\" style=\\"transform: translateY(0%); webkit-transform: translateY(0%);\\"></div>
</div>
</div>
<!---->
</div>
</transition-stub>
</div>
</div>
</div>
<!---->
</div>
</div>
<div class=\\"\\">
<div class=\\"container\\" data-test-id=\\"role\\"><label for=\\"role\\" class=\\"n8n-input-label inputLabel heading medium\\">
<div class=\\"title\\"><span class=\\"n8n-text size-medium bold\\"> Which role best describes you? <!----></span></div>
<!---->
<!---->
<!---->
</label>
<div class=\\"\\">
<div class=\\"n8n-select container\\" name=\\"role\\">
<!---->
<div class=\\"el-select el-select--large\\">
<!---->
<div class=\\"el-input el-input--large el-input--suffix\\">
<!----><input type=\\"text\\" readonly=\\"readonly\\" autocomplete=\\"off\\" placeholder=\\"Select...\\" class=\\"el-input__inner\\">
<!----><span class=\\"el-input__suffix\\"><span class=\\"el-input__suffix-inner\\"><i class=\\"el-select__caret el-input__icon el-icon-arrow-up\\"></i><!----><!----><!----><!----><!----></span>
<!----></span>
<!---->
<!---->
</div>
<transition-stub name=\\"el-zoom-in-top\\">
<div class=\\"el-select-dropdown el-popper\\" style=\\"display: none;\\">
<div class=\\"el-scrollbar\\" style=\\"\\">
<div class=\\"el-select-dropdown__wrap el-scrollbar__wrap el-scrollbar__wrap--hidden-default\\">
<ul class=\\"el-scrollbar__view el-select-dropdown__list\\">
<!---->
<li class=\\"el-select-dropdown__item\\"><span>Business Owner</span></li>
<li class=\\"el-select-dropdown__item\\"><span>Customer support</span></li>
<li class=\\"el-select-dropdown__item\\"><span>Data Science</span></li>
<li class=\\"el-select-dropdown__item\\"><span>Devops</span></li>
<li class=\\"el-select-dropdown__item\\"><span>IT</span></li>
<li class=\\"el-select-dropdown__item\\"><span>Engineering</span></li>
<li class=\\"el-select-dropdown__item\\"><span>Sales and Marketing</span></li>
<li class=\\"el-select-dropdown__item\\"><span>Security</span></li>
<li class=\\"el-select-dropdown__item\\"><span>Other (please specify)</span></li>
</ul>
</div>
<div class=\\"el-scrollbar__bar is-horizontal\\">
<div class=\\"el-scrollbar__thumb\\" style=\\"transform: translateX(0%); webkit-transform: translateX(0%);\\"></div>
</div>
<div class=\\"el-scrollbar__bar is-vertical\\">
<div class=\\"el-scrollbar__thumb\\" style=\\"transform: translateY(0%); webkit-transform: translateY(0%);\\"></div>
</div>
</div>
<!---->
</div>
</transition-stub>
</div>
</div>
</div>
<!---->
</div>
</div>
<div class=\\"\\">
<div class=\\"container\\" data-test-id=\\"automationBeneficiary\\"><label for=\\"automationBeneficiary\\" class=\\"n8n-input-label inputLabel heading medium\\">
<div class=\\"title\\"><span class=\\"n8n-text size-medium bold\\"> Who will your automations mainly be for? <!----></span></div>
<!---->
<!---->
<!---->
</label>
<div class=\\"\\">
<div class=\\"n8n-select container\\" name=\\"automationBeneficiary\\">
<!---->
<div class=\\"el-select el-select--large\\">
<!---->
<div class=\\"el-input el-input--large el-input--suffix\\">
<!----><input type=\\"text\\" readonly=\\"readonly\\" autocomplete=\\"off\\" placeholder=\\"Select...\\" class=\\"el-input__inner\\">
<!----><span class=\\"el-input__suffix\\"><span class=\\"el-input__suffix-inner\\"><i class=\\"el-select__caret el-input__icon el-icon-arrow-up\\"></i><!----><!----><!----><!----><!----></span>
<!----></span>
<!---->
<!---->
</div>
<transition-stub name=\\"el-zoom-in-top\\">
<div class=\\"el-select-dropdown el-popper\\" style=\\"display: none;\\">
<div class=\\"el-scrollbar\\" style=\\"\\">
<div class=\\"el-select-dropdown__wrap el-scrollbar__wrap el-scrollbar__wrap--hidden-default\\">
<ul class=\\"el-scrollbar__view el-select-dropdown__list\\">
<!---->
<li class=\\"el-select-dropdown__item\\"><span>Myself</span></li>
<li class=\\"el-select-dropdown__item\\"><span>My team</span></li>
<li class=\\"el-select-dropdown__item\\"><span>Other teams</span></li>
</ul>
</div>
<div class=\\"el-scrollbar__bar is-horizontal\\">
<div class=\\"el-scrollbar__thumb\\" style=\\"transform: translateX(0%); webkit-transform: translateX(0%);\\"></div>
</div>
<div class=\\"el-scrollbar__bar is-vertical\\">
<div class=\\"el-scrollbar__thumb\\" style=\\"transform: translateY(0%); webkit-transform: translateY(0%);\\"></div>
</div>
</div>
<!---->
</div>
</transition-stub>
</div>
</div>
</div>
<!---->
</div>
</div>
<div class=\\"\\">
<div class=\\"container\\" data-test-id=\\"companySize\\"><label for=\\"companySize\\" class=\\"n8n-input-label inputLabel heading medium\\">
<div class=\\"title\\"><span class=\\"n8n-text size-medium bold\\"> How big is your company? <!----></span></div>
<!---->
<!---->
<!---->
</label>
<div class=\\"\\">
<div class=\\"n8n-select container\\" name=\\"companySize\\">
<!---->
<div class=\\"el-select el-select--large\\">
<!---->
<div class=\\"el-input el-input--large el-input--suffix\\">
<!----><input type=\\"text\\" readonly=\\"readonly\\" autocomplete=\\"off\\" placeholder=\\"Select...\\" class=\\"el-input__inner\\">
<!----><span class=\\"el-input__suffix\\"><span class=\\"el-input__suffix-inner\\"><i class=\\"el-select__caret el-input__icon el-icon-arrow-up\\"></i><!----><!----><!----><!----><!----></span>
<!----></span>
<!---->
<!---->
</div>
<transition-stub name=\\"el-zoom-in-top\\">
<div class=\\"el-select-dropdown el-popper\\" style=\\"display: none;\\">
<div class=\\"el-scrollbar\\" style=\\"\\">
<div class=\\"el-select-dropdown__wrap el-scrollbar__wrap el-scrollbar__wrap--hidden-default\\">
<ul class=\\"el-scrollbar__view el-select-dropdown__list\\">
<!---->
<li class=\\"el-select-dropdown__item\\"><span>Less than 20 people</span></li>
<li class=\\"el-select-dropdown__item\\"><span>20-99 people</span></li>
<li class=\\"el-select-dropdown__item\\"><span>100-499 people</span></li>
<li class=\\"el-select-dropdown__item\\"><span>500-999 people</span></li>
<li class=\\"el-select-dropdown__item\\"><span>1000+ people</span></li>
<li class=\\"el-select-dropdown__item\\"><span>I'm not using n8n for work</span></li>
</ul>
</div>
<div class=\\"el-scrollbar__bar is-horizontal\\">
<div class=\\"el-scrollbar__thumb\\" style=\\"transform: translateX(0%); webkit-transform: translateX(0%);\\"></div>
</div>
<div class=\\"el-scrollbar__bar is-vertical\\">
<div class=\\"el-scrollbar__thumb\\" style=\\"transform: translateY(0%); webkit-transform: translateY(0%);\\"></div>
</div>
</div>
<!---->
</div>
</transition-stub>
</div>
</div>
</div>
<!---->
</div>
</div>
<div class=\\"\\">
<div class=\\"container\\" data-test-id=\\"reportedSource\\"><label for=\\"reportedSource\\" class=\\"n8n-input-label inputLabel heading medium\\">
<div class=\\"title\\"><span class=\\"n8n-text size-medium bold\\"> How did you hear about n8n? <!----></span></div>
<!---->
<!---->
<!---->
</label>
<div class=\\"\\">
<div class=\\"n8n-select container\\" name=\\"reportedSource\\">
<!---->
<div class=\\"el-select el-select--large\\">
<!---->
<div class=\\"el-input el-input--large el-input--suffix\\">
<!----><input type=\\"text\\" readonly=\\"readonly\\" autocomplete=\\"off\\" placeholder=\\"Select...\\" class=\\"el-input__inner\\">
<!----><span class=\\"el-input__suffix\\"><span class=\\"el-input__suffix-inner\\"><i class=\\"el-select__caret el-input__icon el-icon-arrow-up\\"></i><!----><!----><!----><!----><!----></span>
<!----></span>
<!---->
<!---->
</div>
<transition-stub name=\\"el-zoom-in-top\\">
<div class=\\"el-select-dropdown el-popper\\" style=\\"display: none;\\">
<div class=\\"el-scrollbar\\" style=\\"\\">
<div class=\\"el-select-dropdown__wrap el-scrollbar__wrap el-scrollbar__wrap--hidden-default\\">
<ul class=\\"el-scrollbar__view el-select-dropdown__list\\">
<!---->
<li class=\\"el-select-dropdown__item\\"><span>Google</span></li>
<li class=\\"el-select-dropdown__item\\"><span>Twitter</span></li>
<li class=\\"el-select-dropdown__item\\"><span>LinkedIn</span></li>
<li class=\\"el-select-dropdown__item\\"><span>YouTube</span></li>
<li class=\\"el-select-dropdown__item\\"><span>Friend / Word of mouth</span></li>
<li class=\\"el-select-dropdown__item\\"><span>Podcast</span></li>
<li class=\\"el-select-dropdown__item\\"><span>Event</span></li>
<li class=\\"el-select-dropdown__item\\"><span>Other (please specify)</span></li>
</ul>
</div>
<div class=\\"el-scrollbar__bar is-horizontal\\">
<div class=\\"el-scrollbar__thumb\\" style=\\"transform: translateX(0%); webkit-transform: translateX(0%);\\"></div>
</div>
<div class=\\"el-scrollbar__bar is-vertical\\">
<div class=\\"el-scrollbar__thumb\\" style=\\"transform: translateY(0%); webkit-transform: translateY(0%);\\"></div>
</div>
</div>
<!---->
</div>
</transition-stub>
</div>
</div>
</div>
<!---->
</div>
</div>
</div>
</div>
</div>
</div>
<div class=\\"footer\\">
<div><button aria-live=\\"polite\\" class=\\"button button primary medium float-right\\">
<!----><span>Get started</span></button></div>
</div>
</div>
<!---->
</div>
</div>
</transition-stub>"
`;

View file

@ -8,6 +8,7 @@ exports[`RunDataJsonSchema.vue > renders schema for data 1`] = `
<div
class=""
>
<div
class="schema"
>
@ -15,13 +16,14 @@ exports[`RunDataJsonSchema.vue > renders schema for data 1`] = `
class="item"
data-test-id="run-data-schema-item"
>
<!---->
<!---->
<!---->
<!---->
<!--v-if-->
<!--v-if-->
<!--v-if-->
<!--v-if-->
<div
class="sub"
>
<div
class="item"
data-test-id="run-data-schema-item"
@ -39,11 +41,23 @@ exports[`RunDataJsonSchema.vue > renders schema for data 1`] = `
data-target="mappable"
data-value="{{ $json.name }}"
>
<font-awesome-icon-stub
icon="font"
size="sm"
/>
<!---->
<svg
aria-hidden="true"
class="svg-inline--fa fa-font fa-w-14 fa-sm"
data-icon="font"
data-prefix="fas"
focusable="false"
role="img"
viewBox="0 0 448 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
class=""
d="M432 416h-23.41L277.88 53.69A32 32 0 0 0 247.58 32h-47.16a32 32 0 0 0-30.3 21.69L39.41 416H16a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h128a16 16 0 0 0 16-16v-32a16 16 0 0 0-16-16h-19.58l23.3-64h152.56l23.3 64H304a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h128a16 16 0 0 0 16-16v-32a16 16 0 0 0-16-16zM176.85 272L224 142.51 271.15 272z"
fill="currentColor"
/>
</svg>
<!--v-if-->
<span
class=""
>
@ -52,13 +66,13 @@ exports[`RunDataJsonSchema.vue > renders schema for data 1`] = `
</span>
</div>
<span
class="ph-no-capture text"
class="text ph-no-capture"
>
John
</span>
<!---->
<!---->
<!---->
<!--v-if-->
<!--v-if-->
<!--v-if-->
</div>
<div
class="item"
@ -77,11 +91,23 @@ exports[`RunDataJsonSchema.vue > renders schema for data 1`] = `
data-target="mappable"
data-value="{{ $json.age }}"
>
<font-awesome-icon-stub
icon="hashtag"
size="sm"
/>
<!---->
<svg
aria-hidden="true"
class="svg-inline--fa fa-hashtag fa-w-14 fa-sm"
data-icon="hashtag"
data-prefix="fas"
focusable="false"
role="img"
viewBox="0 0 448 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
class=""
d="M440.667 182.109l7.143-40c1.313-7.355-4.342-14.109-11.813-14.109h-74.81l14.623-81.891C377.123 38.754 371.468 32 363.997 32h-40.632a12 12 0 0 0-11.813 9.891L296.175 128H197.54l14.623-81.891C213.477 38.754 207.822 32 200.35 32h-40.632a12 12 0 0 0-11.813 9.891L132.528 128H53.432a12 12 0 0 0-11.813 9.891l-7.143 40C33.163 185.246 38.818 192 46.289 192h74.81L98.242 320H19.146a12 12 0 0 0-11.813 9.891l-7.143 40C-1.123 377.246 4.532 384 12.003 384h74.81L72.19 465.891C70.877 473.246 76.532 480 84.003 480h40.632a12 12 0 0 0 11.813-9.891L151.826 384h98.634l-14.623 81.891C234.523 473.246 240.178 480 247.65 480h40.632a12 12 0 0 0 11.813-9.891L315.472 384h79.096a12 12 0 0 0 11.813-9.891l7.143-40c1.313-7.355-4.342-14.109-11.813-14.109h-74.81l22.857-128h79.096a12 12 0 0 0 11.813-9.891zM261.889 320h-98.634l22.857-128h98.634l-22.857 128z"
fill="currentColor"
/>
</svg>
<!--v-if-->
<span
class=""
>
@ -90,13 +116,13 @@ exports[`RunDataJsonSchema.vue > renders schema for data 1`] = `
</span>
</div>
<span
class="ph-no-capture text"
class="text ph-no-capture"
>
22
</span>
<!---->
<!---->
<!---->
<!--v-if-->
<!--v-if-->
<!--v-if-->
</div>
<div
class="item"
@ -115,11 +141,23 @@ exports[`RunDataJsonSchema.vue > renders schema for data 1`] = `
data-target="mappable"
data-value="{{ $json.hobbies }}"
>
<font-awesome-icon-stub
icon="list"
size="sm"
/>
<!---->
<svg
aria-hidden="true"
class="svg-inline--fa fa-list fa-w-16 fa-sm"
data-icon="list"
data-prefix="fas"
focusable="false"
role="img"
viewBox="0 0 512 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
class=""
d="M80 368H16a16 16 0 0 0-16 16v64a16 16 0 0 0 16 16h64a16 16 0 0 0 16-16v-64a16 16 0 0 0-16-16zm0-320H16A16 16 0 0 0 0 64v64a16 16 0 0 0 16 16h64a16 16 0 0 0 16-16V64a16 16 0 0 0-16-16zm0 160H16a16 16 0 0 0-16 16v64a16 16 0 0 0 16 16h64a16 16 0 0 0 16-16v-64a16 16 0 0 0-16-16zm416 176H176a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h320a16 16 0 0 0 16-16v-32a16 16 0 0 0-16-16zm0-320H176a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h320a16 16 0 0 0 16-16V80a16 16 0 0 0-16-16zm0 160H176a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h320a16 16 0 0 0 16-16v-32a16 16 0 0 0-16-16z"
fill="currentColor"
/>
</svg>
<!--v-if-->
<span
class=""
>
@ -127,9 +165,8 @@ exports[`RunDataJsonSchema.vue > renders schema for data 1`] = `
</span>
</span>
</div>
<!---->
<!--v-if-->
<input
checked="checked"
id="input_array-0-2"
type="checkbox"
/>
@ -137,13 +174,27 @@ exports[`RunDataJsonSchema.vue > renders schema for data 1`] = `
class="toggle"
for="input_array-0-2"
>
<font-awesome-icon-stub
icon="angle-up"
/>
<svg
aria-hidden="true"
class="svg-inline--fa fa-angle-up fa-w-10"
data-icon="angle-up"
data-prefix="fas"
focusable="false"
role="img"
viewBox="0 0 320 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
class=""
d="M177 159.7l136 136c9.4 9.4 9.4 24.6 0 33.9l-22.6 22.6c-9.4 9.4-24.6 9.4-33.9 0L160 255.9l-96.4 96.4c-9.4 9.4-24.6 9.4-33.9 0L7 329.7c-9.4-9.4-9.4-24.6 0-33.9l136-136c9.4-9.5 24.6-9.5 34-.1z"
fill="currentColor"
/>
</svg>
</label>
<div
class="sub"
>
<div
class="item"
data-test-id="run-data-schema-item"
@ -161,10 +212,22 @@ exports[`RunDataJsonSchema.vue > renders schema for data 1`] = `
data-target="mappable"
data-value="{{ $json.hobbies[0] }}"
>
<font-awesome-icon-stub
icon="font"
size="sm"
/>
<svg
aria-hidden="true"
class="svg-inline--fa fa-font fa-w-14 fa-sm"
data-icon="font"
data-prefix="fas"
focusable="false"
role="img"
viewBox="0 0 448 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
class=""
d="M432 416h-23.41L277.88 53.69A32 32 0 0 0 247.58 32h-47.16a32 32 0 0 0-30.3 21.69L39.41 416H16a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h128a16 16 0 0 0 16-16v-32a16 16 0 0 0-16-16h-19.58l23.3-64h152.56l23.3 64H304a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h128a16 16 0 0 0 16-16v-32a16 16 0 0 0-16-16zM176.85 272L224 142.51 271.15 272z"
fill="currentColor"
/>
</svg>
<span>
hobbies
</span>
@ -176,13 +239,13 @@ exports[`RunDataJsonSchema.vue > renders schema for data 1`] = `
</span>
</div>
<span
class="ph-no-capture text"
class="text ph-no-capture"
>
surfing
</span>
<!---->
<!---->
<!---->
<!--v-if-->
<!--v-if-->
<!--v-if-->
</div>
<div
class="item"
@ -201,10 +264,22 @@ exports[`RunDataJsonSchema.vue > renders schema for data 1`] = `
data-target="mappable"
data-value="{{ $json.hobbies[1] }}"
>
<font-awesome-icon-stub
icon="font"
size="sm"
/>
<svg
aria-hidden="true"
class="svg-inline--fa fa-font fa-w-14 fa-sm"
data-icon="font"
data-prefix="fas"
focusable="false"
role="img"
viewBox="0 0 448 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
class=""
d="M432 416h-23.41L277.88 53.69A32 32 0 0 0 247.58 32h-47.16a32 32 0 0 0-30.3 21.69L39.41 416H16a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h128a16 16 0 0 0 16-16v-32a16 16 0 0 0-16-16h-19.58l23.3-64h152.56l23.3 64H304a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h128a16 16 0 0 0 16-16v-32a16 16 0 0 0-16-16zM176.85 272L224 142.51 271.15 272z"
fill="currentColor"
/>
</svg>
<span>
hobbies
</span>
@ -216,23 +291,24 @@ exports[`RunDataJsonSchema.vue > renders schema for data 1`] = `
</span>
</div>
<span
class="ph-no-capture text"
class="text ph-no-capture"
>
traveling
</span>
<!---->
<!---->
<!---->
<!--v-if-->
<!--v-if-->
<!--v-if-->
</div>
</div>
</div>
</div>
</div>
</div>
<div
class="teleporter hidden"
data-v-d4e6e290=""
/>
<!--teleport start-->
<!--teleport end-->
</div>
</div>
</div>
@ -250,15 +326,31 @@ exports[`RunDataJsonSchema.vue > renders schema for empty data 1`] = `
class="iconText"
>
<span
class="n8n-icon n8n-text compact size-medium regular"
class="n8n-text compact size-medium regular n8n-icon n8n-icon"
>
<font-awesome-icon-stub
class="medium"
icon="info-circle"
/>
<svg
aria-hidden="true"
class="svg-inline--fa fa-info-circle fa-w-16 medium"
data-icon="info-circle"
data-prefix="fas"
focusable="false"
role="img"
viewBox="0 0 512 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
class=""
d="M256 8C119.043 8 8 119.083 8 256c0 136.997 111.043 248 248 248s248-111.003 248-248C504 119.083 392.957 8 256 8zm0 110c23.196 0 42 18.804 42 42s-18.804 42-42 42-42-18.804-42-42 18.804-42 42-42zm56 254c0 6.627-5.373 12-12 12h-88c-6.627 0-12-5.373-12-12v-24c0-6.627 5.373-12 12-12h12v-64h-12c-6.627 0-12-5.373-12-12v-24c0-6.627 5.373-12 12-12h64c6.627 0 12 5.373 12 12v100h12c6.627 0 12 5.373 12 12v24z"
fill="currentColor"
/>
</svg>
</span>
<span>
No data to show - item(s) exist, but theyre empty
</span>
</span>
</div>
@ -274,6 +366,7 @@ exports[`RunDataJsonSchema.vue > renders schema with spaces and dots 1`] = `
<div
class=""
>
<div
class="schema"
>
@ -281,13 +374,14 @@ exports[`RunDataJsonSchema.vue > renders schema with spaces and dots 1`] = `
class="item"
data-test-id="run-data-schema-item"
>
<!---->
<!---->
<!---->
<!---->
<!--v-if-->
<!--v-if-->
<!--v-if-->
<!--v-if-->
<div
class="sub"
>
<div
class="item"
data-test-id="run-data-schema-item"
@ -305,11 +399,23 @@ exports[`RunDataJsonSchema.vue > renders schema with spaces and dots 1`] = `
data-target="mappable"
data-value="{{ $json['hello world'] }}"
>
<font-awesome-icon-stub
icon="list"
size="sm"
/>
<!---->
<svg
aria-hidden="true"
class="svg-inline--fa fa-list fa-w-16 fa-sm"
data-icon="list"
data-prefix="fas"
focusable="false"
role="img"
viewBox="0 0 512 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
class=""
d="M80 368H16a16 16 0 0 0-16 16v64a16 16 0 0 0 16 16h64a16 16 0 0 0 16-16v-64a16 16 0 0 0-16-16zm0-320H16A16 16 0 0 0 0 64v64a16 16 0 0 0 16 16h64a16 16 0 0 0 16-16V64a16 16 0 0 0-16-16zm0 160H16a16 16 0 0 0-16 16v64a16 16 0 0 0 16 16h64a16 16 0 0 0 16-16v-64a16 16 0 0 0-16-16zm416 176H176a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h320a16 16 0 0 0 16-16v-32a16 16 0 0 0-16-16zm0-320H176a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h320a16 16 0 0 0 16-16V80a16 16 0 0 0-16-16zm0 160H176a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h320a16 16 0 0 0 16-16v-32a16 16 0 0 0-16-16z"
fill="currentColor"
/>
</svg>
<!--v-if-->
<span
class=""
>
@ -317,9 +423,8 @@ exports[`RunDataJsonSchema.vue > renders schema with spaces and dots 1`] = `
</span>
</span>
</div>
<!---->
<!--v-if-->
<input
checked="checked"
id="input_array-0-0"
type="checkbox"
/>
@ -327,13 +432,27 @@ exports[`RunDataJsonSchema.vue > renders schema with spaces and dots 1`] = `
class="toggle"
for="input_array-0-0"
>
<font-awesome-icon-stub
icon="angle-up"
/>
<svg
aria-hidden="true"
class="svg-inline--fa fa-angle-up fa-w-10"
data-icon="angle-up"
data-prefix="fas"
focusable="false"
role="img"
viewBox="0 0 320 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
class=""
d="M177 159.7l136 136c9.4 9.4 9.4 24.6 0 33.9l-22.6 22.6c-9.4 9.4-24.6 9.4-33.9 0L160 255.9l-96.4 96.4c-9.4 9.4-24.6 9.4-33.9 0L7 329.7c-9.4-9.4-9.4-24.6 0-33.9l136-136c9.4-9.5 24.6-9.5 34-.1z"
fill="currentColor"
/>
</svg>
</label>
<div
class="sub"
>
<div
class="item"
data-test-id="run-data-schema-item"
@ -351,10 +470,22 @@ exports[`RunDataJsonSchema.vue > renders schema with spaces and dots 1`] = `
data-target="mappable"
data-value="{{ $json['hello world'][0] }}"
>
<font-awesome-icon-stub
icon="cube"
size="sm"
/>
<svg
aria-hidden="true"
class="svg-inline--fa fa-cube fa-w-16 fa-sm"
data-icon="cube"
data-prefix="fas"
focusable="false"
role="img"
viewBox="0 0 512 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
class=""
d="M239.1 6.3l-208 78c-18.7 7-31.1 25-31.1 45v225.1c0 18.2 10.3 34.8 26.5 42.9l208 104c13.5 6.8 29.4 6.8 42.9 0l208-104c16.3-8.1 26.5-24.8 26.5-42.9V129.3c0-20-12.4-37.9-31.1-44.9l-208-78C262 2.2 250 2.2 239.1 6.3zM256 68.4l192 72v1.1l-192 78-192-78v-1.1l192-72zm32 356V275.5l160-65v133.9l-160 80z"
fill="currentColor"
/>
</svg>
<span>
hello world
</span>
@ -365,9 +496,8 @@ exports[`RunDataJsonSchema.vue > renders schema with spaces and dots 1`] = `
</span>
</span>
</div>
<!---->
<!--v-if-->
<input
checked="checked"
id="input_object-1-0"
type="checkbox"
/>
@ -375,13 +505,27 @@ exports[`RunDataJsonSchema.vue > renders schema with spaces and dots 1`] = `
class="toggle"
for="input_object-1-0"
>
<font-awesome-icon-stub
icon="angle-up"
/>
<svg
aria-hidden="true"
class="svg-inline--fa fa-angle-up fa-w-10"
data-icon="angle-up"
data-prefix="fas"
focusable="false"
role="img"
viewBox="0 0 320 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
class=""
d="M177 159.7l136 136c9.4 9.4 9.4 24.6 0 33.9l-22.6 22.6c-9.4 9.4-24.6 9.4-33.9 0L160 255.9l-96.4 96.4c-9.4 9.4-24.6 9.4-33.9 0L7 329.7c-9.4-9.4-9.4-24.6 0-33.9l136-136c9.4-9.5 24.6-9.5 34-.1z"
fill="currentColor"
/>
</svg>
</label>
<div
class="sub"
>
<div
class="item"
data-test-id="run-data-schema-item"
@ -399,11 +543,23 @@ exports[`RunDataJsonSchema.vue > renders schema with spaces and dots 1`] = `
data-target="mappable"
data-value="{{ $json['hello world'][0].test }}"
>
<font-awesome-icon-stub
icon="cube"
size="sm"
/>
<!---->
<svg
aria-hidden="true"
class="svg-inline--fa fa-cube fa-w-16 fa-sm"
data-icon="cube"
data-prefix="fas"
focusable="false"
role="img"
viewBox="0 0 512 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
class=""
d="M239.1 6.3l-208 78c-18.7 7-31.1 25-31.1 45v225.1c0 18.2 10.3 34.8 26.5 42.9l208 104c13.5 6.8 29.4 6.8 42.9 0l208-104c16.3-8.1 26.5-24.8 26.5-42.9V129.3c0-20-12.4-37.9-31.1-44.9l-208-78C262 2.2 250 2.2 239.1 6.3zM256 68.4l192 72v1.1l-192 78-192-78v-1.1l192-72zm32 356V275.5l160-65v133.9l-160 80z"
fill="currentColor"
/>
</svg>
<!--v-if-->
<span
class=""
>
@ -411,9 +567,8 @@ exports[`RunDataJsonSchema.vue > renders schema with spaces and dots 1`] = `
</span>
</span>
</div>
<!---->
<!--v-if-->
<input
checked="checked"
id="input_object-2-0"
type="checkbox"
/>
@ -421,13 +576,27 @@ exports[`RunDataJsonSchema.vue > renders schema with spaces and dots 1`] = `
class="toggle"
for="input_object-2-0"
>
<font-awesome-icon-stub
icon="angle-up"
/>
<svg
aria-hidden="true"
class="svg-inline--fa fa-angle-up fa-w-10"
data-icon="angle-up"
data-prefix="fas"
focusable="false"
role="img"
viewBox="0 0 320 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
class=""
d="M177 159.7l136 136c9.4 9.4 9.4 24.6 0 33.9l-22.6 22.6c-9.4 9.4-24.6 9.4-33.9 0L160 255.9l-96.4 96.4c-9.4 9.4-24.6 9.4-33.9 0L7 329.7c-9.4-9.4-9.4-24.6 0-33.9l136-136c9.4-9.5 24.6-9.5 34-.1z"
fill="currentColor"
/>
</svg>
</label>
<div
class="sub"
>
<div
class="item"
data-test-id="run-data-schema-item"
@ -445,11 +614,23 @@ exports[`RunDataJsonSchema.vue > renders schema with spaces and dots 1`] = `
data-target="mappable"
data-value="{{ $json['hello world'][0].test['more to think about'] }}"
>
<font-awesome-icon-stub
icon="hashtag"
size="sm"
/>
<!---->
<svg
aria-hidden="true"
class="svg-inline--fa fa-hashtag fa-w-14 fa-sm"
data-icon="hashtag"
data-prefix="fas"
focusable="false"
role="img"
viewBox="0 0 448 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
class=""
d="M440.667 182.109l7.143-40c1.313-7.355-4.342-14.109-11.813-14.109h-74.81l14.623-81.891C377.123 38.754 371.468 32 363.997 32h-40.632a12 12 0 0 0-11.813 9.891L296.175 128H197.54l14.623-81.891C213.477 38.754 207.822 32 200.35 32h-40.632a12 12 0 0 0-11.813 9.891L132.528 128H53.432a12 12 0 0 0-11.813 9.891l-7.143 40C33.163 185.246 38.818 192 46.289 192h74.81L98.242 320H19.146a12 12 0 0 0-11.813 9.891l-7.143 40C-1.123 377.246 4.532 384 12.003 384h74.81L72.19 465.891C70.877 473.246 76.532 480 84.003 480h40.632a12 12 0 0 0 11.813-9.891L151.826 384h98.634l-14.623 81.891C234.523 473.246 240.178 480 247.65 480h40.632a12 12 0 0 0 11.813-9.891L315.472 384h79.096a12 12 0 0 0 11.813-9.891l7.143-40c1.313-7.355-4.342-14.109-11.813-14.109h-74.81l22.857-128h79.096a12 12 0 0 0 11.813-9.891zM261.889 320h-98.634l22.857-128h98.634l-22.857 128z"
fill="currentColor"
/>
</svg>
<!--v-if-->
<span
class=""
>
@ -458,14 +639,15 @@ exports[`RunDataJsonSchema.vue > renders schema with spaces and dots 1`] = `
</span>
</div>
<span
class="ph-no-capture text"
class="text ph-no-capture"
>
1
</span>
<!---->
<!---->
<!---->
<!--v-if-->
<!--v-if-->
<!--v-if-->
</div>
</div>
</div>
<div
@ -485,11 +667,23 @@ exports[`RunDataJsonSchema.vue > renders schema with spaces and dots 1`] = `
data-target="mappable"
data-value="{{ $json['hello world'][0]['test.how'] }}"
>
<font-awesome-icon-stub
icon="font"
size="sm"
/>
<!---->
<svg
aria-hidden="true"
class="svg-inline--fa fa-font fa-w-14 fa-sm"
data-icon="font"
data-prefix="fas"
focusable="false"
role="img"
viewBox="0 0 448 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
class=""
d="M432 416h-23.41L277.88 53.69A32 32 0 0 0 247.58 32h-47.16a32 32 0 0 0-30.3 21.69L39.41 416H16a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h128a16 16 0 0 0 16-16v-32a16 16 0 0 0-16-16h-19.58l23.3-64h152.56l23.3 64H304a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h128a16 16 0 0 0 16-16v-32a16 16 0 0 0-16-16zM176.85 272L224 142.51 271.15 272z"
fill="currentColor"
/>
</svg>
<!--v-if-->
<span
class=""
>
@ -498,25 +692,27 @@ exports[`RunDataJsonSchema.vue > renders schema with spaces and dots 1`] = `
</span>
</div>
<span
class="ph-no-capture text"
class="text ph-no-capture"
>
ignore
</span>
<!---->
<!---->
<!---->
<!--v-if-->
<!--v-if-->
<!--v-if-->
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div
class="teleporter hidden"
data-v-d4e6e290=""
/>
<!--teleport start-->
<!--teleport end-->
</div>
</div>
</div>

View file

@ -1,7 +1,7 @@
// Vitest Snapshot v1
exports[`VariablesRow > should render correctly 1`] = `
"<tr data-test-id=\\"variables-row\\" class=\\"variablesRow\\">
"<tr class=\\"variablesRow\\" data-test-id=\\"variables-row\\">
<td class=\\"variables-key-column\\">
<div><span>key</span></div>
</td>
@ -9,70 +9,84 @@ exports[`VariablesRow > should render correctly 1`] = `
<div><span>value</span></div>
</td>
<td class=\\"variables-usage-column\\">
<div>
<n8n-tooltip-stub justifybuttons=\\"flex-end\\" buttons=\\"\\" placement=\\"top\\"><span class=\\"usageSyntax\\">$vars.key</span></n8n-tooltip-stub>
<div><span class=\\"usageSyntax el-tooltip__trigger el-tooltip__trigger\\">$vars.key</span>
<!--teleport start-->
<!--teleport end-->
</div>
</td>
<td>
<div class=\\"buttons hoverButtons\\">
<n8n-tooltip-stub justifybuttons=\\"flex-end\\" buttons=\\"\\" placement=\\"top\\">
<div><button disabled=\\"disabled\\" aria-disabled=\\"true\\" aria-live=\\"polite\\" class=\\"mr-xs button button tertiary medium disabled\\" data-test-id=\\"variable-row-edit-button\\">
<!----><span> Edit </span></button></div>
</n8n-tooltip-stub>
<n8n-tooltip-stub justifybuttons=\\"flex-end\\" buttons=\\"\\" placement=\\"top\\">
<div><button disabled=\\"disabled\\" aria-disabled=\\"true\\" aria-live=\\"polite\\" class=\\"button button tertiary medium disabled\\" data-test-id=\\"variable-row-delete-button\\">
<!----><span> Delete </span></button></div>
</n8n-tooltip-stub>
<div class=\\"el-tooltip__trigger\\"><button class=\\"button button tertiary medium disabled mr-xs mr-xs\\" disabled=\\"\\" aria-disabled=\\"true\\" aria-live=\\"polite\\" data-test-id=\\"variable-row-edit-button\\">
<!--v-if--><span>Edit</span>
</button></div>
<!--teleport start-->
<!--teleport end-->
<div class=\\"el-tooltip__trigger\\"><button class=\\"button button tertiary medium disabled\\" disabled=\\"\\" aria-disabled=\\"true\\" aria-live=\\"polite\\" data-test-id=\\"variable-row-delete-button\\">
<!--v-if--><span>Delete</span>
</button></div>
<!--teleport start-->
<!--teleport end-->
</div>
</td>
</tr>"
`;
exports[`VariablesRow > should show key and value inputs in edit mode 1`] = `
"<tr data-test-id=\\"variables-row\\" class=\\"variablesRow\\">
"<tr class=\\"variablesRow\\" data-test-id=\\"variables-row\\">
<td class=\\"variables-key-column\\">
<div>
<div class=\\"container\\" data-test-id=\\"variable-row-key-input\\">
<!---->
<!--v-if-->
<div class=\\"\\">
<div class=\\"el-input el-input--large n8n-input\\">
<!----><input type=\\"text\\" autocomplete=\\"off\\" name=\\"key\\" placeholder=\\"Enter a name\\" class=\\"el-input__inner\\">
<!---->
<!---->
<!---->
<!---->
<!-- input -->
<!-- prepend slot -->
<!--v-if-->
<div class=\\"el-input__wrapper is-focus\\">
<!-- prefix slot -->
<!--v-if--><input class=\\"el-input__inner\\" autocomplete=\\"off\\" name=\\"key\\" rows=\\"2\\" maxlength=\\"Infinity\\" title=\\"\\" type=\\"text\\" tabindex=\\"0\\" placeholder=\\"Enter a name\\" id=\\"el-id-5581-7\\"><!-- suffix slot -->
<!--v-if-->
</div><!-- append slot -->
<!--v-if-->
</div>
</div>
<!---->
<!--v-if-->
</div>
</div>
</td>
<td class=\\"variables-value-column\\">
<div>
<div class=\\"container\\" data-test-id=\\"variable-row-value-input\\">
<!---->
<!--v-if-->
<div class=\\"\\">
<div class=\\"el-input el-input--large n8n-input\\">
<!----><input type=\\"text\\" autocomplete=\\"off\\" name=\\"value\\" placeholder=\\"Enter a value\\" class=\\"el-input__inner\\">
<!---->
<!---->
<!---->
<!---->
<!-- input -->
<!-- prepend slot -->
<!--v-if-->
<div class=\\"el-input__wrapper\\">
<!-- prefix slot -->
<!--v-if--><input class=\\"el-input__inner\\" autocomplete=\\"off\\" name=\\"value\\" rows=\\"2\\" maxlength=\\"Infinity\\" title=\\"\\" type=\\"text\\" tabindex=\\"0\\" placeholder=\\"Enter a value\\" id=\\"el-id-5581-8\\"><!-- suffix slot -->
<!--v-if-->
</div><!-- append slot -->
<!--v-if-->
</div>
</div>
<!---->
<!--v-if-->
</div>
</div>
</td>
<td class=\\"variables-usage-column\\">
<div>
<n8n-tooltip-stub justifybuttons=\\"flex-end\\" buttons=\\"\\" placement=\\"top\\"><span class=\\"usageSyntax\\">$vars.key</span></n8n-tooltip-stub>
<div><span class=\\"usageSyntax el-tooltip__trigger el-tooltip__trigger\\">$vars.key</span>
<!--teleport start-->
<!--teleport end-->
</div>
</td>
<td>
<div class=\\"buttons\\"><button aria-live=\\"polite\\" class=\\"mr-xs button button tertiary medium\\" data-test-id=\\"variable-row-cancel-button\\">
<!----><span> Cancel </span></button><button aria-live=\\"polite\\" class=\\"button button primary medium\\" data-test-id=\\"variable-row-save-button\\">
<!----><span> Save </span></button></div>
<div class=\\"buttons\\"><button class=\\"button button tertiary medium mr-xs mr-xs\\" aria-live=\\"polite\\" data-test-id=\\"variable-row-cancel-button\\">
<!--v-if--><span>Cancel</span>
</button><button class=\\"button button primary medium\\" aria-live=\\"polite\\" data-test-id=\\"variable-row-save-button\\">
<!--v-if--><span>Save</span>
</button></div>
</td>
</tr>"
`;

View file

@ -101,6 +101,7 @@ export const DEFAULT_SETUP = {
dependentParametersValues: 'gid=0',
inputSize: 'small',
labelSize: 'small',
teleported: false,
node: {
parameters: NODE_PARAMETER_VALUES,
id: 'f63efb2d-3cc5-4500-89f9-b39aab19baf5',

View file

@ -3,7 +3,7 @@
<template #aside v-if="showAside">
<div :class="[$style['heading-wrapper'], 'mb-xs']">
<n8n-heading size="2xlarge">
{{ $locale.baseText(`${resourceKey}.heading`) }}
{{ i18n.baseText(`${resourceKey}.heading`) }}
</n8n-heading>
</div>
@ -16,7 +16,7 @@
@click="$emit('click:add', $event)"
data-test-id="resources-list-add"
>
{{ $locale.baseText(`${resourceKey}.add`) }}
{{ i18n.baseText(`${resourceKey}.add`) }}
</n8n-button>
</slot>
</div>
@ -24,8 +24,8 @@
<enterprise-edition :features="[EnterpriseEditionFeature.Sharing]" v-if="shareable">
<resource-ownership-select
v-model="isOwnerSubview"
:my-resources-label="$locale.baseText(`${resourceKey}.menu.my`)"
:all-resources-label="$locale.baseText(`${resourceKey}.menu.all`)"
:my-resources-label="i18n.baseText(`${resourceKey}.menu.my`)"
:all-resources-label="i18n.baseText(`${resourceKey}.menu.all`)"
/>
</enterprise-edition>
</template>
@ -42,7 +42,7 @@
data-test-id="empty-resources-list"
emoji="👋"
:heading="
$locale.baseText(
i18n.baseText(
usersStore.currentUser.firstName
? `${resourceKey}.empty.heading`
: `${resourceKey}.empty.heading.userNotSetup`,
@ -51,8 +51,8 @@
},
)
"
:description="$locale.baseText(`${resourceKey}.empty.description`)"
:buttonText="$locale.baseText(`${resourceKey}.empty.button`)"
:description="i18n.baseText(`${resourceKey}.empty.description`)"
:buttonText="i18n.baseText(`${resourceKey}.empty.button`)"
buttonType="secondary"
@click:button="$emit('click:add', $event)"
/>
@ -65,7 +65,7 @@
<n8n-input
:modelValue="filtersModel.search"
:class="[$style['search'], 'mr-2xs']"
:placeholder="$locale.baseText(`${resourceKey}.search.placeholder`)"
:placeholder="i18n.baseText(`${resourceKey}.search.placeholder`)"
clearable
ref="search"
data-test-id="resources-list-search"
@ -82,7 +82,7 @@
data-test-id="resources-list-sort-item"
:key="sortOption"
:value="sortOption"
:label="$locale.baseText(`${resourceKey}.sort.${sortOption}`)"
:label="i18n.baseText(`${resourceKey}.sort.${sortOption}`)"
/>
</n8n-select>
<resource-filters-dropdown
@ -106,9 +106,9 @@
<div v-if="showFiltersDropdown" v-show="hasFilters" class="mt-xs">
<n8n-info-tip :bold="false">
{{ $locale.baseText(`${resourceKey}.filters.active`) }}
{{ i18n.baseText(`${resourceKey}.filters.active`) }}
<n8n-link @click="resetFilters" size="small">
{{ $locale.baseText(`${resourceKey}.filters.active.reset`) }}
{{ i18n.baseText(`${resourceKey}.filters.active.reset`) }}
</n8n-link>
</n8n-info-tip>
</div>
@ -153,22 +153,20 @@
</div>
<n8n-text color="text-base" size="medium" data-test-id="resources-list-empty" v-else>
{{ $locale.baseText(`${resourceKey}.noResults`) }}
{{ i18n.baseText(`${resourceKey}.noResults`) }}
<template v-if="shouldSwitchToAllSubview">
<span v-if="!filtersModel.search">
({{ $locale.baseText(`${resourceKey}.noResults.switchToShared.preamble`) }}
({{ i18n.baseText(`${resourceKey}.noResults.switchToShared.preamble`) }}
<n8n-link @click="setOwnerSubview(false)">
{{ $locale.baseText(`${resourceKey}.noResults.switchToShared.link`) }} </n8n-link
{{ i18n.baseText(`${resourceKey}.noResults.switchToShared.link`) }} </n8n-link
>)
</span>
<span v-else>
({{
$locale.baseText(`${resourceKey}.noResults.withSearch.switchToShared.preamble`)
}}
({{ i18n.baseText(`${resourceKey}.noResults.withSearch.switchToShared.preamble`) }}
<n8n-link @click="setOwnerSubview(false)">
{{
$locale.baseText(`${resourceKey}.noResults.withSearch.switchToShared.link`)
i18n.baseText(`${resourceKey}.noResults.withSearch.switchToShared.link`)
}} </n8n-link
>)
</span>
@ -197,6 +195,7 @@ import { useSettingsStore } from '@/stores/settings.store';
import { useUsersStore } from '@/stores/users.store';
import type { N8nInput } from 'n8n-design-system';
import type { DatatableColumn } from 'n8n-design-system';
import { useI18n } from '@/composables';
export interface IResource {
id: string;
@ -286,6 +285,13 @@ export default defineComponent({
}),
},
},
setup() {
const i18n = useI18n();
return {
i18n,
};
},
data() {
return {
loading: true,
@ -413,7 +419,7 @@ export default defineComponent({
this.isOwnerSubview = active;
},
getTelemetrySubview(): string {
return this.$locale.baseText(
return this.i18n.baseText(
`${this.resourceKey as IResourceKeyType}.menu.${this.isOwnerSubview ? 'my' : 'all'}`,
);
},

View file

@ -1,19 +1,5 @@
import { i18n } from '@/plugins/i18n';
import { useRootStore } from '@/stores/n8nRoot.store';
export function useI18n() {
const isEnglishLocale = useRootStore().defaultLocale === 'en';
function localizeNodeName(nodeName: string, type: string) {
if (isEnglishLocale) return nodeName;
const nodeTypeName = i18n.shortNodeType(type);
return i18n.headerText({
key: `headers.${nodeTypeName}.displayName`,
fallback: nodeName,
});
}
return { i18n, localizeNodeName };
return i18n;
}

View file

@ -8,7 +8,7 @@ interface LoadingService {
}
export function useLoadingService() {
const { i18n } = useI18n();
const i18n = useI18n();
const loadingService = ref<LoadingService | null>(null);
function startLoading(text?: string) {

View file

@ -17,7 +17,7 @@ export function useToast() {
const telemetry = useTelemetry();
const workflowsStore = useWorkflowsStore();
const externalHooks = useExternalHooks();
const { i18n } = useI18n();
const i18n = useI18n();
function showMessage(
messageData: Omit<NotificationOptions, 'message'> & { message?: string },

View file

@ -568,3 +568,6 @@ export const CLOUD_BASE_URL_STAGING = 'https://stage-api.n8n.cloud';
export const CLOUD_BASE_URL_PRODUCTION = 'https://api.n8n.cloud';
export const CLOUD_TRIAL_CHECK_INTERVAL = 5000;
// A path that does not exist so that nothing is selected by default
export const nonExistingJsonPath = '_!^&*';

View file

@ -1,3 +1,3 @@
import { createEventBus } from 'n8n-design-system';
import { createEventBus } from 'n8n-design-system/utils';
export const codeNodeEditorEventBus = createEventBus();

View file

@ -1,3 +1,3 @@
import { createEventBus } from 'n8n-design-system';
import { createEventBus } from 'n8n-design-system/utils';
export const dataPinningEventBus = createEventBus();

View file

@ -1,3 +1,3 @@
import { createEventBus } from 'n8n-design-system';
import { createEventBus } from 'n8n-design-system/utils';
export const htmlEditorEventBus = createEventBus();

View file

@ -1,3 +1,3 @@
import { createEventBus } from 'n8n-design-system';
import { createEventBus } from 'n8n-design-system/utils';
export const globalLinkActionsEventBus = createEventBus();

View file

@ -1,3 +1,3 @@
import { createEventBus } from 'n8n-design-system';
import { createEventBus } from 'n8n-design-system/utils';
export const nodeViewEventBus = createEventBus();

View file

@ -1,5 +1,3 @@
// The Vue build version to load with the `import` command
// (runtime-only or standalone) has been set in webpack.base.conf with an alias.
import { createApp } from 'vue';
import 'vue-json-pretty/lib/styles.css';
@ -30,8 +28,6 @@ const pinia = createPinia();
const app = createApp(App);
// Vue.config.productionTip = false;
app.use(TelemetryPlugin);
app.use(PiniaVuePlugin);
app.use(I18nPlugin);
@ -44,13 +40,6 @@ app.use(i18nInstance);
app.mount('#app');
// new Vue({
// i18n: i18nInstance,
// router,
// pinia,
// render: (h) => h(App),
// }).$mount('#app');
router.afterEach((to, from) => {
void runExternalHook('main.routeChange', useWebhooksStore(), { from, to });
});

View file

@ -1,7 +1,7 @@
import type { INodeUi } from '@/Interface';
import type { IConnection } from 'n8n-workflow';
import type { XYPosition } from '../Interface';
import { createEventBus } from 'n8n-design-system';
import { createEventBus } from 'n8n-design-system/utils';
// Command names don't serve any particular purpose in the app
// but they make it easier to identify each command on stack

View file

@ -3,10 +3,10 @@ import type { Plugin } from 'vue';
import 'regenerator-runtime/runtime';
import ElementPlus from 'element-plus';
import { ElLoading, ElMessageBox, ElNotification } from 'element-plus';
import { ElLoading, ElMessageBox } from 'element-plus';
import { N8nPlugin } from 'n8n-design-system';
import EnterpriseEdition from '@/components/EnterpriseEdition.ee.vue';
import { useMessage } from '@/composables/useMessage';
import EnterpriseEdition from '@/components/EnterpriseEdition.ee.vue';
export const GlobalComponentsPlugin: Plugin<{}> = {
install(app) {
@ -17,8 +17,8 @@ export const GlobalComponentsPlugin: Plugin<{}> = {
app.use(ElementPlus);
app.use(N8nPlugin);
app.use(ElLoading);
app.use(ElNotification);
// app.use(ElLoading);
// app.use(ElNotification);
app.config.globalProperties.$loading = ElLoading.service;
app.config.globalProperties.$msgbox = ElMessageBox;

View file

@ -14,6 +14,7 @@ import englishBaseText from './locales/en.json';
import { useUIStore } from '@/stores/ui.store';
import { useNDVStore } from '@/stores/ndv.store';
import type { INodeProperties, INodePropertyCollection, INodePropertyOptions } from 'n8n-workflow';
import { useRootStore } from '@/stores';
export const i18nInstance = createI18n({
locale: 'en',
@ -311,6 +312,19 @@ export class I18nClass {
};
}
localizeNodeName(nodeName: string, type: string) {
const isEnglishLocale = useRootStore().defaultLocale === 'en';
if (isEnglishLocale) return nodeName;
const nodeTypeName = this.shortNodeType(type);
return this.headerText({
key: `headers.${nodeTypeName}.displayName`,
fallback: nodeName,
});
}
rootVars: Record<string, string | undefined> = {
$binary: this.baseText('codeNodeEditor.completer.binary'),
$execution: this.baseText('codeNodeEditor.completer.$execution'),
@ -595,13 +609,13 @@ export const i18n: I18nClass = new I18nClass();
export const I18nPlugin: Plugin<{}> = {
async install(app) {
await locale.use('en');
locale.i18n((key: string, options?: { interpolate: Record<string, unknown> }) =>
i18nInstance.global.t(key, options?.interpolate || {}),
);
app.config.globalProperties.$locale = i18n;
await locale.use('en');
},
};
@ -609,12 +623,6 @@ export const I18nPlugin: Plugin<{}> = {
// typings
// ----------------------------------
declare module '@vue/runtime-core' {
interface ComponentCustomProperties {
$locale: I18nClass;
}
}
type GetBaseTextKey<T> = T extends `_${string}` ? never : T;
export type BaseTextKey = GetBaseTextKey<keyof typeof englishBaseText>;

View file

@ -1,11 +1,3 @@
import type { Telemetry } from '.';
declare module 'vue/types/vue' {
interface Vue {
$telemetry: Telemetry;
}
}
declare global {
interface Window {
rudderanalytics: RudderStack;
@ -60,3 +52,5 @@ interface RudderStack extends Array<unknown> {
reset(): void;
}
export {};

View file

@ -1,12 +1,22 @@
declare module '*.vue' {
import type { DefineComponent } from 'vue';
const component: DefineComponent<{}, {}, any>;
export default component;
}
import type { I18nClass } from '@/plugins/i18n';
import type { Route } from 'vue-router';
import type { Telemetry } from '@/plugins/telemetry';
declare module 'vue' {
interface ComponentCustomOptions {
beforeRouteEnter?(to: Route, from: Route, next: () => void): void;
beforeRouteLeave?(to: Route, from: Route, next: () => void): void;
beforeRouteUpdate?(to: Route, from: Route, next: () => void): void;
}
// @TODO Check if still needed
declare module '@vue/runtime-core' {
interface ComponentCustomProperties {
$style: Record<string, string>;
$locale: I18nClass;
$telemetry: Telemetry;
}
}
/**
* @docs https://vuejs.org/guide/typescript/options-api.html#augmenting-global-properties
*/
export {};

View file

@ -1,7 +1,9 @@
import { computed, reactive } from 'vue';
import { defineStore } from 'pinia';
import { EnterpriseEditionFeature } from '@/constants';
import { useSettingsStore, useRootStore, useUsersStore } from '@/stores';
import { useSettingsStore } from '@/stores/settings.store';
import { useRootStore } from '@/stores/n8nRoot.store';
import { useUsersStore } from '@/stores/users.store';
import * as vcApi from '@/api/sourceControl';
import type { SourceControlPreferences } from '@/Interface';

View file

@ -348,14 +348,16 @@ export default defineComponent({
CanvasControls,
},
setup(props) {
const locale = useI18n();
return {
locale,
...useCanvasMouseSelect(),
...useGlobalLinkActions(),
...useTitleChange(),
...useToast(),
...useMessage(),
...useUniqueNodeName(),
...useI18n(),
...workflowRun.setup?.(props),
};
},

View file

@ -2,7 +2,7 @@
import { useI18n } from '@/composables';
import { useUIStore, useAuditLogsStore } from '@/stores';
const { i18n: locale } = useI18n();
const locale = useI18n();
const uiStore = useUIStore();
const auditLogsStore = useAuditLogsStore();

View file

@ -162,7 +162,7 @@ import { mapStores } from 'pinia';
import { useUsersStore } from '@/stores/users.store';
import { useSettingsStore } from '@/stores/settings.store';
import { useUIStore } from '@/stores';
import { createEventBus } from 'n8n-design-system';
import { createEventBus } from 'n8n-design-system/utils';
import type { N8nFormInputs } from 'n8n-design-system';
import type { CellStyle } from 'element-plus';

View file

@ -88,7 +88,7 @@ import { LOG_STREAM_MODAL_KEY, EnterpriseEditionFeature } from '../constants';
import type { MessageEventBusDestinationOptions } from 'n8n-workflow';
import { deepCopy, defaultMessageEventBusDestinationOptions } from 'n8n-workflow';
import EventDestinationCard from '@/components/SettingsLogStreaming/EventDestinationCard.ee.vue';
import { createEventBus } from 'n8n-design-system';
import { createEventBus } from 'n8n-design-system/utils';
export default defineComponent({
name: 'SettingsLogStreamingView',

View file

@ -2,7 +2,7 @@
<div :class="$style.container" data-test-id="personal-settings-container">
<div :class="$style.header">
<n8n-heading size="2xlarge">{{
$locale.baseText('settings.personal.personalSettings')
i18n.baseText('settings.personal.personalSettings')
}}</n8n-heading>
<div class="ph-no-capture" :class="$style.user">
<span :class="$style.username" data-test-id="current-user-name">
@ -18,7 +18,7 @@
<div>
<div :class="$style.sectionHeader">
<n8n-heading size="large">{{
$locale.baseText('settings.personal.basicInformation')
i18n.baseText('settings.personal.basicInformation')
}}</n8n-heading>
</div>
<div data-test-id="personal-data-form">
@ -34,12 +34,12 @@
</div>
<div v-if="!signInWithLdap && !signInWithSaml">
<div :class="$style.sectionHeader">
<n8n-heading size="large">{{ $locale.baseText('settings.personal.security') }}</n8n-heading>
<n8n-heading size="large">{{ i18n.baseText('settings.personal.security') }}</n8n-heading>
</div>
<div>
<n8n-input-label :label="$locale.baseText('auth.password')">
<n8n-input-label :label="i18n.baseText('auth.password')">
<n8n-link @click="openPasswordModal" data-test-id="change-password-link">{{
$locale.baseText('auth.changePassword')
i18n.baseText('auth.changePassword')
}}</n8n-link>
</n8n-input-label>
</div>
@ -47,7 +47,7 @@
<div>
<n8n-button
float="right"
:label="$locale.baseText('settings.personal.save')"
:label="i18n.baseText('settings.personal.save')"
size="large"
:disabled="!hasAnyChanges || !readyToSubmit"
data-test-id="save-settings-button"
@ -58,7 +58,7 @@
</template>
<script lang="ts">
import { useToast } from '@/composables';
import { useI18n, useToast } from '@/composables';
import { CHANGE_PASSWORD_MODAL_KEY } from '@/constants';
import type { IFormInputs, IUser } from '@/Interface';
import { useUIStore } from '@/stores/ui.store';
@ -66,12 +66,15 @@ import { useUsersStore } from '@/stores/users.store';
import { useSettingsStore } from '@/stores/settings.store';
import { mapStores } from 'pinia';
import { defineComponent } from 'vue';
import { createEventBus } from 'n8n-design-system';
import { createEventBus } from 'n8n-design-system/utils';
export default defineComponent({
name: 'SettingsPersonalView',
setup() {
const i18n = useI18n();
return {
i18n,
...useToast(),
};
},
@ -89,7 +92,7 @@ export default defineComponent({
name: 'firstName',
initialValue: this.currentUser?.firstName,
properties: {
label: this.$locale.baseText('auth.firstName'),
label: this.i18n.baseText('auth.firstName'),
maxlength: 32,
required: true,
autocomplete: 'given-name',
@ -101,7 +104,7 @@ export default defineComponent({
name: 'lastName',
initialValue: this.currentUser?.lastName,
properties: {
label: this.$locale.baseText('auth.lastName'),
label: this.i18n.baseText('auth.lastName'),
maxlength: 32,
required: true,
autocomplete: 'family-name',
@ -113,7 +116,7 @@ export default defineComponent({
name: 'email',
initialValue: this.currentUser?.email,
properties: {
label: this.$locale.baseText('auth.email'),
label: this.i18n.baseText('auth.email'),
type: 'email',
required: true,
validationRules: [{ name: 'VALID_EMAIL' }],
@ -160,13 +163,13 @@ export default defineComponent({
email: form.email,
});
this.showToast({
title: this.$locale.baseText('settings.personal.personalSettingsUpdated'),
title: this.i18n.baseText('settings.personal.personalSettingsUpdated'),
message: '',
type: 'success',
});
this.hasAnyChanges = false;
} catch (e) {
this.showError(e, this.$locale.baseText('settings.personal.personalSettingsUpdatedError'));
this.showError(e, this.i18n.baseText('settings.personal.personalSettingsUpdatedError'));
}
},
onSaveClick() {

View file

@ -6,7 +6,7 @@ import { useUIStore, useSourceControlStore } from '@/stores';
import { useToast, useMessage, useLoadingService, useI18n } from '@/composables';
import CopyInput from '@/components/CopyInput.vue';
const { i18n: locale } = useI18n();
const locale = useI18n();
const sourceControlStore = useSourceControlStore();
const uiStore = useUIStore();
const toast = useToast();

View file

@ -10,7 +10,7 @@ const IdentityProviderSettingsType = {
XML: 'xml',
};
const { i18n } = useI18n();
const i18n = useI18n();
const ssoStore = useSSOStore();
const uiStore = useUIStore();
const message = useMessage();

View file

@ -26,7 +26,7 @@ const environmentsStore = useEnvironmentsStore();
const usersStore = useUsersStore();
const uiStore = useUIStore();
const telemetry = useTelemetry();
const { i18n } = useI18n();
const i18n = useI18n();
const message = useMessage();
const sourceControlStore = useSourceControlStore();
let sourceControlStoreUnsubscribe = () => {};

View file

@ -10,17 +10,16 @@ const renderComponent = (renderOptions: Parameters<typeof render>[1] = {}) =>
merge(
{
pinia: createTestingPinia(),
stubs: {
SSOLogin: {
template: '<div data-test-id="sso-login"></div>',
global: {
stubs: {
SSOLogin: {
template: '<div data-test-id="sso-login"></div>',
},
},
},
},
renderOptions,
),
(vue) => {
vue.use(PiniaVuePlugin);
},
);
describe('AuthView', () => {

View file

@ -33,9 +33,6 @@ const renderComponent = (renderOptions: Parameters<typeof render>[1] = {}) =>
},
renderOptions,
),
(vue) => {
vue.use(PiniaVuePlugin);
},
);
describe('SamlOnboarding', () => {

View file

@ -20,9 +20,6 @@ const renderComponent = (renderOptions: Parameters<typeof render>[1] = {}) =>
},
renderOptions,
),
(vue) => {
vue.use(PiniaVuePlugin);
},
);
describe('SettingsAuditLogs', () => {

Some files were not shown because too many files have changed in this diff Show more