mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-18 02:21:13 +00:00
fix(editor): Fix broken types for globally defined components (no-changelog) (#16505)
Co-authored-by: Mutasem Aldmour <mutasem@n8n.io>
This commit is contained in:
@@ -86,6 +86,7 @@
|
||||
"vue": "catalog:frontend",
|
||||
"vue-agile": "^2.0.0",
|
||||
"vue-chartjs": "^5.2.0",
|
||||
"vue-component-type-helpers": "^2.2.10",
|
||||
"vue-github-button": "^3.1.3",
|
||||
"vue-i18n": "catalog:frontend",
|
||||
"vue-json-pretty": "2.2.4",
|
||||
|
||||
@@ -293,6 +293,56 @@ export type BaseResource = {
|
||||
name: string;
|
||||
};
|
||||
|
||||
export type FolderResource = BaseFolderItem & {
|
||||
resourceType: 'folder';
|
||||
};
|
||||
|
||||
export type WorkflowResource = BaseResource & {
|
||||
resourceType: 'workflow';
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
active: boolean;
|
||||
isArchived: boolean;
|
||||
homeProject?: ProjectSharingData;
|
||||
scopes?: Scope[];
|
||||
tags?: ITag[] | string[];
|
||||
sharedWithProjects?: ProjectSharingData[];
|
||||
readOnly: boolean;
|
||||
parentFolder?: ResourceParentFolder;
|
||||
};
|
||||
|
||||
export type VariableResource = BaseResource & {
|
||||
resourceType: 'variable';
|
||||
key?: string;
|
||||
value?: string;
|
||||
};
|
||||
|
||||
export type CredentialsResource = BaseResource & {
|
||||
resourceType: 'credential';
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
type: string;
|
||||
homeProject?: ProjectSharingData;
|
||||
scopes?: Scope[];
|
||||
sharedWithProjects?: ProjectSharingData[];
|
||||
readOnly: boolean;
|
||||
needsSetup: boolean;
|
||||
};
|
||||
|
||||
export type Resource = WorkflowResource | FolderResource | CredentialsResource | VariableResource;
|
||||
|
||||
export type BaseFilters = {
|
||||
search: string;
|
||||
homeProject: string;
|
||||
[key: string]: boolean | string | string[];
|
||||
};
|
||||
|
||||
export type SortingAndPaginationUpdates = {
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
sort?: string;
|
||||
};
|
||||
|
||||
export type WorkflowListItem = Omit<
|
||||
IWorkflowDb,
|
||||
'nodes' | 'connections' | 'settings' | 'pinData' | 'usedCredentials' | 'meta'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Plugin } from 'vue';
|
||||
import type { Component, Plugin } from 'vue';
|
||||
import { render } from '@testing-library/vue';
|
||||
import { i18nInstance } from '@n8n/i18n';
|
||||
import { GlobalComponentsPlugin } from '@/plugins/components';
|
||||
@@ -10,6 +10,7 @@ import type { Telemetry } from '@/plugins/telemetry';
|
||||
import vueJsonPretty from 'vue-json-pretty';
|
||||
import merge from 'lodash/merge';
|
||||
import type { TestingPinia } from '@pinia/testing';
|
||||
import * as components from '@n8n/design-system/components';
|
||||
|
||||
export type RenderComponent = Parameters<typeof render>[0];
|
||||
export type RenderOptions = Parameters<typeof render>[1] & {
|
||||
@@ -25,6 +26,14 @@ const TelemetryPlugin: Plugin<{}> = {
|
||||
},
|
||||
};
|
||||
|
||||
const TestingGlobalComponentsPlugin: Plugin<{}> = {
|
||||
install(app) {
|
||||
for (const [name, component] of Object.entries(components)) {
|
||||
app.component(name, component as unknown as Component);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
const defaultOptions = {
|
||||
global: {
|
||||
stubs: {
|
||||
@@ -38,6 +47,7 @@ const defaultOptions = {
|
||||
GlobalComponentsPlugin,
|
||||
GlobalDirectivesPlugin,
|
||||
TelemetryPlugin,
|
||||
TestingGlobalComponentsPlugin,
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -76,7 +76,7 @@ const getExpirationTime = (apiKey: ApiKey): string => {
|
||||
|
||||
<template #append>
|
||||
<div ref="cardActions" :class="$style.cardActions">
|
||||
<n8n-action-toggle :actions="ACTION_LIST" theme="dark" @action="onAction" />
|
||||
<N8nActionToggle :actions="ACTION_LIST" theme="dark" @action="onAction" />
|
||||
</div>
|
||||
</template>
|
||||
</n8n-card>
|
||||
|
||||
@@ -6,7 +6,7 @@ import Modal from '@/components/Modal.vue';
|
||||
import { useUsersStore } from '@/stores/users.store';
|
||||
import { createFormEventBus } from '@n8n/design-system/utils';
|
||||
import { createEventBus } from '@n8n/utils/event-bus';
|
||||
import type { IFormInputs, IFormInput } from '@/Interface';
|
||||
import type { IFormInputs, IFormInput, FormFieldValueUpdate, FormValues } from '@/Interface';
|
||||
import { useI18n } from '@n8n/i18n';
|
||||
|
||||
const config = ref<IFormInputs | null>(null);
|
||||
@@ -33,17 +33,14 @@ const passwordsMatch = (value: string | number | boolean | null | undefined) =>
|
||||
return false;
|
||||
};
|
||||
|
||||
const onInput = (e: { name: string; value: string }) => {
|
||||
if (e.name === 'password') {
|
||||
const onInput = (e: FormFieldValueUpdate) => {
|
||||
if (e.name === 'password' && typeof e.value === 'string') {
|
||||
password.value = e.value;
|
||||
}
|
||||
};
|
||||
|
||||
const onSubmit = async (values: {
|
||||
currentPassword: string;
|
||||
password: string;
|
||||
mfaCode?: string;
|
||||
}) => {
|
||||
const onSubmit = async (data: FormValues) => {
|
||||
const values = data as { currentPassword: string; password: string; mfaCode?: string };
|
||||
try {
|
||||
loading.value = true;
|
||||
await usersStore.updateCurrentUserPassword({
|
||||
@@ -143,6 +140,7 @@ onMounted(() => {
|
||||
>
|
||||
<template #content>
|
||||
<n8n-form-inputs
|
||||
v-if="config"
|
||||
:inputs="config"
|
||||
:event-bus="formBus"
|
||||
:column-view="true"
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
<script lang="ts" setup>
|
||||
import { useUIStore } from '@/stores/ui.store';
|
||||
import type { PublicInstalledPackage } from 'n8n-workflow';
|
||||
import type { IUser, PublicInstalledPackage } from 'n8n-workflow';
|
||||
import { NPM_PACKAGE_DOCS_BASE_URL, COMMUNITY_PACKAGE_MANAGE_ACTIONS } from '@/constants';
|
||||
import { useI18n } from '@n8n/i18n';
|
||||
import { useTelemetry } from '@/composables/useTelemetry';
|
||||
import { useSettingsStore } from '@/stores/settings.store';
|
||||
import type { UserAction } from '@n8n/design-system';
|
||||
|
||||
interface Props {
|
||||
communityPackage?: PublicInstalledPackage | null;
|
||||
@@ -22,7 +23,7 @@ const i18n = useI18n();
|
||||
const telemetry = useTelemetry();
|
||||
const settingsStore = useSettingsStore();
|
||||
|
||||
const packageActions = [
|
||||
const packageActions: Array<UserAction<IUser>> = [
|
||||
{
|
||||
label: i18n.baseText('settings.communityNodes.viewDocsAction.label'),
|
||||
value: COMMUNITY_PACKAGE_MANAGE_ACTIONS.VIEW_DOCS,
|
||||
|
||||
@@ -12,7 +12,7 @@ import { useProjectsStore } from '@/stores/projects.store';
|
||||
import ProjectCardBadge from '@/components/Projects/ProjectCardBadge.vue';
|
||||
import { useI18n } from '@n8n/i18n';
|
||||
import { ResourceType } from '@/utils/projects.utils';
|
||||
import type { CredentialsResource } from './layouts/ResourcesListLayout.vue';
|
||||
import type { CredentialsResource } from '@/Interface';
|
||||
|
||||
const CREDENTIAL_LIST_ITEM_ACTIONS = {
|
||||
OPEN: 'open',
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { BaseTextKey } from '@n8n/i18n';
|
||||
import type { TestTableColumn } from '@/components/Evaluations.ee/shared/TestTableBase.vue';
|
||||
import { useI18n } from '@n8n/i18n';
|
||||
import { useRouter } from 'vue-router';
|
||||
import type { BadgeTheme } from '@n8n/design-system';
|
||||
|
||||
defineProps<{
|
||||
column: TestTableColumn<T>;
|
||||
@@ -39,7 +40,7 @@ const errorTooltipMap: Record<string, BaseTextKey> = {
|
||||
};
|
||||
|
||||
// FIXME: move status logic to a parent component
|
||||
const statusThemeMap: Record<string, string> = {
|
||||
const statusThemeMap: Record<string, BadgeTheme> = {
|
||||
new: 'default',
|
||||
running: 'warning',
|
||||
evaluation_running: 'warning',
|
||||
|
||||
@@ -206,7 +206,7 @@ defineExpose({ focus, select });
|
||||
outline
|
||||
type="tertiary"
|
||||
icon="external-link-alt"
|
||||
size="xsmall"
|
||||
size="mini"
|
||||
:class="$style['expression-editor-modal-opener']"
|
||||
data-test-id="expander"
|
||||
@click="emit('modal-opener-click')"
|
||||
|
||||
@@ -9,6 +9,7 @@ import { mockedStore } from '@/__tests__/utils';
|
||||
import { useProjectsStore } from '@/stores/projects.store';
|
||||
import { ProjectTypes, type Project } from '@/types/projects.types';
|
||||
import { useFoldersStore } from '@/stores/folders.store';
|
||||
import type { IUser } from 'n8n-workflow';
|
||||
|
||||
vi.mock('vue-router', async (importOriginal) => ({
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
|
||||
@@ -41,7 +42,7 @@ const TEST_FOLDER_CHILD: FolderShortInfo = {
|
||||
parentFolder: TEST_FOLDER.id,
|
||||
};
|
||||
|
||||
const TEST_ACTIONS: UserAction[] = [
|
||||
const TEST_ACTIONS: Array<UserAction<IUser>> = [
|
||||
{ label: 'Action 1', value: 'action1', disabled: false },
|
||||
{ label: 'Action 2', value: 'action2', disabled: true },
|
||||
];
|
||||
|
||||
@@ -7,11 +7,12 @@ import { type PathItem } from '@n8n/design-system/components/N8nBreadcrumbs/Brea
|
||||
import { computed, onBeforeUnmount, ref, watch } from 'vue';
|
||||
import { useFoldersStore } from '@/stores/folders.store';
|
||||
import type { FolderPathItem, FolderShortInfo } from '@/Interface';
|
||||
import type { IUser } from 'n8n-workflow';
|
||||
|
||||
type Props = {
|
||||
// Current folder can be null when showing breadcrumbs for workflows in project root
|
||||
currentFolder?: FolderShortInfo | null;
|
||||
actions?: UserAction[];
|
||||
actions?: Array<UserAction<IUser>>;
|
||||
hiddenItemsTrigger?: 'hover' | 'click';
|
||||
currentFolderAsLink?: boolean;
|
||||
visibleLevels?: 1 | 2;
|
||||
|
||||
@@ -2,8 +2,8 @@ import { createComponentRenderer } from '@/__tests__/render';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import FolderCard from './FolderCard.vue';
|
||||
import { createPinia, setActivePinia } from 'pinia';
|
||||
import type { FolderResource } from '../layouts/ResourcesListLayout.vue';
|
||||
import type { FolderPathItem, UserAction } from '@/Interface';
|
||||
import type { FolderResource, FolderPathItem, UserAction } from '@/Interface';
|
||||
import type { IUser } from 'n8n-workflow';
|
||||
|
||||
vi.mock('vue-router', () => {
|
||||
const push = vi.fn();
|
||||
@@ -54,7 +54,7 @@ const renderComponent = createComponentRenderer(FolderCard, {
|
||||
actions: [
|
||||
{ label: 'Open', value: 'open', disabled: false },
|
||||
{ label: 'Delete', value: 'delete', disabled: false },
|
||||
] as const satisfies UserAction[],
|
||||
] as const satisfies Array<UserAction<IUser>>,
|
||||
breadcrumbs: DEFAULT_BREADCRUMBS,
|
||||
},
|
||||
global: {
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue';
|
||||
import { FOLDER_LIST_ITEM_ACTIONS } from './constants';
|
||||
import type { FolderResource } from '../layouts/ResourcesListLayout.vue';
|
||||
import { ProjectTypes, type Project } from '@/types/projects.types';
|
||||
import { useI18n } from '@n8n/i18n';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { VIEWS } from '@/constants';
|
||||
import type { UserAction } from '@/Interface';
|
||||
import type { FolderResource, UserAction } from '@/Interface';
|
||||
import { ResourceType } from '@/utils/projects.utils';
|
||||
import type { PathItem } from '@n8n/design-system/components/N8nBreadcrumbs/Breadcrumbs.vue';
|
||||
import { useFoldersStore } from '@/stores/folders.store';
|
||||
import { type IUser } from 'n8n-workflow';
|
||||
|
||||
type Props = {
|
||||
data: FolderResource;
|
||||
personalProject: Project | null;
|
||||
actions: UserAction[];
|
||||
actions: Array<UserAction<IUser>>;
|
||||
readOnly?: boolean;
|
||||
showOwnershipBadge?: boolean;
|
||||
};
|
||||
@@ -36,6 +36,7 @@ const emit = defineEmits<{
|
||||
}>();
|
||||
|
||||
const hiddenBreadcrumbsItemsAsync = ref<Promise<PathItem[]>>(new Promise(() => {}));
|
||||
|
||||
const cachedHiddenBreadcrumbsItems = ref<PathItem[]>([]);
|
||||
|
||||
const resourceTypeLabel = computed(() => i18n.baseText('generic.folder').toLowerCase());
|
||||
|
||||
@@ -118,7 +118,7 @@ const onClaimCreditsClicked = async () => {
|
||||
})
|
||||
}}</n8n-text
|
||||
>
|
||||
<n8n-text size="small" bold="true">
|
||||
<n8n-text size="small" :bold="true">
|
||||
{{ i18n.baseText('freeAi.credits.callout.success.title.part2') }}</n8n-text
|
||||
>
|
||||
</n8n-callout>
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
NodeConnectionTypes,
|
||||
traverseNodeParameters,
|
||||
} from 'n8n-workflow';
|
||||
import type { IFormInput } from '@n8n/design-system';
|
||||
import type { FormFieldValueUpdate, IFormInput } from '@n8n/design-system';
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useTelemetry } from '@/composables/useTelemetry';
|
||||
@@ -249,9 +249,11 @@ const onExecute = async () => {
|
||||
};
|
||||
|
||||
// Add handler for tool selection change
|
||||
const onUpdate = (change: { name: string; value: string }) => {
|
||||
const onUpdate = (change: FormFieldValueUpdate) => {
|
||||
if (change.name !== 'toolName') return;
|
||||
selectedTool.value = change.value;
|
||||
if (typeof change.value === 'string') {
|
||||
selectedTool.value = change.value;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
|
||||
@@ -2,17 +2,18 @@
|
||||
import { nextTick, ref } from 'vue';
|
||||
import { useToast } from '@/composables/useToast';
|
||||
import { onClickOutside } from '@vueuse/core';
|
||||
import type { InputType } from '@n8n/design-system';
|
||||
|
||||
interface Props {
|
||||
modelValue: string;
|
||||
subtitle?: string;
|
||||
type: string;
|
||||
type: InputType;
|
||||
readonly?: boolean;
|
||||
placeholder?: string;
|
||||
maxlength?: number;
|
||||
required?: boolean;
|
||||
autosize?: boolean | { minRows: number; maxRows: number };
|
||||
inputType?: string;
|
||||
inputType?: InputType;
|
||||
maxHeight?: string;
|
||||
}
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
|
||||
@@ -2,7 +2,13 @@
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { useToast } from '@/composables/useToast';
|
||||
import Modal from './Modal.vue';
|
||||
import type { IFormInputs, IInviteResponse, IUser, InvitableRoleName } from '@/Interface';
|
||||
import type {
|
||||
FormFieldValueUpdate,
|
||||
IFormInputs,
|
||||
IInviteResponse,
|
||||
IUser,
|
||||
InvitableRoleName,
|
||||
} from '@/Interface';
|
||||
import { EnterpriseEditionFeature, VALID_EMAIL_REGEX, INVITE_USER_MODAL_KEY } from '@/constants';
|
||||
import { ROLE } from '@n8n/api-types';
|
||||
import { useUsersStore } from '@/stores/users.store';
|
||||
@@ -127,11 +133,15 @@ const validateEmails = (value: string | number | boolean | null | undefined) =>
|
||||
return false;
|
||||
};
|
||||
|
||||
function onInput(e: { name: string; value: InvitableRoleName }) {
|
||||
if (e.name === 'emails') {
|
||||
function isInvitableRoleName(val: unknown): val is InvitableRoleName {
|
||||
return typeof val === 'string' && [ROLE.Member, ROLE.Admin].includes(val as InvitableRoleName);
|
||||
}
|
||||
|
||||
function onInput(e: FormFieldValueUpdate) {
|
||||
if (e.name === 'emails' && typeof e.value === 'string') {
|
||||
emails.value = e.value;
|
||||
}
|
||||
if (e.name === 'role') {
|
||||
if (e.name === 'role' && isInvitableRoleName(e.value)) {
|
||||
role.value = e.value;
|
||||
}
|
||||
}
|
||||
@@ -312,7 +322,7 @@ function getEmail(email: string): string {
|
||||
</n8n-users-list>
|
||||
</div>
|
||||
<n8n-form-inputs
|
||||
v-else
|
||||
v-else-if="config"
|
||||
:inputs="config"
|
||||
:event-bus="formBus"
|
||||
:column-view="true"
|
||||
|
||||
@@ -16,8 +16,8 @@ const emit = defineEmits<{
|
||||
'update:modelValue': [tab: MAIN_HEADER_TABS, event: MouseEvent];
|
||||
}>();
|
||||
|
||||
function onUpdateModelValue(tab: MAIN_HEADER_TABS, event: MouseEvent): void {
|
||||
emit('update:modelValue', tab, event);
|
||||
function onUpdateModelValue(tab: string, event: MouseEvent): void {
|
||||
emit('update:modelValue', tab as MAIN_HEADER_TABS, event);
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -428,7 +428,8 @@ async function handleFileImport(): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
async function onWorkflowMenuSelect(action: WORKFLOW_MENU_ACTIONS): Promise<void> {
|
||||
async function onWorkflowMenuSelect(value: string): Promise<void> {
|
||||
const action = value as WORKFLOW_MENU_ACTIONS;
|
||||
switch (action) {
|
||||
case WORKFLOW_MENU_ACTIONS.DUPLICATE: {
|
||||
uiStore.openModalWithData({
|
||||
|
||||
@@ -25,6 +25,7 @@ import { usePageRedirectionHelper } from '@/composables/usePageRedirectionHelper
|
||||
|
||||
import { useGlobalEntityCreation } from '@/composables/useGlobalEntityCreation';
|
||||
import { N8nNavigationDropdown, N8nTooltip, N8nLink, N8nIconButton } from '@n8n/design-system';
|
||||
import type { IMenuItem } from '@n8n/design-system';
|
||||
import { onClickOutside, type VueInstance } from '@vueuse/core';
|
||||
import Logo from './Logo/Logo.vue';
|
||||
|
||||
@@ -67,7 +68,7 @@ const userMenuItems = ref([
|
||||
},
|
||||
]);
|
||||
|
||||
const mainMenuItems = computed(() => [
|
||||
const mainMenuItems = computed<IMenuItem[]>(() => [
|
||||
{
|
||||
id: 'cloud-admin',
|
||||
position: 'bottom',
|
||||
|
||||
@@ -8,7 +8,7 @@ import { LOCAL_STORAGE_MAIN_PANEL_RELATIVE_WIDTH, MAIN_NODE_PANEL_WIDTH } from '
|
||||
import { useNDVStore } from '@/stores/ndv.store';
|
||||
import { ndvEventBus } from '@/event-bus';
|
||||
import NDVFloatingNodes from '@/components/NDVFloatingNodes.vue';
|
||||
import type { MainPanelType, XYPosition } from '@/Interface';
|
||||
import type { Direction, MainPanelType, XYPosition } from '@/Interface';
|
||||
import { ref, onMounted, onBeforeUnmount, computed, watch, nextTick } from 'vue';
|
||||
import { useUIStore } from '@/stores/ui.store';
|
||||
import { useThrottleFn } from '@vueuse/core';
|
||||
@@ -151,8 +151,8 @@ const outputPanelRelativeTranslate = computed((): number => {
|
||||
return currentRelativeLeftDelta > 0 ? currentRelativeLeftDelta : 0;
|
||||
});
|
||||
|
||||
const supportedResizeDirections = computed((): string[] => {
|
||||
const supportedDirections = ['right'];
|
||||
const supportedResizeDirections = computed((): Direction[] => {
|
||||
const supportedDirections = ['right' as Direction];
|
||||
|
||||
if (props.isDraggable) supportedDirections.push('left');
|
||||
return supportedDirections;
|
||||
|
||||
@@ -99,10 +99,11 @@ describe('NDVSubConnections', () => {
|
||||
<div class="connectionType"><span class="connectionLabel">Tools</span>
|
||||
<div>
|
||||
<div class="connectedNodesWrapper" style="--nodes-length: 0;">
|
||||
<div class="plusButton">
|
||||
<n8n-tooltip placement="top" teleported="true" offset="10" show-after="300" disabled="false">
|
||||
<n8n-icon-button size="medium" icon="plus" type="tertiary" data-test-id="add-subnode-ai_tool-0"></n8n-icon-button>
|
||||
</n8n-tooltip>
|
||||
<div class="plusButton"><button class="button button tertiary medium withIcon square el-tooltip__trigger el-tooltip__trigger" aria-live="polite" data-test-id="add-subnode-ai_tool-0"><span class="icon"><span class="n8n-text compact size-medium regular n8n-icon n8n-icon"><!----></span></span>
|
||||
<!--v-if-->
|
||||
</button>
|
||||
<!--teleport start-->
|
||||
<!--teleport end-->
|
||||
</div>
|
||||
<!--v-if-->
|
||||
</div>
|
||||
|
||||
@@ -26,10 +26,11 @@ import { useRunWorkflow } from '@/composables/useRunWorkflow';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useI18n } from '@n8n/i18n';
|
||||
import { useTelemetry } from '@/composables/useTelemetry';
|
||||
import { type IUpdateInformation } from '@/Interface';
|
||||
import type { ButtonSize, IUpdateInformation } from '@/Interface';
|
||||
import { generateCodeForAiTransform } from '@/components/ButtonParameter/utils';
|
||||
import { needsAgentInput } from '@/utils/nodes/nodeTransforms';
|
||||
import { useUIStore } from '@/stores/ui.store';
|
||||
import type { ButtonType } from '@n8n/design-system';
|
||||
|
||||
const NODE_TEST_STEP_POPUP_COUNT_KEY = 'N8N_NODE_TEST_STEP_POPUP_COUNT';
|
||||
const MAX_POPUP_COUNT = 10;
|
||||
@@ -41,8 +42,8 @@ const props = withDefaults(
|
||||
telemetrySource: string;
|
||||
disabled?: boolean;
|
||||
label?: string;
|
||||
type?: string;
|
||||
size?: string;
|
||||
type?: ButtonType;
|
||||
size?: ButtonSize;
|
||||
transparent?: boolean;
|
||||
hideIcon?: boolean;
|
||||
tooltip?: string;
|
||||
|
||||
@@ -119,7 +119,7 @@ const options = computed<ITab[]>(() => {
|
||||
return options;
|
||||
});
|
||||
|
||||
function onTabSelect(tab: string) {
|
||||
function onTabSelect(tab: string | number) {
|
||||
if (tab === 'docs' && props.nodeType) {
|
||||
void externalHooks.run('dataDisplay.onDocumentationUrlClick', {
|
||||
nodeType: props.nodeType,
|
||||
@@ -147,7 +147,7 @@ function onTabSelect(tab: string) {
|
||||
}
|
||||
}
|
||||
|
||||
function onTooltipClick(tab: string, event: MouseEvent) {
|
||||
function onTooltipClick(tab: string | number, event: MouseEvent) {
|
||||
if (tab === 'communityNode' && (event.target as Element).localName === 'a') {
|
||||
telemetry.track('user clicked cnr docs link', { source: 'node details view' });
|
||||
}
|
||||
|
||||
@@ -26,9 +26,9 @@ const emit = defineEmits<{
|
||||
v-if="!isReadOnly"
|
||||
type="tertiary"
|
||||
:class="['n8n-input', $style.overrideCloseButton]"
|
||||
outline="false"
|
||||
:outline="false"
|
||||
icon="xmark"
|
||||
size="xsmall"
|
||||
size="mini"
|
||||
@click="emit('close')"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -114,438 +114,441 @@ const isSaving = ref(false);
|
||||
const userPermissions = computed(() =>
|
||||
getResourcePermissions(usersStore.currentUser?.globalScopes),
|
||||
);
|
||||
const survey = computed<IFormInputs>(() => [
|
||||
{
|
||||
name: COMPANY_TYPE_KEY,
|
||||
properties: {
|
||||
label: i18n.baseText('personalizationModal.whatBestDescribesYourCompany'),
|
||||
type: 'select',
|
||||
placeholder: i18n.baseText('personalizationModal.select'),
|
||||
options: [
|
||||
{
|
||||
label: i18n.baseText('personalizationModal.saas'),
|
||||
value: SAAS_COMPANY_TYPE,
|
||||
},
|
||||
{
|
||||
label: i18n.baseText('personalizationModal.eCommerce'),
|
||||
value: ECOMMERCE_COMPANY_TYPE,
|
||||
},
|
||||
const survey = computed<IFormInputs>(
|
||||
() =>
|
||||
[
|
||||
{
|
||||
name: COMPANY_TYPE_KEY,
|
||||
properties: {
|
||||
label: i18n.baseText('personalizationModal.whatBestDescribesYourCompany'),
|
||||
type: 'select',
|
||||
placeholder: i18n.baseText('personalizationModal.select'),
|
||||
options: [
|
||||
{
|
||||
label: i18n.baseText('personalizationModal.saas'),
|
||||
value: SAAS_COMPANY_TYPE,
|
||||
},
|
||||
{
|
||||
label: i18n.baseText('personalizationModal.eCommerce'),
|
||||
value: ECOMMERCE_COMPANY_TYPE,
|
||||
},
|
||||
|
||||
{
|
||||
label: i18n.baseText('personalizationModal.digitalAgencyOrConsultant'),
|
||||
value: DIGITAL_AGENCY_COMPANY_TYPE,
|
||||
{
|
||||
label: i18n.baseText('personalizationModal.digitalAgencyOrConsultant'),
|
||||
value: DIGITAL_AGENCY_COMPANY_TYPE,
|
||||
},
|
||||
{
|
||||
label: i18n.baseText('personalizationModal.systemsIntegrator'),
|
||||
value: SYSTEMS_INTEGRATOR_COMPANY_TYPE,
|
||||
},
|
||||
{
|
||||
value: EDUCATION_TYPE,
|
||||
label: i18n.baseText('personalizationModal.education'),
|
||||
},
|
||||
{
|
||||
label: i18n.baseText('personalizationModal.other'),
|
||||
value: OTHER_COMPANY_TYPE,
|
||||
},
|
||||
{
|
||||
label: i18n.baseText('personalizationModal.imNotUsingN8nForWork'),
|
||||
value: PERSONAL_COMPANY_TYPE,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: i18n.baseText('personalizationModal.systemsIntegrator'),
|
||||
value: SYSTEMS_INTEGRATOR_COMPANY_TYPE,
|
||||
},
|
||||
{
|
||||
name: COMPANY_INDUSTRY_EXTENDED_KEY,
|
||||
properties: {
|
||||
type: 'multi-select',
|
||||
label: i18n.baseText('personalizationModal.whichIndustriesIsYourCompanyIn'),
|
||||
placeholder: i18n.baseText('personalizationModal.select'),
|
||||
options: [
|
||||
{
|
||||
value: FINANCE_INSURANCE_INDUSTRY,
|
||||
label: i18n.baseText('personalizationModal.financeOrInsurance'),
|
||||
},
|
||||
{
|
||||
value: GOVERNMENT_INDUSTRY,
|
||||
label: i18n.baseText('personalizationModal.government'),
|
||||
},
|
||||
{
|
||||
value: HEALTHCARE_INDUSTRY,
|
||||
label: i18n.baseText('personalizationModal.healthcare'),
|
||||
},
|
||||
{
|
||||
value: IT_INDUSTRY,
|
||||
label: i18n.baseText('personalizationModal.it'),
|
||||
},
|
||||
{
|
||||
value: LEGAL_INDUSTRY,
|
||||
label: i18n.baseText('personalizationModal.legal'),
|
||||
},
|
||||
{
|
||||
value: MSP_INDUSTRY,
|
||||
label: i18n.baseText('personalizationModal.managedServiceProvider'),
|
||||
},
|
||||
{
|
||||
value: MARKETING_INDUSTRY,
|
||||
label: i18n.baseText('personalizationModal.marketing'),
|
||||
},
|
||||
{
|
||||
value: MEDIA_INDUSTRY,
|
||||
label: i18n.baseText('personalizationModal.media'),
|
||||
},
|
||||
{
|
||||
value: MANUFACTURING_INDUSTRY,
|
||||
label: i18n.baseText('personalizationModal.manufacturing'),
|
||||
},
|
||||
{
|
||||
value: PHYSICAL_RETAIL_OR_SERVICES,
|
||||
label: i18n.baseText('personalizationModal.physicalRetailOrServices'),
|
||||
},
|
||||
{
|
||||
value: REAL_ESTATE_OR_CONSTRUCTION,
|
||||
label: i18n.baseText('personalizationModal.realEstateOrConstruction'),
|
||||
},
|
||||
{
|
||||
value: SECURITY_INDUSTRY,
|
||||
label: i18n.baseText('personalizationModal.security'),
|
||||
},
|
||||
{
|
||||
value: TELECOMS_INDUSTRY,
|
||||
label: i18n.baseText('personalizationModal.telecoms'),
|
||||
},
|
||||
{
|
||||
value: OTHER_INDUSTRY_OPTION,
|
||||
label: i18n.baseText('personalizationModal.otherPleaseSpecify'),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
value: EDUCATION_TYPE,
|
||||
label: i18n.baseText('personalizationModal.education'),
|
||||
shouldDisplay(values): boolean {
|
||||
const companyType = (values as IPersonalizationLatestVersion)[COMPANY_TYPE_KEY];
|
||||
return companyType === OTHER_COMPANY_TYPE;
|
||||
},
|
||||
{
|
||||
label: i18n.baseText('personalizationModal.other'),
|
||||
value: OTHER_COMPANY_TYPE,
|
||||
},
|
||||
{
|
||||
name: OTHER_COMPANY_INDUSTRY_EXTENDED_KEY,
|
||||
properties: {
|
||||
placeholder: i18n.baseText('personalizationModal.specifyYourCompanysIndustry'),
|
||||
},
|
||||
{
|
||||
label: i18n.baseText('personalizationModal.imNotUsingN8nForWork'),
|
||||
value: PERSONAL_COMPANY_TYPE,
|
||||
shouldDisplay(values): boolean {
|
||||
const companyType = (values as IPersonalizationLatestVersion)[COMPANY_TYPE_KEY];
|
||||
const companyIndustry = (values as IPersonalizationLatestVersion)[
|
||||
COMPANY_INDUSTRY_EXTENDED_KEY
|
||||
];
|
||||
return (
|
||||
companyType === OTHER_COMPANY_TYPE &&
|
||||
!!companyIndustry &&
|
||||
companyIndustry.includes(OTHER_INDUSTRY_OPTION)
|
||||
);
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: COMPANY_INDUSTRY_EXTENDED_KEY,
|
||||
properties: {
|
||||
type: 'multi-select',
|
||||
label: i18n.baseText('personalizationModal.whichIndustriesIsYourCompanyIn'),
|
||||
placeholder: i18n.baseText('personalizationModal.select'),
|
||||
options: [
|
||||
{
|
||||
value: FINANCE_INSURANCE_INDUSTRY,
|
||||
label: i18n.baseText('personalizationModal.financeOrInsurance'),
|
||||
},
|
||||
{
|
||||
name: ROLE_KEY,
|
||||
properties: {
|
||||
type: 'select',
|
||||
label: i18n.baseText('personalizationModal.whichRoleBestDescribesYou'),
|
||||
placeholder: i18n.baseText('personalizationModal.select'),
|
||||
options: [
|
||||
{
|
||||
value: ROLE_BUSINESS_OWNER,
|
||||
label: i18n.baseText('personalizationModal.businessOwner'),
|
||||
},
|
||||
{
|
||||
value: ROLE_CUSTOMER_SUPPORT,
|
||||
label: i18n.baseText('personalizationModal.customerSupport'),
|
||||
},
|
||||
{
|
||||
value: ROLE_DATA_SCIENCE,
|
||||
label: i18n.baseText('personalizationModal.dataScience'),
|
||||
},
|
||||
{
|
||||
value: ROLE_DEVOPS,
|
||||
label: i18n.baseText('personalizationModal.devops'),
|
||||
},
|
||||
{
|
||||
value: ROLE_IT,
|
||||
label: i18n.baseText('personalizationModal.it'),
|
||||
},
|
||||
{
|
||||
value: ROLE_ENGINEERING,
|
||||
label: i18n.baseText('personalizationModal.engineering'),
|
||||
},
|
||||
{
|
||||
value: ROLE_SALES_AND_MARKETING,
|
||||
label: i18n.baseText('personalizationModal.salesAndMarketing'),
|
||||
},
|
||||
{
|
||||
value: ROLE_SECURITY,
|
||||
label: i18n.baseText('personalizationModal.security'),
|
||||
},
|
||||
{
|
||||
value: ROLE_OTHER,
|
||||
label: i18n.baseText('personalizationModal.otherPleaseSpecify'),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
value: GOVERNMENT_INDUSTRY,
|
||||
label: i18n.baseText('personalizationModal.government'),
|
||||
shouldDisplay(values): boolean {
|
||||
const companyType = (values as IPersonalizationLatestVersion)[COMPANY_TYPE_KEY];
|
||||
return companyType !== PERSONAL_COMPANY_TYPE;
|
||||
},
|
||||
{
|
||||
value: HEALTHCARE_INDUSTRY,
|
||||
label: i18n.baseText('personalizationModal.healthcare'),
|
||||
},
|
||||
{
|
||||
name: ROLE_OTHER_KEY,
|
||||
properties: {
|
||||
placeholder: i18n.baseText('personalizationModal.specifyYourRole'),
|
||||
},
|
||||
{
|
||||
value: IT_INDUSTRY,
|
||||
label: i18n.baseText('personalizationModal.it'),
|
||||
shouldDisplay(values): boolean {
|
||||
const companyType = (values as IPersonalizationLatestVersion)[COMPANY_TYPE_KEY];
|
||||
const role = (values as IPersonalizationLatestVersion)[ROLE_KEY];
|
||||
return companyType !== PERSONAL_COMPANY_TYPE && role === ROLE_OTHER;
|
||||
},
|
||||
{
|
||||
value: LEGAL_INDUSTRY,
|
||||
label: i18n.baseText('personalizationModal.legal'),
|
||||
},
|
||||
{
|
||||
name: DEVOPS_AUTOMATION_GOAL_KEY,
|
||||
properties: {
|
||||
type: 'multi-select',
|
||||
label: i18n.baseText('personalizationModal.whatAreYouLookingToAutomate'),
|
||||
placeholder: i18n.baseText('personalizationModal.select'),
|
||||
options: [
|
||||
{
|
||||
value: DEVOPS_AUTOMATION_CI_CD_GOAL,
|
||||
label: i18n.baseText('personalizationModal.cicd'),
|
||||
},
|
||||
{
|
||||
value: DEVOPS_AUTOMATION_CLOUD_INFRASTRUCTURE_ORCHESTRATION_GOAL,
|
||||
label: i18n.baseText('personalizationModal.cloudInfrastructureOrchestration'),
|
||||
},
|
||||
{
|
||||
value: DEVOPS_AUTOMATION_DATA_SYNCING_GOAL,
|
||||
label: i18n.baseText('personalizationModal.dataSynching'),
|
||||
},
|
||||
{
|
||||
value: DEVOPS_INCIDENT_RESPONSE_GOAL,
|
||||
label: i18n.baseText('personalizationModal.incidentResponse'),
|
||||
},
|
||||
{
|
||||
value: DEVOPS_MONITORING_AND_ALERTING_GOAL,
|
||||
label: i18n.baseText('personalizationModal.monitoringAndAlerting'),
|
||||
},
|
||||
{
|
||||
value: DEVOPS_REPORTING_GOAL,
|
||||
label: i18n.baseText('personalizationModal.reporting'),
|
||||
},
|
||||
{
|
||||
value: DEVOPS_TICKETING_SYSTEMS_INTEGRATIONS_GOAL,
|
||||
label: i18n.baseText('personalizationModal.ticketingSystemsIntegrations'),
|
||||
},
|
||||
{
|
||||
value: OTHER_AUTOMATION_GOAL,
|
||||
label: i18n.baseText('personalizationModal.other'),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
value: MSP_INDUSTRY,
|
||||
label: i18n.baseText('personalizationModal.managedServiceProvider'),
|
||||
shouldDisplay(values): boolean {
|
||||
const companyType = (values as IPersonalizationLatestVersion)[COMPANY_TYPE_KEY];
|
||||
const role = (values as IPersonalizationLatestVersion)[ROLE_KEY] as string;
|
||||
return (
|
||||
companyType !== PERSONAL_COMPANY_TYPE &&
|
||||
[ROLE_DEVOPS, ROLE_ENGINEERING, ROLE_IT].includes(role)
|
||||
);
|
||||
},
|
||||
{
|
||||
value: MARKETING_INDUSTRY,
|
||||
label: i18n.baseText('personalizationModal.marketing'),
|
||||
},
|
||||
{
|
||||
name: DEVOPS_AUTOMATION_GOAL_OTHER_KEY,
|
||||
properties: {
|
||||
placeholder: i18n.baseText('personalizationModal.specifyYourAutomationGoal'),
|
||||
},
|
||||
{
|
||||
value: MEDIA_INDUSTRY,
|
||||
label: i18n.baseText('personalizationModal.media'),
|
||||
shouldDisplay(values): boolean {
|
||||
const companyType = (values as IPersonalizationLatestVersion)[COMPANY_TYPE_KEY];
|
||||
const goals = (values as IPersonalizationLatestVersion)[DEVOPS_AUTOMATION_GOAL_KEY];
|
||||
const role = (values as IPersonalizationLatestVersion)[ROLE_KEY] as string;
|
||||
return (
|
||||
companyType !== PERSONAL_COMPANY_TYPE &&
|
||||
[ROLE_DEVOPS, ROLE_ENGINEERING, ROLE_IT].includes(role) &&
|
||||
!!goals &&
|
||||
goals.includes(DEVOPS_AUTOMATION_OTHER)
|
||||
);
|
||||
},
|
||||
{
|
||||
value: MANUFACTURING_INDUSTRY,
|
||||
label: i18n.baseText('personalizationModal.manufacturing'),
|
||||
},
|
||||
{
|
||||
name: MARKETING_AUTOMATION_GOAL_KEY,
|
||||
properties: {
|
||||
type: 'multi-select',
|
||||
label: i18n.baseText('personalizationModal.specifySalesMarketingGoal'),
|
||||
placeholder: i18n.baseText('personalizationModal.select'),
|
||||
options: [
|
||||
{
|
||||
label: i18n.baseText('personalizationModal.leadGeneration'),
|
||||
value: MARKETING_AUTOMATION_LEAD_GENERATION_GOAL,
|
||||
},
|
||||
{
|
||||
label: i18n.baseText('personalizationModal.customerCommunication'),
|
||||
value: MARKETING_AUTOMATION_CUSTOMER_COMMUNICATION,
|
||||
},
|
||||
{
|
||||
label: i18n.baseText('personalizationModal.customerActions'),
|
||||
value: MARKETING_AUTOMATION_ACTIONS,
|
||||
},
|
||||
{
|
||||
label: i18n.baseText('personalizationModal.adCampaign'),
|
||||
value: MARKETING_AUTOMATION_AD_CAMPAIGN,
|
||||
},
|
||||
{
|
||||
label: i18n.baseText('personalizationModal.reporting'),
|
||||
value: MARKETING_AUTOMATION_REPORTING,
|
||||
},
|
||||
{
|
||||
label: i18n.baseText('personalizationModal.dataSynching'),
|
||||
value: MARKETING_AUTOMATION_DATA_SYNCHING,
|
||||
},
|
||||
{
|
||||
label: i18n.baseText('personalizationModal.other'),
|
||||
value: MARKETING_AUTOMATION_OTHER,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
value: PHYSICAL_RETAIL_OR_SERVICES,
|
||||
label: i18n.baseText('personalizationModal.physicalRetailOrServices'),
|
||||
shouldDisplay(values): boolean {
|
||||
const companyType = (values as IPersonalizationLatestVersion)[COMPANY_TYPE_KEY];
|
||||
const role = (values as IPersonalizationLatestVersion)[ROLE_KEY];
|
||||
return companyType !== PERSONAL_COMPANY_TYPE && role === ROLE_SALES_AND_MARKETING;
|
||||
},
|
||||
{
|
||||
value: REAL_ESTATE_OR_CONSTRUCTION,
|
||||
label: i18n.baseText('personalizationModal.realEstateOrConstruction'),
|
||||
},
|
||||
{
|
||||
name: OTHER_MARKETING_AUTOMATION_GOAL_KEY,
|
||||
properties: {
|
||||
placeholder: i18n.baseText('personalizationModal.specifyOtherSalesAndMarketingGoal'),
|
||||
},
|
||||
{
|
||||
value: SECURITY_INDUSTRY,
|
||||
label: i18n.baseText('personalizationModal.security'),
|
||||
shouldDisplay(values): boolean {
|
||||
const companyType = (values as IPersonalizationLatestVersion)[COMPANY_TYPE_KEY];
|
||||
const goals = (values as IPersonalizationLatestVersion)[MARKETING_AUTOMATION_GOAL_KEY];
|
||||
const role = (values as IPersonalizationLatestVersion)[ROLE_KEY];
|
||||
return (
|
||||
companyType !== PERSONAL_COMPANY_TYPE &&
|
||||
role === ROLE_SALES_AND_MARKETING &&
|
||||
!!goals &&
|
||||
goals.includes(MARKETING_AUTOMATION_OTHER)
|
||||
);
|
||||
},
|
||||
{
|
||||
value: TELECOMS_INDUSTRY,
|
||||
label: i18n.baseText('personalizationModal.telecoms'),
|
||||
},
|
||||
{
|
||||
name: AUTOMATION_BENEFICIARY_KEY,
|
||||
properties: {
|
||||
type: 'select',
|
||||
label: i18n.baseText('personalizationModal.specifyAutomationBeneficiary'),
|
||||
placeholder: i18n.baseText('personalizationModal.select'),
|
||||
options: [
|
||||
{
|
||||
label: i18n.baseText('personalizationModal.myself'),
|
||||
value: AUTOMATION_BENEFICIARY_SELF,
|
||||
},
|
||||
{
|
||||
label: i18n.baseText('personalizationModal.myTeam'),
|
||||
value: AUTOMATION_BENEFICIARY_MY_TEAM,
|
||||
},
|
||||
{
|
||||
label: i18n.baseText('personalizationModal.otherTeams'),
|
||||
value: AUTOMATION_BENEFICIARY_OTHER_TEAMS,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
value: OTHER_INDUSTRY_OPTION,
|
||||
label: i18n.baseText('personalizationModal.otherPleaseSpecify'),
|
||||
shouldDisplay(values): boolean {
|
||||
const companyType = (values as IPersonalizationLatestVersion)[COMPANY_TYPE_KEY];
|
||||
return companyType !== PERSONAL_COMPANY_TYPE;
|
||||
},
|
||||
],
|
||||
},
|
||||
shouldDisplay(values): boolean {
|
||||
const companyType = (values as IPersonalizationLatestVersion)[COMPANY_TYPE_KEY];
|
||||
return companyType === OTHER_COMPANY_TYPE;
|
||||
},
|
||||
},
|
||||
{
|
||||
name: OTHER_COMPANY_INDUSTRY_EXTENDED_KEY,
|
||||
properties: {
|
||||
placeholder: i18n.baseText('personalizationModal.specifyYourCompanysIndustry'),
|
||||
},
|
||||
shouldDisplay(values): boolean {
|
||||
const companyType = (values as IPersonalizationLatestVersion)[COMPANY_TYPE_KEY];
|
||||
const companyIndustry = (values as IPersonalizationLatestVersion)[
|
||||
COMPANY_INDUSTRY_EXTENDED_KEY
|
||||
];
|
||||
return (
|
||||
companyType === OTHER_COMPANY_TYPE &&
|
||||
!!companyIndustry &&
|
||||
companyIndustry.includes(OTHER_INDUSTRY_OPTION)
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: ROLE_KEY,
|
||||
properties: {
|
||||
type: 'select',
|
||||
label: i18n.baseText('personalizationModal.whichRoleBestDescribesYou'),
|
||||
placeholder: i18n.baseText('personalizationModal.select'),
|
||||
options: [
|
||||
{
|
||||
value: ROLE_BUSINESS_OWNER,
|
||||
label: i18n.baseText('personalizationModal.businessOwner'),
|
||||
},
|
||||
{
|
||||
name: COMPANY_SIZE_KEY,
|
||||
properties: {
|
||||
type: 'select',
|
||||
label: i18n.baseText('personalizationModal.howBigIsYourCompany'),
|
||||
placeholder: i18n.baseText('personalizationModal.select'),
|
||||
options: [
|
||||
{
|
||||
label: i18n.baseText('personalizationModal.lessThan20People'),
|
||||
value: COMPANY_SIZE_20_OR_LESS,
|
||||
},
|
||||
{
|
||||
label: `20-99 ${i18n.baseText('personalizationModal.people')}`,
|
||||
value: COMPANY_SIZE_20_99,
|
||||
},
|
||||
{
|
||||
label: `100-499 ${i18n.baseText('personalizationModal.people')}`,
|
||||
value: COMPANY_SIZE_100_499,
|
||||
},
|
||||
{
|
||||
label: `500-999 ${i18n.baseText('personalizationModal.people')}`,
|
||||
value: COMPANY_SIZE_500_999,
|
||||
},
|
||||
{
|
||||
label: `1000+ ${i18n.baseText('personalizationModal.people')}`,
|
||||
value: COMPANY_SIZE_1000_OR_MORE,
|
||||
},
|
||||
{
|
||||
label: i18n.baseText('personalizationModal.imNotUsingN8nForWork'),
|
||||
value: COMPANY_SIZE_PERSONAL_USE,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
value: ROLE_CUSTOMER_SUPPORT,
|
||||
label: i18n.baseText('personalizationModal.customerSupport'),
|
||||
shouldDisplay(values): boolean {
|
||||
const companyType = (values as IPersonalizationLatestVersion)[COMPANY_TYPE_KEY];
|
||||
return companyType !== PERSONAL_COMPANY_TYPE;
|
||||
},
|
||||
{
|
||||
value: ROLE_DATA_SCIENCE,
|
||||
label: i18n.baseText('personalizationModal.dataScience'),
|
||||
},
|
||||
{
|
||||
name: REPORTED_SOURCE_KEY,
|
||||
properties: {
|
||||
type: 'select',
|
||||
label: i18n.baseText('personalizationModal.howDidYouHearAboutN8n'),
|
||||
placeholder: i18n.baseText('personalizationModal.select'),
|
||||
options: [
|
||||
{
|
||||
label: 'Google',
|
||||
value: REPORTED_SOURCE_GOOGLE,
|
||||
},
|
||||
{
|
||||
label: 'Twitter',
|
||||
value: REPORTED_SOURCE_TWITTER,
|
||||
},
|
||||
{
|
||||
label: 'LinkedIn',
|
||||
value: REPORTED_SOURCE_LINKEDIN,
|
||||
},
|
||||
{
|
||||
label: 'YouTube',
|
||||
value: REPORTED_SOURCE_YOUTUBE,
|
||||
},
|
||||
{
|
||||
label: i18n.baseText('personalizationModal.friendWordOfMouth'),
|
||||
value: REPORTED_SOURCE_FRIEND,
|
||||
},
|
||||
{
|
||||
label: i18n.baseText('personalizationModal.podcast'),
|
||||
value: REPORTED_SOURCE_PODCAST,
|
||||
},
|
||||
{
|
||||
label: i18n.baseText('personalizationModal.event'),
|
||||
value: REPORTED_SOURCE_EVENT,
|
||||
},
|
||||
{
|
||||
label: i18n.baseText('personalizationModal.otherPleaseSpecify'),
|
||||
value: REPORTED_SOURCE_OTHER,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
value: ROLE_DEVOPS,
|
||||
label: i18n.baseText('personalizationModal.devops'),
|
||||
},
|
||||
{
|
||||
name: REPORTED_SOURCE_OTHER_KEY,
|
||||
properties: {
|
||||
placeholder: i18n.baseText('personalizationModal.specifyReportedSource'),
|
||||
},
|
||||
{
|
||||
value: ROLE_IT,
|
||||
label: i18n.baseText('personalizationModal.it'),
|
||||
shouldDisplay(values): boolean {
|
||||
const reportedSource = (values as IPersonalizationLatestVersion)[REPORTED_SOURCE_KEY];
|
||||
return reportedSource === REPORTED_SOURCE_OTHER;
|
||||
},
|
||||
{
|
||||
value: ROLE_ENGINEERING,
|
||||
label: i18n.baseText('personalizationModal.engineering'),
|
||||
},
|
||||
{
|
||||
value: ROLE_SALES_AND_MARKETING,
|
||||
label: i18n.baseText('personalizationModal.salesAndMarketing'),
|
||||
},
|
||||
{
|
||||
value: ROLE_SECURITY,
|
||||
label: i18n.baseText('personalizationModal.security'),
|
||||
},
|
||||
{
|
||||
value: ROLE_OTHER,
|
||||
label: i18n.baseText('personalizationModal.otherPleaseSpecify'),
|
||||
},
|
||||
],
|
||||
},
|
||||
shouldDisplay(values): boolean {
|
||||
const companyType = (values as IPersonalizationLatestVersion)[COMPANY_TYPE_KEY];
|
||||
return companyType !== PERSONAL_COMPANY_TYPE;
|
||||
},
|
||||
},
|
||||
{
|
||||
name: ROLE_OTHER_KEY,
|
||||
properties: {
|
||||
placeholder: i18n.baseText('personalizationModal.specifyYourRole'),
|
||||
},
|
||||
shouldDisplay(values): boolean {
|
||||
const companyType = (values as IPersonalizationLatestVersion)[COMPANY_TYPE_KEY];
|
||||
const role = (values as IPersonalizationLatestVersion)[ROLE_KEY];
|
||||
return companyType !== PERSONAL_COMPANY_TYPE && role === ROLE_OTHER;
|
||||
},
|
||||
},
|
||||
{
|
||||
name: DEVOPS_AUTOMATION_GOAL_KEY,
|
||||
properties: {
|
||||
type: 'multi-select',
|
||||
label: i18n.baseText('personalizationModal.whatAreYouLookingToAutomate'),
|
||||
placeholder: i18n.baseText('personalizationModal.select'),
|
||||
options: [
|
||||
{
|
||||
value: DEVOPS_AUTOMATION_CI_CD_GOAL,
|
||||
label: i18n.baseText('personalizationModal.cicd'),
|
||||
},
|
||||
{
|
||||
value: DEVOPS_AUTOMATION_CLOUD_INFRASTRUCTURE_ORCHESTRATION_GOAL,
|
||||
label: i18n.baseText('personalizationModal.cloudInfrastructureOrchestration'),
|
||||
},
|
||||
{
|
||||
value: DEVOPS_AUTOMATION_DATA_SYNCING_GOAL,
|
||||
label: i18n.baseText('personalizationModal.dataSynching'),
|
||||
},
|
||||
{
|
||||
value: DEVOPS_INCIDENT_RESPONSE_GOAL,
|
||||
label: i18n.baseText('personalizationModal.incidentResponse'),
|
||||
},
|
||||
{
|
||||
value: DEVOPS_MONITORING_AND_ALERTING_GOAL,
|
||||
label: i18n.baseText('personalizationModal.monitoringAndAlerting'),
|
||||
},
|
||||
{
|
||||
value: DEVOPS_REPORTING_GOAL,
|
||||
label: i18n.baseText('personalizationModal.reporting'),
|
||||
},
|
||||
{
|
||||
value: DEVOPS_TICKETING_SYSTEMS_INTEGRATIONS_GOAL,
|
||||
label: i18n.baseText('personalizationModal.ticketingSystemsIntegrations'),
|
||||
},
|
||||
{
|
||||
value: OTHER_AUTOMATION_GOAL,
|
||||
label: i18n.baseText('personalizationModal.other'),
|
||||
},
|
||||
],
|
||||
},
|
||||
shouldDisplay(values): boolean {
|
||||
const companyType = (values as IPersonalizationLatestVersion)[COMPANY_TYPE_KEY];
|
||||
const role = (values as IPersonalizationLatestVersion)[ROLE_KEY] as string;
|
||||
return (
|
||||
companyType !== PERSONAL_COMPANY_TYPE &&
|
||||
[ROLE_DEVOPS, ROLE_ENGINEERING, ROLE_IT].includes(role)
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: DEVOPS_AUTOMATION_GOAL_OTHER_KEY,
|
||||
properties: {
|
||||
placeholder: i18n.baseText('personalizationModal.specifyYourAutomationGoal'),
|
||||
},
|
||||
shouldDisplay(values): boolean {
|
||||
const companyType = (values as IPersonalizationLatestVersion)[COMPANY_TYPE_KEY];
|
||||
const goals = (values as IPersonalizationLatestVersion)[DEVOPS_AUTOMATION_GOAL_KEY];
|
||||
const role = (values as IPersonalizationLatestVersion)[ROLE_KEY] as string;
|
||||
return (
|
||||
companyType !== PERSONAL_COMPANY_TYPE &&
|
||||
[ROLE_DEVOPS, ROLE_ENGINEERING, ROLE_IT].includes(role) &&
|
||||
!!goals &&
|
||||
goals.includes(DEVOPS_AUTOMATION_OTHER)
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: MARKETING_AUTOMATION_GOAL_KEY,
|
||||
properties: {
|
||||
type: 'multi-select',
|
||||
label: i18n.baseText('personalizationModal.specifySalesMarketingGoal'),
|
||||
placeholder: i18n.baseText('personalizationModal.select'),
|
||||
options: [
|
||||
{
|
||||
label: i18n.baseText('personalizationModal.leadGeneration'),
|
||||
value: MARKETING_AUTOMATION_LEAD_GENERATION_GOAL,
|
||||
},
|
||||
{
|
||||
label: i18n.baseText('personalizationModal.customerCommunication'),
|
||||
value: MARKETING_AUTOMATION_CUSTOMER_COMMUNICATION,
|
||||
},
|
||||
{
|
||||
label: i18n.baseText('personalizationModal.customerActions'),
|
||||
value: MARKETING_AUTOMATION_ACTIONS,
|
||||
},
|
||||
{
|
||||
label: i18n.baseText('personalizationModal.adCampaign'),
|
||||
value: MARKETING_AUTOMATION_AD_CAMPAIGN,
|
||||
},
|
||||
{
|
||||
label: i18n.baseText('personalizationModal.reporting'),
|
||||
value: MARKETING_AUTOMATION_REPORTING,
|
||||
},
|
||||
{
|
||||
label: i18n.baseText('personalizationModal.dataSynching'),
|
||||
value: MARKETING_AUTOMATION_DATA_SYNCHING,
|
||||
},
|
||||
{
|
||||
label: i18n.baseText('personalizationModal.other'),
|
||||
value: MARKETING_AUTOMATION_OTHER,
|
||||
},
|
||||
],
|
||||
},
|
||||
shouldDisplay(values): boolean {
|
||||
const companyType = (values as IPersonalizationLatestVersion)[COMPANY_TYPE_KEY];
|
||||
const role = (values as IPersonalizationLatestVersion)[ROLE_KEY];
|
||||
return companyType !== PERSONAL_COMPANY_TYPE && role === ROLE_SALES_AND_MARKETING;
|
||||
},
|
||||
},
|
||||
{
|
||||
name: OTHER_MARKETING_AUTOMATION_GOAL_KEY,
|
||||
properties: {
|
||||
placeholder: i18n.baseText('personalizationModal.specifyOtherSalesAndMarketingGoal'),
|
||||
},
|
||||
shouldDisplay(values): boolean {
|
||||
const companyType = (values as IPersonalizationLatestVersion)[COMPANY_TYPE_KEY];
|
||||
const goals = (values as IPersonalizationLatestVersion)[MARKETING_AUTOMATION_GOAL_KEY];
|
||||
const role = (values as IPersonalizationLatestVersion)[ROLE_KEY];
|
||||
return (
|
||||
companyType !== PERSONAL_COMPANY_TYPE &&
|
||||
role === ROLE_SALES_AND_MARKETING &&
|
||||
!!goals &&
|
||||
goals.includes(MARKETING_AUTOMATION_OTHER)
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: AUTOMATION_BENEFICIARY_KEY,
|
||||
properties: {
|
||||
type: 'select',
|
||||
label: i18n.baseText('personalizationModal.specifyAutomationBeneficiary'),
|
||||
placeholder: i18n.baseText('personalizationModal.select'),
|
||||
options: [
|
||||
{
|
||||
label: i18n.baseText('personalizationModal.myself'),
|
||||
value: AUTOMATION_BENEFICIARY_SELF,
|
||||
},
|
||||
{
|
||||
label: i18n.baseText('personalizationModal.myTeam'),
|
||||
value: AUTOMATION_BENEFICIARY_MY_TEAM,
|
||||
},
|
||||
{
|
||||
label: i18n.baseText('personalizationModal.otherTeams'),
|
||||
value: AUTOMATION_BENEFICIARY_OTHER_TEAMS,
|
||||
},
|
||||
],
|
||||
},
|
||||
shouldDisplay(values): boolean {
|
||||
const companyType = (values as IPersonalizationLatestVersion)[COMPANY_TYPE_KEY];
|
||||
return companyType !== PERSONAL_COMPANY_TYPE;
|
||||
},
|
||||
},
|
||||
{
|
||||
name: COMPANY_SIZE_KEY,
|
||||
properties: {
|
||||
type: 'select',
|
||||
label: i18n.baseText('personalizationModal.howBigIsYourCompany'),
|
||||
placeholder: i18n.baseText('personalizationModal.select'),
|
||||
options: [
|
||||
{
|
||||
label: i18n.baseText('personalizationModal.lessThan20People'),
|
||||
value: COMPANY_SIZE_20_OR_LESS,
|
||||
},
|
||||
{
|
||||
label: `20-99 ${i18n.baseText('personalizationModal.people')}`,
|
||||
value: COMPANY_SIZE_20_99,
|
||||
},
|
||||
{
|
||||
label: `100-499 ${i18n.baseText('personalizationModal.people')}`,
|
||||
value: COMPANY_SIZE_100_499,
|
||||
},
|
||||
{
|
||||
label: `500-999 ${i18n.baseText('personalizationModal.people')}`,
|
||||
value: COMPANY_SIZE_500_999,
|
||||
},
|
||||
{
|
||||
label: `1000+ ${i18n.baseText('personalizationModal.people')}`,
|
||||
value: COMPANY_SIZE_1000_OR_MORE,
|
||||
},
|
||||
{
|
||||
label: i18n.baseText('personalizationModal.imNotUsingN8nForWork'),
|
||||
value: COMPANY_SIZE_PERSONAL_USE,
|
||||
},
|
||||
],
|
||||
},
|
||||
shouldDisplay(values): boolean {
|
||||
const companyType = (values as IPersonalizationLatestVersion)[COMPANY_TYPE_KEY];
|
||||
return companyType !== PERSONAL_COMPANY_TYPE;
|
||||
},
|
||||
},
|
||||
{
|
||||
name: REPORTED_SOURCE_KEY,
|
||||
properties: {
|
||||
type: 'select',
|
||||
label: i18n.baseText('personalizationModal.howDidYouHearAboutN8n'),
|
||||
placeholder: i18n.baseText('personalizationModal.select'),
|
||||
options: [
|
||||
{
|
||||
label: 'Google',
|
||||
value: REPORTED_SOURCE_GOOGLE,
|
||||
},
|
||||
{
|
||||
label: 'Twitter',
|
||||
value: REPORTED_SOURCE_TWITTER,
|
||||
},
|
||||
{
|
||||
label: 'LinkedIn',
|
||||
value: REPORTED_SOURCE_LINKEDIN,
|
||||
},
|
||||
{
|
||||
label: 'YouTube',
|
||||
value: REPORTED_SOURCE_YOUTUBE,
|
||||
},
|
||||
{
|
||||
label: i18n.baseText('personalizationModal.friendWordOfMouth'),
|
||||
value: REPORTED_SOURCE_FRIEND,
|
||||
},
|
||||
{
|
||||
label: i18n.baseText('personalizationModal.podcast'),
|
||||
value: REPORTED_SOURCE_PODCAST,
|
||||
},
|
||||
{
|
||||
label: i18n.baseText('personalizationModal.event'),
|
||||
value: REPORTED_SOURCE_EVENT,
|
||||
},
|
||||
{
|
||||
label: i18n.baseText('personalizationModal.otherPleaseSpecify'),
|
||||
value: REPORTED_SOURCE_OTHER,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: REPORTED_SOURCE_OTHER_KEY,
|
||||
properties: {
|
||||
placeholder: i18n.baseText('personalizationModal.specifyReportedSource'),
|
||||
},
|
||||
shouldDisplay(values): boolean {
|
||||
const reportedSource = (values as IPersonalizationLatestVersion)[REPORTED_SOURCE_KEY];
|
||||
return reportedSource === REPORTED_SOURCE_OTHER;
|
||||
},
|
||||
},
|
||||
]);
|
||||
},
|
||||
] as const,
|
||||
);
|
||||
|
||||
const onSave = () => {
|
||||
formBus.emit('submit');
|
||||
@@ -575,7 +578,7 @@ const closeDialog = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const onSubmit = async (values: IPersonalizationLatestVersion) => {
|
||||
const onSubmit = async (values: object) => {
|
||||
isSaving.value = true;
|
||||
|
||||
try {
|
||||
|
||||
@@ -4,11 +4,7 @@ import { useI18n } from '@n8n/i18n';
|
||||
import { ResourceType, splitName } from '@/utils/projects.utils';
|
||||
import type { Project, ProjectIcon as BadgeIcon } from '@/types/projects.types';
|
||||
import { ProjectTypes } from '@/types/projects.types';
|
||||
import type {
|
||||
CredentialsResource,
|
||||
FolderResource,
|
||||
WorkflowResource,
|
||||
} from '../layouts/ResourcesListLayout.vue';
|
||||
import type { CredentialsResource, FolderResource, WorkflowResource } from '@/Interface';
|
||||
import { VIEWS } from '@/constants';
|
||||
|
||||
type Props = {
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
<script lang="ts" setup>
|
||||
import type { ButtonType } from '@n8n/design-system';
|
||||
import type { ButtonType, UserAction } from '@n8n/design-system';
|
||||
import { N8nIconButton, N8nActionToggle } from '@n8n/design-system';
|
||||
import { ref } from 'vue';
|
||||
import type { IUser } from 'n8n-workflow';
|
||||
import { useTemplateRef } from 'vue';
|
||||
|
||||
type Action = {
|
||||
label: string;
|
||||
value: string;
|
||||
disabled: boolean;
|
||||
};
|
||||
defineProps<{
|
||||
actions: Action[];
|
||||
actions: Array<UserAction<IUser>>;
|
||||
disabled?: boolean;
|
||||
type?: ButtonType;
|
||||
}>();
|
||||
@@ -18,7 +14,7 @@ const emit = defineEmits<{
|
||||
action: [id: string];
|
||||
}>();
|
||||
|
||||
const actionToggleRef = ref<InstanceType<typeof N8nActionToggle> | null>(null);
|
||||
const actionToggleRef = useTemplateRef('actionToggleRef');
|
||||
|
||||
defineExpose({
|
||||
openActionToggle: (isOpen: boolean) => actionToggleRef.value?.openActionToggle(isOpen),
|
||||
|
||||
@@ -16,6 +16,7 @@ import ProjectCreateResource from '@/components/Projects/ProjectCreateResource.v
|
||||
import { useSettingsStore } from '@/stores/settings.store';
|
||||
import { useProjectPages } from '@/composables/useProjectPages';
|
||||
import { truncateTextToFitWidth } from '@/utils/formatters/textFormatter';
|
||||
import type { IUser } from 'n8n-workflow';
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
@@ -96,7 +97,7 @@ const createWorkflowButton = computed(() => ({
|
||||
}));
|
||||
|
||||
const menu = computed(() => {
|
||||
const items: UserAction[] = [
|
||||
const items: Array<UserAction<IUser>> = [
|
||||
{
|
||||
value: ACTION_TYPES.CREDENTIAL,
|
||||
label: i18n.baseText('projects.header.create.credential'),
|
||||
|
||||
@@ -240,7 +240,7 @@ onMounted(async () => {
|
||||
v-for="p in filteredProjects"
|
||||
:key="p.id"
|
||||
:value="p.id"
|
||||
:label="p.name"
|
||||
:label="p.name ?? ''"
|
||||
></N8nOption>
|
||||
</N8nSelect>
|
||||
<N8nText>
|
||||
|
||||
@@ -43,10 +43,10 @@ const shared = computed<IMenuItem>(() => ({
|
||||
},
|
||||
}));
|
||||
|
||||
const getProjectMenuItem = (project: ProjectListItem) => ({
|
||||
const getProjectMenuItem = (project: ProjectListItem): IMenuItem => ({
|
||||
id: project.id,
|
||||
label: project.name,
|
||||
icon: project.icon,
|
||||
label: project.name ?? '',
|
||||
icon: project.icon as IMenuItem['icon'],
|
||||
route: {
|
||||
to: {
|
||||
name: VIEWS.PROJECTS_WORKFLOWS,
|
||||
@@ -70,6 +70,14 @@ const personalProject = computed<IMenuItem>(() => ({
|
||||
const showAddFirstProject = computed(
|
||||
() => projectsStore.isTeamProjectFeatureEnabled && !displayProjects.value.length,
|
||||
);
|
||||
|
||||
const activeTabId = computed(() => {
|
||||
return (
|
||||
(Array.isArray(projectsStore.projectNavActiveId)
|
||||
? projectsStore.projectNavActiveId[0]
|
||||
: projectsStore.projectNavActiveId) ?? undefined
|
||||
);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -78,7 +86,7 @@ const showAddFirstProject = computed(
|
||||
<N8nMenuItem
|
||||
:item="home"
|
||||
:compact="props.collapsed"
|
||||
:active-tab="projectsStore.projectNavActiveId"
|
||||
:active-tab="activeTabId"
|
||||
mode="tabs"
|
||||
data-test-id="project-home-menu-item"
|
||||
/>
|
||||
@@ -86,7 +94,7 @@ const showAddFirstProject = computed(
|
||||
v-if="projectsStore.isTeamProjectFeatureEnabled || isFoldersFeatureEnabled"
|
||||
:item="personalProject"
|
||||
:compact="props.collapsed"
|
||||
:active-tab="projectsStore.projectNavActiveId"
|
||||
:active-tab="activeTabId"
|
||||
mode="tabs"
|
||||
data-test-id="project-personal-menu-item"
|
||||
/>
|
||||
@@ -94,7 +102,7 @@ const showAddFirstProject = computed(
|
||||
v-if="projectsStore.isTeamProjectFeatureEnabled || isFoldersFeatureEnabled"
|
||||
:item="shared"
|
||||
:compact="props.collapsed"
|
||||
:active-tab="projectsStore.projectNavActiveId"
|
||||
:active-tab="activeTabId"
|
||||
mode="tabs"
|
||||
data-test-id="project-shared-menu-item"
|
||||
/>
|
||||
@@ -136,7 +144,7 @@ const showAddFirstProject = computed(
|
||||
}"
|
||||
:item="getProjectMenuItem(project)"
|
||||
:compact="props.collapsed"
|
||||
:active-tab="projectsStore.projectNavActiveId"
|
||||
:active-tab="activeTabId"
|
||||
mode="tabs"
|
||||
data-test-id="project-menu-item"
|
||||
/>
|
||||
|
||||
@@ -136,7 +136,7 @@ watch(
|
||||
v-for="project in filteredProjects"
|
||||
:key="project.id"
|
||||
:value="project.id"
|
||||
:label="project.name"
|
||||
:label="project.name ?? ''"
|
||||
>
|
||||
<ProjectSharingInfo :project="project" />
|
||||
</N8nOption>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useRoute } from 'vue-router';
|
||||
import { VIEWS } from '@/constants';
|
||||
import { useI18n } from '@n8n/i18n';
|
||||
import type { BaseTextKey } from '@n8n/i18n';
|
||||
import type { TabOptions } from '@n8n/design-system';
|
||||
|
||||
type Props = {
|
||||
showSettings?: boolean;
|
||||
@@ -23,6 +24,8 @@ const route = useRoute();
|
||||
|
||||
const selectedTab = ref<RouteRecordName | null | undefined>('');
|
||||
|
||||
const selectedTabLabel = computed(() => (selectedTab.value ? String(selectedTab.value) : ''));
|
||||
|
||||
const projectId = computed(() => {
|
||||
return Array.isArray(route?.params?.projectId)
|
||||
? route.params.projectId[0]
|
||||
@@ -70,16 +73,16 @@ const createTab = (
|
||||
label: BaseTextKey,
|
||||
routeKey: string,
|
||||
routes: Record<string, { name: RouteRecordName; params?: Record<string, string | number> }>,
|
||||
) => {
|
||||
): TabOptions<string> => {
|
||||
return {
|
||||
label: locale.baseText(label),
|
||||
value: routes[routeKey].name,
|
||||
value: routes[routeKey].name as string,
|
||||
to: routes[routeKey],
|
||||
};
|
||||
};
|
||||
|
||||
// Generate the tabs configuration
|
||||
const options = computed(() => {
|
||||
const options = computed<Array<TabOptions<string>>>(() => {
|
||||
const routes = getRouteConfigs();
|
||||
const tabs = [
|
||||
createTab('mainSidebar.workflows', 'workflows', routes),
|
||||
@@ -93,7 +96,7 @@ const options = computed(() => {
|
||||
if (props.showSettings) {
|
||||
tabs.push({
|
||||
label: locale.baseText('projects.settings'),
|
||||
value: VIEWS.PROJECT_SETTINGS,
|
||||
value: VIEWS.PROJECT_SETTINGS as string,
|
||||
to: { name: VIEWS.PROJECT_SETTINGS, params: { projectId: projectId.value } },
|
||||
});
|
||||
}
|
||||
@@ -110,8 +113,17 @@ watch(
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
function onSelectTab(value: string | number) {
|
||||
selectedTab.value = value as RouteRecordName;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<N8nTabs v-model="selectedTab" :options="options" data-test-id="project-tabs" />
|
||||
<N8nTabs
|
||||
:model-value="selectedTabLabel"
|
||||
:options="options"
|
||||
data-test-id="project-tabs"
|
||||
@update:model-value="onSelectTab"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@@ -4,7 +4,7 @@ import Modal from '../Modal.vue';
|
||||
import { PROMPT_MFA_CODE_MODAL_KEY } from '@/constants';
|
||||
import { useI18n } from '@n8n/i18n';
|
||||
import { promptMfaCodeBus } from '@/event-bus';
|
||||
import type { IFormInputs } from '@/Interface';
|
||||
import { type IFormInput } from '@/Interface';
|
||||
import { createFormEventBus } from '@n8n/design-system/utils';
|
||||
import { validate as validateUuid } from 'uuid';
|
||||
|
||||
@@ -13,7 +13,7 @@ const i18n = useI18n();
|
||||
const formBus = createFormEventBus();
|
||||
const readyToSubmit = ref(false);
|
||||
|
||||
const formFields: IFormInputs = [
|
||||
const formFields: IFormInput[] = [
|
||||
{
|
||||
name: 'mfaCodeOrMfaRecoveryCode',
|
||||
initialValue: '',
|
||||
@@ -25,9 +25,14 @@ const formFields: IFormInputs = [
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
];
|
||||
] as const;
|
||||
|
||||
function onSubmit(values: { mfaCodeOrMfaRecoveryCode: string }) {
|
||||
function onSubmit(values: object) {
|
||||
if (
|
||||
!('mfaCodeOrMfaRecoveryCode' in values && typeof values.mfaCodeOrMfaRecoveryCode === 'string')
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (validateUuid(values.mfaCodeOrMfaRecoveryCode)) {
|
||||
promptMfaCodeBus.emit('close', {
|
||||
mfaRecoveryCode: values.mfaCodeOrMfaRecoveryCode,
|
||||
|
||||
@@ -220,9 +220,9 @@ const hasMultipleModes = computed(() => {
|
||||
});
|
||||
|
||||
const hasOnlyListMode = computed(() => hasOnlyListModeUtil(props.parameter));
|
||||
const valueToDisplay = computed<NodeParameterValue>(() => {
|
||||
const valueToDisplay = computed<INodeParameterResourceLocator['value']>(() => {
|
||||
if (typeof props.modelValue !== 'object') {
|
||||
return props.modelValue;
|
||||
return `${props.modelValue}`;
|
||||
}
|
||||
|
||||
if (isListMode.value) {
|
||||
@@ -398,6 +398,9 @@ const handleAddResourceClick = async () => {
|
||||
const newResource = (await nodeTypesStore.getNodeParameterActionResult(
|
||||
requestParams,
|
||||
)) as NodeParameterValue;
|
||||
if (typeof newResource === 'boolean') {
|
||||
return;
|
||||
}
|
||||
|
||||
refreshList();
|
||||
await loadResources();
|
||||
@@ -563,7 +566,7 @@ function findModeByName(name: string): INodePropertyMode | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
function getModeLabel(mode: INodePropertyMode): string | null {
|
||||
function getModeLabel(mode: INodePropertyMode): string | undefined {
|
||||
if (mode.name === 'id' || mode.name === 'url' || mode.name === 'list') {
|
||||
return i18n.baseText(`resourceLocator.mode.${mode.name}`);
|
||||
}
|
||||
@@ -571,7 +574,7 @@ function getModeLabel(mode: INodePropertyMode): string | null {
|
||||
return mode.displayName;
|
||||
}
|
||||
|
||||
function onInputChange(value: NodeParameterValue): void {
|
||||
function onInputChange(value: INodeParameterResourceLocator['value']): void {
|
||||
const params: INodeParameterResourceLocator = { __rl: true, value, mode: selectedMode.value };
|
||||
if (isListMode.value) {
|
||||
const resource = currentQueryResults.value.find((result) => result.value === value);
|
||||
@@ -823,7 +826,7 @@ function showResourceDropdown() {
|
||||
resourceDropdownVisible.value = true;
|
||||
}
|
||||
|
||||
function onListItemSelected(value: NodeParameterValue) {
|
||||
function onListItemSelected(value: INodeParameterResourceLocator['value']) {
|
||||
onInputChange(value);
|
||||
hideResourceDropdown();
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { IResourceLocatorResultExpanded } from '@/Interface';
|
||||
import { N8nLoading } from '@n8n/design-system';
|
||||
import type { EventBus } from '@n8n/utils/event-bus';
|
||||
import { createEventBus } from '@n8n/utils/event-bus';
|
||||
import type { NodeParameterValue } from 'n8n-workflow';
|
||||
import type { INodeParameterResourceLocator, NodeParameterValue } from 'n8n-workflow';
|
||||
import { computed, onBeforeUnmount, onMounted, ref, useCssModule, watch } from 'vue';
|
||||
|
||||
const SEARCH_BAR_HEIGHT_PX = 40;
|
||||
@@ -43,7 +43,7 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: NodeParameterValue];
|
||||
'update:modelValue': [value: INodeParameterResourceLocator['value']];
|
||||
loadMore: [];
|
||||
filter: [filter: string];
|
||||
addResourceClick: [];
|
||||
@@ -164,7 +164,7 @@ function onKeyDown(e: KeyboardEvent) {
|
||||
const selected = sortedResources.value[hoverIndex.value - 1]?.value;
|
||||
|
||||
// Selected resource can be empty when loading or empty results
|
||||
if (selected) {
|
||||
if (selected && typeof selected !== 'boolean') {
|
||||
emit('update:modelValue', selected);
|
||||
}
|
||||
}
|
||||
@@ -175,6 +175,10 @@ function onFilterInput(value: string) {
|
||||
}
|
||||
|
||||
function onItemClick(selected: string | number | boolean) {
|
||||
if (typeof selected === 'boolean') {
|
||||
return;
|
||||
}
|
||||
|
||||
emit('update:modelValue', selected);
|
||||
}
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ const onSSOLogin = async () => {
|
||||
<div :class="$style.divider">
|
||||
<span>{{ i18n.baseText('sso.login.divider') }}</span>
|
||||
</div>
|
||||
<n8n-button
|
||||
<N8nButton
|
||||
size="large"
|
||||
type="primary"
|
||||
outline
|
||||
|
||||
@@ -2,13 +2,14 @@
|
||||
import KeyboardShortcutTooltip from '@/components/KeyboardShortcutTooltip.vue';
|
||||
import { useI18n } from '@n8n/i18n';
|
||||
import { computed } from 'vue';
|
||||
import type { ButtonType } from '@n8n/design-system';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
saved: boolean;
|
||||
isSaving?: boolean;
|
||||
disabled?: boolean;
|
||||
type?: string;
|
||||
type?: ButtonType;
|
||||
withShortcut?: boolean;
|
||||
shortcutTooltip?: string;
|
||||
savingLabel?: string;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<script lang="ts" setup>
|
||||
import type { WorkflowResource } from '@/components/layouts/ResourcesListLayout.vue';
|
||||
import ProjectCardBadge from '@/components/Projects/ProjectCardBadge.vue';
|
||||
import { useLoadingService } from '@/composables/useLoadingService';
|
||||
import { useTelemetry } from '@/composables/useTelemetry';
|
||||
@@ -41,6 +40,7 @@ import { useRoute } from 'vue-router';
|
||||
import { DynamicScroller, DynamicScrollerItem } from 'vue-virtual-scroller';
|
||||
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css';
|
||||
import Modal from './Modal.vue';
|
||||
import { type WorkflowResource } from '@/Interface';
|
||||
|
||||
const props = defineProps<{
|
||||
data: { eventBus: EventBus; status: SourceControlledFile[] };
|
||||
|
||||
@@ -9,13 +9,15 @@ import type {
|
||||
ITemplatesNode,
|
||||
ITemplatesWorkflow,
|
||||
} from '@n8n/rest-api-client/api/templates';
|
||||
import type { ITag } from '@n8n/rest-api-client/api/tags';
|
||||
import { useTemplatesStore } from '@/stores/templates.store';
|
||||
import TimeAgo from '@/components/TimeAgo.vue';
|
||||
import { isFullTemplatesCollection, isTemplatesWorkflow } from '@/utils/templates/typeGuards';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useI18n } from '@n8n/i18n';
|
||||
import { computed } from 'vue';
|
||||
|
||||
defineProps<{
|
||||
const props = defineProps<{
|
||||
template: ITemplatesWorkflow | ITemplatesCollection | ITemplatesCollectionFull | null;
|
||||
blockTitle: string;
|
||||
loading: boolean;
|
||||
@@ -26,6 +28,15 @@ const i18n = useI18n();
|
||||
|
||||
const templatesStore = useTemplatesStore();
|
||||
|
||||
const categoriesAsTags = computed<ITag[]>(() =>
|
||||
props.template && 'categories' in props.template
|
||||
? props.template.categories.map((category) => ({
|
||||
id: `${category.id}`,
|
||||
name: category.name,
|
||||
}))
|
||||
: [],
|
||||
);
|
||||
|
||||
const redirectToCategory = (id: string) => {
|
||||
templatesStore.resetSessionId();
|
||||
void router.push(`/templates?categories=${id}`);
|
||||
@@ -62,10 +73,10 @@ const redirectToSearchPage = (node: ITemplatesNode) => {
|
||||
</TemplateDetailsBlock>
|
||||
|
||||
<TemplateDetailsBlock
|
||||
v-if="!loading && isFullTemplatesCollection(template) && template.categories.length > 0"
|
||||
v-if="!loading && isFullTemplatesCollection(template) && categoriesAsTags.length > 0"
|
||||
:title="i18n.baseText('template.details.categories')"
|
||||
>
|
||||
<n8n-tags :tags="template.categories" @click:tag="redirectToCategory" />
|
||||
<n8n-tags :tags="categoriesAsTags" @click:tag="redirectToCategory" />
|
||||
</TemplateDetailsBlock>
|
||||
|
||||
<TemplateDetailsBlock
|
||||
|
||||
@@ -473,7 +473,7 @@ const onDragEnd = (el: HTMLElement) => {
|
||||
</VirtualSchemaItem>
|
||||
|
||||
<N8nTooltip v-else-if="item.type === 'icon'" :content="item.tooltip" placement="top">
|
||||
<N8nIcon :size="14" :icon="item.icon" class="icon" />
|
||||
<N8nIcon size="small" :icon="item.icon" class="icon" />
|
||||
</N8nTooltip>
|
||||
|
||||
<div
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import type { IconColor } from '@n8n/design-system';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
icon?: string;
|
||||
iconColor?: string;
|
||||
iconColor?: IconColor;
|
||||
initialExpanded?: boolean;
|
||||
}>(),
|
||||
{
|
||||
icon: 'tasks',
|
||||
iconColor: 'black',
|
||||
iconColor: 'text-dark',
|
||||
initialExpanded: true,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -95,7 +95,7 @@ orchestrationStore.$onAction(({ name, store }) => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<WorkerAccordion icon="tasks" icon-color="black" :initial-expanded="false">
|
||||
<WorkerAccordion icon="tasks" icon-color="text-dark" :initial-expanded="false">
|
||||
<template #title>
|
||||
{{ i18n.baseText('workerList.item.chartsTitle') }}
|
||||
</template>
|
||||
|
||||
@@ -20,7 +20,7 @@ function runningSince(started: Date): string {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<WorkerAccordion icon="tasks" icon-color="black" :initial-expanded="true">
|
||||
<WorkerAccordion icon="tasks" icon-color="text-dark" :initial-expanded="true">
|
||||
<template #title>
|
||||
{{ i18n.baseText('workerList.item.jobListTitle') }} ({{ items.length }})
|
||||
</template>
|
||||
|
||||
@@ -25,7 +25,7 @@ function onCopyToClipboard(content: string) {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<WorkerAccordion icon="tasks" icon-color="black" :initial-expanded="false">
|
||||
<WorkerAccordion icon="tasks" icon-color="text-dark" :initial-expanded="false">
|
||||
<template #title>
|
||||
{{ i18n.baseText('workerList.item.netListTitle') }} ({{ items.length }})
|
||||
</template>
|
||||
|
||||
@@ -51,7 +51,7 @@ const onClick = async () => {
|
||||
<div :class="$style.container">
|
||||
<div>
|
||||
<n8n-text color="text-base"> You can deactivate </n8n-text>
|
||||
<n8n-link :to="workflowUrl" underline="true"> '{{ data.workflowName }}' </n8n-link>
|
||||
<n8n-link :to="workflowUrl" :underline="true"> '{{ data.workflowName }}' </n8n-link>
|
||||
<n8n-text color="text-base">
|
||||
and activate this one, or adjust the following URL path in either workflow:
|
||||
</n8n-text>
|
||||
|
||||
@@ -24,7 +24,7 @@ import { useRoute, useRouter } from 'vue-router';
|
||||
import { useTelemetry } from '@/composables/useTelemetry';
|
||||
import { ResourceType } from '@/utils/projects.utils';
|
||||
import type { EventBus } from '@n8n/utils/event-bus';
|
||||
import type { WorkflowResource } from './layouts/ResourcesListLayout.vue';
|
||||
import type { WorkflowResource } from '@/Interface';
|
||||
import type { IUser } from 'n8n-workflow';
|
||||
import { type ProjectSharingData, ProjectTypes } from '@/types/projects.types';
|
||||
import type { PathItem } from '@n8n/design-system/components/N8nBreadcrumbs/Breadcrumbs.vue';
|
||||
@@ -406,6 +406,11 @@ const onBreadcrumbItemClick = async (item: PathItem) => {
|
||||
await router.push(item.href);
|
||||
}
|
||||
};
|
||||
|
||||
const tags = computed(
|
||||
() =>
|
||||
props.data.tags?.map((tag) => (typeof tag === 'string' ? { id: tag, name: tag } : tag)) ?? [],
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -448,7 +453,7 @@ const onBreadcrumbItemClick = async (item: PathItem) => {
|
||||
:class="$style.cardTags"
|
||||
>
|
||||
<n8n-tags
|
||||
:tags="data.tags"
|
||||
:tags="tags"
|
||||
:truncate-at="3"
|
||||
truncate
|
||||
data-test-id="workflow-card-tags"
|
||||
|
||||
@@ -9,9 +9,10 @@ import WorkflowHistoryContent from '@/components/WorkflowHistory/WorkflowHistory
|
||||
import type { WorkflowHistoryActionTypes } from '@n8n/rest-api-client/api/workflowHistory';
|
||||
import { workflowVersionDataFactory } from '@/stores/__tests__/utils/workflowHistoryTestUtils';
|
||||
import type { IWorkflowDb } from '@/Interface';
|
||||
import type { IUser } from 'n8n-workflow';
|
||||
|
||||
const actionTypes: WorkflowHistoryActionTypes = ['restore', 'clone', 'open', 'download'];
|
||||
const actions: UserAction[] = actionTypes.map((value) => ({
|
||||
const actions: Array<UserAction<IUser>> = actionTypes.map((value) => ({
|
||||
label: value,
|
||||
disabled: false,
|
||||
value,
|
||||
|
||||
@@ -9,13 +9,14 @@ import type {
|
||||
import WorkflowPreview from '@/components/WorkflowPreview.vue';
|
||||
import WorkflowHistoryListItem from '@/components/WorkflowHistory/WorkflowHistoryListItem.vue';
|
||||
import { useI18n } from '@n8n/i18n';
|
||||
import type { IUser } from 'n8n-workflow';
|
||||
|
||||
const i18n = useI18n();
|
||||
|
||||
const props = defineProps<{
|
||||
workflow: IWorkflowDb | null;
|
||||
workflowVersion: WorkflowVersion | null;
|
||||
actions: UserAction[];
|
||||
actions: Array<UserAction<IUser>>;
|
||||
isListLoading?: boolean;
|
||||
isFirstItemShown?: boolean;
|
||||
}>();
|
||||
|
||||
@@ -7,6 +7,7 @@ import { createComponentRenderer } from '@/__tests__/render';
|
||||
import WorkflowHistoryList from '@/components/WorkflowHistory/WorkflowHistoryList.vue';
|
||||
import type { WorkflowHistoryActionTypes } from '@n8n/rest-api-client/api/workflowHistory';
|
||||
import { workflowHistoryDataFactory } from '@/stores/__tests__/utils/workflowHistoryTestUtils';
|
||||
import type { IUser } from 'n8n-workflow';
|
||||
|
||||
vi.stubGlobal(
|
||||
'IntersectionObserver',
|
||||
@@ -19,7 +20,7 @@ vi.stubGlobal(
|
||||
);
|
||||
|
||||
const actionTypes: WorkflowHistoryActionTypes = ['restore', 'clone', 'open', 'download'];
|
||||
const actions: UserAction[] = actionTypes.map((value) => ({
|
||||
const actions: Array<UserAction<IUser>> = actionTypes.map((value) => ({
|
||||
label: value,
|
||||
disabled: false,
|
||||
value,
|
||||
|
||||
@@ -9,11 +9,12 @@ import type {
|
||||
WorkflowHistoryRequestParams,
|
||||
} from '@n8n/rest-api-client/api/workflowHistory';
|
||||
import WorkflowHistoryListItem from '@/components/WorkflowHistory/WorkflowHistoryListItem.vue';
|
||||
import type { IUser } from 'n8n-workflow';
|
||||
|
||||
const props = defineProps<{
|
||||
items: WorkflowHistory[];
|
||||
activeItem: WorkflowHistory | null;
|
||||
actions: UserAction[];
|
||||
actions: Array<UserAction<IUser>>;
|
||||
requestNumberOfItems: number;
|
||||
lastReceivedItemsLength: number;
|
||||
evaluatedPruneTime: number;
|
||||
|
||||
@@ -5,9 +5,10 @@ import { createComponentRenderer } from '@/__tests__/render';
|
||||
import WorkflowHistoryListItem from '@/components/WorkflowHistory/WorkflowHistoryListItem.vue';
|
||||
import type { WorkflowHistoryActionTypes } from '@n8n/rest-api-client/api/workflowHistory';
|
||||
import { workflowHistoryDataFactory } from '@/stores/__tests__/utils/workflowHistoryTestUtils';
|
||||
import { type IUser } from 'n8n-workflow';
|
||||
|
||||
const actionTypes: WorkflowHistoryActionTypes = ['restore', 'clone', 'open', 'download'];
|
||||
const actions: UserAction[] = actionTypes.map((value) => ({
|
||||
const actions: Array<UserAction<IUser>> = actionTypes.map((value) => ({
|
||||
label: value,
|
||||
disabled: false,
|
||||
value,
|
||||
|
||||
@@ -8,11 +8,12 @@ import type {
|
||||
WorkflowHistoryActionTypes,
|
||||
} from '@n8n/rest-api-client/api/workflowHistory';
|
||||
import { useI18n } from '@n8n/i18n';
|
||||
import type { IUser } from 'n8n-workflow';
|
||||
|
||||
const props = defineProps<{
|
||||
item: WorkflowHistory;
|
||||
index: number;
|
||||
actions: UserAction[];
|
||||
actions: Array<UserAction<IUser>>;
|
||||
isActive: boolean;
|
||||
}>();
|
||||
const emit = defineEmits<{
|
||||
@@ -62,7 +63,8 @@ const idLabel = computed<string>(() =>
|
||||
i18n.baseText('workflowHistory.item.id', { interpolate: { id: props.item.versionId } }),
|
||||
);
|
||||
|
||||
const onAction = (action: WorkflowHistoryActionTypes[number]) => {
|
||||
const onAction = (value: string) => {
|
||||
const action = value as WorkflowHistoryActionTypes[number];
|
||||
emit('action', {
|
||||
action,
|
||||
id: props.item.versionId,
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { useI18n } from '@n8n/i18n';
|
||||
import Modal from '@/components/Modal.vue';
|
||||
import { useUIStore } from '@/stores/ui.store';
|
||||
import type { ButtonType } from '@n8n/design-system';
|
||||
|
||||
const props = defineProps<{
|
||||
modalName: string;
|
||||
@@ -11,7 +12,7 @@ const props = defineProps<{
|
||||
beforeClose: () => void;
|
||||
buttons: Array<{
|
||||
text: string;
|
||||
type: string;
|
||||
type: ButtonType;
|
||||
action: () => void;
|
||||
}>;
|
||||
};
|
||||
|
||||
@@ -122,9 +122,9 @@ const getCreateResourceLabel = computed(() => {
|
||||
});
|
||||
});
|
||||
|
||||
const valueToDisplay = computed<NodeParameterValue>(() => {
|
||||
const valueToDisplay = computed<INodeParameterResourceLocator['value']>(() => {
|
||||
if (typeof props.modelValue !== 'object') {
|
||||
return props.modelValue;
|
||||
return props.modelValue ?? '';
|
||||
}
|
||||
|
||||
if (isListMode.value) {
|
||||
@@ -208,9 +208,6 @@ async function refreshCachedWorkflow() {
|
||||
}
|
||||
|
||||
const workflowId = props.modelValue.value;
|
||||
if (workflowId === true) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await workflowsStore.fetchWorkflow(`${workflowId}`);
|
||||
onInputChange(workflowId);
|
||||
|
||||
@@ -49,7 +49,7 @@ export function useWorkflowResourceLocatorModes(
|
||||
};
|
||||
}
|
||||
|
||||
function getModeLabel(mode: INodePropertyMode): string | null {
|
||||
function getModeLabel(mode: INodePropertyMode): string | undefined {
|
||||
if (mode.name === 'id' || mode.name === 'list') {
|
||||
return i18n.baseText(`resourceLocator.mode.${mode.name}`);
|
||||
}
|
||||
|
||||
@@ -701,7 +701,7 @@ onMounted(async () => {
|
||||
>
|
||||
<N8nOption
|
||||
v-for="option of saveManualOptions"
|
||||
:key="option.key"
|
||||
:key="`${option.key}`"
|
||||
:label="option.value"
|
||||
:value="option.key"
|
||||
>
|
||||
@@ -730,7 +730,7 @@ onMounted(async () => {
|
||||
>
|
||||
<N8nOption
|
||||
v-for="option of saveExecutionProgressOptions"
|
||||
:key="option.key"
|
||||
:key="`${option.key}`"
|
||||
:label="option.value"
|
||||
:value="option.key"
|
||||
>
|
||||
|
||||
@@ -316,7 +316,7 @@ exports[`VirtualSchema.vue > renders preview schema when enabled and available 1
|
||||
|
||||
|
||||
<span
|
||||
class="n8n-text compact size-14 regular n8n-icon icon el-tooltip__trigger el-tooltip__trigger icon el-tooltip__trigger el-tooltip__trigger n8n-icon icon el-tooltip__trigger el-tooltip__trigger icon el-tooltip__trigger el-tooltip__trigger"
|
||||
class="n8n-text compact size-small regular n8n-icon icon el-tooltip__trigger el-tooltip__trigger icon el-tooltip__trigger el-tooltip__trigger n8n-icon icon el-tooltip__trigger el-tooltip__trigger icon el-tooltip__trigger el-tooltip__trigger"
|
||||
data-v-d00cba9a=""
|
||||
>
|
||||
|
||||
@@ -325,7 +325,7 @@ exports[`VirtualSchema.vue > renders preview schema when enabled and available 1
|
||||
beatfade="false"
|
||||
border="false"
|
||||
bounce="false"
|
||||
class="14"
|
||||
class="small"
|
||||
fade="false"
|
||||
fixedwidth="false"
|
||||
flash="false"
|
||||
|
||||
@@ -3,10 +3,11 @@ import { useUIStore } from '@/stores/ui.store';
|
||||
import { computed, useSlots } from 'vue';
|
||||
import type { BannerName } from '@n8n/api-types';
|
||||
import { useI18n } from '@n8n/i18n';
|
||||
import type { CalloutTheme } from '@n8n/design-system';
|
||||
|
||||
interface Props {
|
||||
name: BannerName;
|
||||
theme?: string;
|
||||
theme?: CalloutTheme;
|
||||
customIcon?: string;
|
||||
dismissible?: boolean;
|
||||
}
|
||||
|
||||
@@ -157,6 +157,12 @@ const goToUpgrade = () => {
|
||||
void pageRedirectionHelper.goToUpgrade('custom-data-filter', 'upgrade-custom-data-filter');
|
||||
};
|
||||
|
||||
const onExactMatchChange = (e: string | number | boolean) => {
|
||||
if (typeof e === 'boolean') {
|
||||
onFilterMetaChange(0, 'exactMatch', e);
|
||||
}
|
||||
};
|
||||
|
||||
onBeforeMount(() => {
|
||||
isCustomDataFilterTracked.value = false;
|
||||
});
|
||||
@@ -348,7 +354,7 @@ onBeforeMount(() => {
|
||||
:model-value="filter.metadata[0]?.exactMatch"
|
||||
:disabled="!isAdvancedExecutionFilterEnabled"
|
||||
data-test-id="execution-filter-saved-data-exact-match-checkbox"
|
||||
@update:model-value="onFilterMetaChange(0, 'exactMatch', $event)"
|
||||
@update:model-value="onExactMatchChange"
|
||||
/>
|
||||
</n8n-tooltip>
|
||||
</div>
|
||||
|
||||
@@ -10,6 +10,8 @@ import { deepCopy } from 'n8n-workflow';
|
||||
import { useNpsSurveyStore } from '@/stores/npsSurvey.store';
|
||||
import { useI18n } from '@n8n/i18n';
|
||||
import { useWorkflowSaving } from '@/composables/useWorkflowSaving';
|
||||
import type { IconColor } from '@n8n/design-system';
|
||||
import { type IAccordionItem } from '@n8n/design-system/components/N8nInfoAccordion/InfoAccordion.vue';
|
||||
|
||||
interface IWorkflowSaveSettings {
|
||||
saveFailedExecutions: boolean;
|
||||
@@ -47,7 +49,7 @@ const workflowSaveSettings = ref({
|
||||
saveTestExecutions: false,
|
||||
} as IWorkflowSaveSettings);
|
||||
|
||||
const accordionItems = computed(() => [
|
||||
const accordionItems = computed((): IAccordionItem[] => [
|
||||
{
|
||||
id: 'productionExecutions',
|
||||
label: locale.baseText('executionsLandingPage.emptyState.accordion.productionExecutions'),
|
||||
@@ -77,7 +79,7 @@ const shouldExpandAccordion = computed(() => {
|
||||
!workflowSaveSettings.value.saveTestExecutions
|
||||
);
|
||||
});
|
||||
const productionExecutionsIcon = computed(() => {
|
||||
const productionExecutionsIcon = computed((): { color: IconColor; icon: string } => {
|
||||
if (productionExecutionsStatus.value === 'saving') {
|
||||
return { icon: 'check', color: 'success' };
|
||||
} else if (productionExecutionsStatus.value === 'not-saving') {
|
||||
@@ -104,9 +106,9 @@ const accordionIcon = computed(() => {
|
||||
!workflowSaveSettings.value.saveTestExecutions ||
|
||||
productionExecutionsStatus.value !== 'saving'
|
||||
) {
|
||||
return { icon: 'exclamation-triangle', color: 'warning' };
|
||||
return { icon: 'exclamation-triangle', color: 'warning' as IconColor };
|
||||
}
|
||||
return null;
|
||||
return undefined;
|
||||
});
|
||||
const currentWorkflowId = computed(() => workflowsStore.workflowId);
|
||||
const isNewWorkflow = computed(() => {
|
||||
|
||||
@@ -4,7 +4,7 @@ import { EnterpriseEditionFeature } from '@/constants';
|
||||
import { useProjectsStore } from '@/stores/projects.store';
|
||||
import type { ProjectSharingData } from '@/types/projects.types';
|
||||
import ProjectSharing from '@/components/Projects/ProjectSharing.vue';
|
||||
import type { BaseFilters } from '../layouts/ResourcesListLayout.vue';
|
||||
import type { BaseFilters } from '@/Interface';
|
||||
import { useI18n } from '@n8n/i18n';
|
||||
|
||||
type IResourceFiltersType = Record<string, boolean | string | string[]>;
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { setActivePinia } from 'pinia';
|
||||
import { createTestingPinia } from '@pinia/testing';
|
||||
import { createComponentRenderer } from '@/__tests__/render';
|
||||
import ResourcesListLayout, { type Resource } from '@/components/layouts/ResourcesListLayout.vue';
|
||||
import ResourcesListLayout from '@/components/layouts/ResourcesListLayout.vue';
|
||||
import type { Resource } from '@/Interface';
|
||||
import type router from 'vue-router';
|
||||
import type { ProjectSharingData } from 'n8n-workflow';
|
||||
import { waitAllPromises } from '@/__tests__/utils';
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<script lang="ts" setup>
|
||||
<script lang="ts" setup generic="ResourceType extends Resource = Resource">
|
||||
import { computed, nextTick, ref, onMounted, watch, onBeforeUnmount } from 'vue';
|
||||
|
||||
import { type ProjectSharingData } from '@/types/projects.types';
|
||||
import PageViewLayout from '@/components/layouts/PageViewLayout.vue';
|
||||
import PageViewLayoutList from '@/components/layouts/PageViewLayoutList.vue';
|
||||
import ResourceFiltersDropdown from '@/components/forms/ResourceFiltersDropdown.vue';
|
||||
@@ -13,64 +12,12 @@ import { useTelemetry } from '@/composables/useTelemetry';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
|
||||
import type { BaseTextKey } from '@n8n/i18n';
|
||||
import type { Scope } from '@n8n/permissions';
|
||||
import type { ITag } from '@n8n/rest-api-client/api/tags';
|
||||
import type { BaseFolderItem, BaseResource, ResourceParentFolder } from '@/Interface';
|
||||
import type { BaseFilters, Resource, SortingAndPaginationUpdates } from '@/Interface';
|
||||
import { isSharedResource, isResourceSortableByDate } from '@/utils/typeGuards';
|
||||
import { useN8nLocalStorage } from '@/composables/useN8nLocalStorage';
|
||||
|
||||
type ResourceKeyType = 'credentials' | 'workflows' | 'variables' | 'folders';
|
||||
|
||||
export type FolderResource = BaseFolderItem & {
|
||||
resourceType: 'folder';
|
||||
};
|
||||
|
||||
export type WorkflowResource = BaseResource & {
|
||||
resourceType: 'workflow';
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
active: boolean;
|
||||
isArchived: boolean;
|
||||
homeProject?: ProjectSharingData;
|
||||
scopes?: Scope[];
|
||||
tags?: ITag[] | string[];
|
||||
sharedWithProjects?: ProjectSharingData[];
|
||||
readOnly: boolean;
|
||||
parentFolder?: ResourceParentFolder;
|
||||
};
|
||||
|
||||
export type VariableResource = BaseResource & {
|
||||
resourceType: 'variable';
|
||||
key?: string;
|
||||
value?: string;
|
||||
};
|
||||
|
||||
export type CredentialsResource = BaseResource & {
|
||||
resourceType: 'credential';
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
type: string;
|
||||
homeProject?: ProjectSharingData;
|
||||
scopes?: Scope[];
|
||||
sharedWithProjects?: ProjectSharingData[];
|
||||
readOnly: boolean;
|
||||
needsSetup: boolean;
|
||||
};
|
||||
|
||||
export type Resource = WorkflowResource | FolderResource | CredentialsResource | VariableResource;
|
||||
|
||||
export type BaseFilters = {
|
||||
search: string;
|
||||
homeProject: string;
|
||||
[key: string]: boolean | string | string[];
|
||||
};
|
||||
|
||||
export type SortingAndPaginationUpdates = {
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
sort?: string;
|
||||
};
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const i18n = useI18n();
|
||||
@@ -82,19 +29,19 @@ const n8nLocalStorage = useN8nLocalStorage();
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
resourceKey: ResourceKeyType;
|
||||
displayName?: (resource: Resource) => string;
|
||||
resources: Resource[];
|
||||
displayName?: (resource: ResourceType) => string;
|
||||
resources: ResourceType[];
|
||||
disabled: boolean;
|
||||
initialize?: () => Promise<void>;
|
||||
filters?: BaseFilters;
|
||||
additionalFiltersHandler?: (
|
||||
resource: Resource,
|
||||
resource: ResourceType,
|
||||
filters: BaseFilters,
|
||||
matches: boolean,
|
||||
) => boolean;
|
||||
shareable?: boolean;
|
||||
showFiltersDropdown?: boolean;
|
||||
sortFns?: Record<string, (a: Resource, b: Resource) => number>;
|
||||
sortFns?: Record<string, (a: ResourceType, b: ResourceType) => number>;
|
||||
sortOptions?: string[];
|
||||
type?: 'datatable' | 'list-full' | 'list-paginated';
|
||||
typeProps: { itemSize: number } | { columns: DatatableColumn[] };
|
||||
@@ -108,7 +55,7 @@ const props = withDefaults(
|
||||
hasEmptyState?: boolean;
|
||||
}>(),
|
||||
{
|
||||
displayName: (resource: Resource) => resource.name || '',
|
||||
displayName: (resource: ResourceType) => resource.name || '',
|
||||
initialize: async () => {},
|
||||
filters: () => ({ search: '', homeProject: '' }),
|
||||
sortFns: () => ({}),
|
||||
@@ -190,7 +137,7 @@ const filterKeys = computed(() => {
|
||||
return Object.keys(filtersModel.value);
|
||||
});
|
||||
|
||||
const filteredAndSortedResources = computed(() => {
|
||||
const filteredAndSortedResources = computed((): ResourceType[] => {
|
||||
if (props.dontPerformSortingAndFiltering) {
|
||||
return props.resources;
|
||||
}
|
||||
@@ -200,7 +147,11 @@ const filteredAndSortedResources = computed(() => {
|
||||
if (filtersModel.value.homeProject && isSharedResource(resource)) {
|
||||
matches =
|
||||
matches &&
|
||||
!!(resource.homeProject && resource.homeProject.id === filtersModel.value.homeProject);
|
||||
!!(
|
||||
'homeProject' in resource &&
|
||||
resource.homeProject &&
|
||||
resource.homeProject.id === filtersModel.value.homeProject
|
||||
);
|
||||
}
|
||||
|
||||
if (filtersModel.value.search) {
|
||||
@@ -222,16 +173,27 @@ const filteredAndSortedResources = computed(() => {
|
||||
if (!sortableByDate) {
|
||||
return 0;
|
||||
}
|
||||
return props.sortFns.lastUpdated
|
||||
? props.sortFns.lastUpdated(a, b)
|
||||
: new Date(b.updatedAt ?? '').valueOf() - new Date(a.updatedAt ?? '').valueOf();
|
||||
|
||||
if ('updatedAt' in a && 'updatedAt' in b) {
|
||||
return props.sortFns.lastUpdated
|
||||
? props.sortFns.lastUpdated(a, b)
|
||||
: new Date(b.updatedAt ?? '').valueOf() - new Date(a.updatedAt ?? '').valueOf();
|
||||
}
|
||||
|
||||
return 0;
|
||||
|
||||
case 'lastCreated':
|
||||
if (!sortableByDate) {
|
||||
return 0;
|
||||
}
|
||||
return props.sortFns.lastCreated
|
||||
? props.sortFns.lastCreated(a, b)
|
||||
: new Date(b.createdAt ?? '').valueOf() - new Date(a.createdAt ?? '').valueOf();
|
||||
|
||||
if ('createdAt' in a && 'createdAt' in b) {
|
||||
return props.sortFns.lastCreated
|
||||
? props.sortFns.lastCreated(a, b)
|
||||
: new Date(b.createdAt ?? '').valueOf() - new Date(a.createdAt ?? '').valueOf();
|
||||
}
|
||||
|
||||
return 0;
|
||||
case 'nameAsc':
|
||||
return props.sortFns.nameAsc
|
||||
? props.sortFns.nameAsc(a, b)
|
||||
@@ -718,7 +680,7 @@ defineExpose({
|
||||
data-test-id="resources-list"
|
||||
:items="filteredAndSortedResources"
|
||||
:item-size="itemSize()"
|
||||
item-key="id"
|
||||
:item-key="'id'"
|
||||
>
|
||||
<template #default="{ item, updateItemSize }">
|
||||
<slot :data="item" :update-item-size="updateItemSize" />
|
||||
|
||||
@@ -386,7 +386,7 @@ export function useWorkflowSaving({ router }: { router: ReturnType<typeof useRou
|
||||
});
|
||||
|
||||
const createdTags = (workflowData.tags || []) as ITag[];
|
||||
const tagIds = createdTags.map((tag: ITag): string => tag.id);
|
||||
const tagIds = createdTags.map((tag: ITag) => tag.id);
|
||||
workflowsStore.setWorkflowTagIds(tagIds);
|
||||
|
||||
const templateId = router.currentRoute.value.query.templateId;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import type { PropType } from 'vue';
|
||||
import { useAttrs } from 'vue';
|
||||
|
||||
defineProps({
|
||||
label: {
|
||||
@@ -15,6 +16,13 @@ defineProps({
|
||||
default: 'top',
|
||||
},
|
||||
});
|
||||
|
||||
const attrs = useAttrs();
|
||||
|
||||
const onClick = () => {
|
||||
// @ts-expect-error Attrs onClick is not typed
|
||||
attrs.onClick?.();
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -23,7 +31,7 @@ defineProps({
|
||||
<template #content>
|
||||
{{ label }}
|
||||
</template>
|
||||
<n8n-icon :class="$style.icon" :icon="icon" size="xsmall" @click="$attrs.onClick" />
|
||||
<n8n-icon :class="$style.icon" :icon="icon" size="xsmall" @click="onClick" />
|
||||
</n8n-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -4,19 +4,21 @@ import type {
|
||||
TriggerPanelDefinition,
|
||||
} from 'n8n-workflow';
|
||||
import { nodeConnectionTypes } from 'n8n-workflow';
|
||||
import type { IExecutionResponse, ICredentialsResponse, NewCredentialsModal } from '@/Interface';
|
||||
import type { Connection as VueFlowConnection } from '@vue-flow/core';
|
||||
import type { RouteLocationRaw } from 'vue-router';
|
||||
import type { CanvasConnectionMode } from '@/types';
|
||||
import { canvasConnectionModes } from '@/types';
|
||||
import type { ComponentPublicInstance } from 'vue';
|
||||
import type {
|
||||
IExecutionResponse,
|
||||
ICredentialsResponse,
|
||||
NewCredentialsModal,
|
||||
CredentialsResource,
|
||||
FolderResource,
|
||||
Resource,
|
||||
VariableResource,
|
||||
WorkflowResource,
|
||||
} from '@/components/layouts/ResourcesListLayout.vue';
|
||||
} from '@/Interface';
|
||||
import type { Connection as VueFlowConnection } from '@vue-flow/core';
|
||||
import type { RouteLocationRaw } from 'vue-router';
|
||||
import type { CanvasConnectionMode } from '@/types';
|
||||
import { canvasConnectionModes } from '@/types';
|
||||
import type { ComponentPublicInstance } from 'vue';
|
||||
|
||||
/*
|
||||
Type guards used in editor-ui project
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import Logo from '@/components/Logo/Logo.vue';
|
||||
import SSOLogin from '@/components/SSOLogin.vue';
|
||||
import type { IFormBoxConfig } from '@/Interface';
|
||||
import type { FormFieldValueUpdate, IFormBoxConfig } from '@/Interface';
|
||||
import { useSettingsStore } from '@/stores/settings.store';
|
||||
import type { EmailOrLdapLoginIdAndPassword } from './SigninView.vue';
|
||||
|
||||
@@ -19,17 +19,17 @@ withDefaults(
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
update: [{ name: string; value: string }];
|
||||
update: [FormFieldValueUpdate];
|
||||
submit: [values: EmailOrLdapLoginIdAndPassword];
|
||||
secondaryClick: [];
|
||||
}>();
|
||||
|
||||
const onUpdate = (e: { name: string; value: string }) => {
|
||||
const onUpdate = (e: FormFieldValueUpdate) => {
|
||||
emit('update', e);
|
||||
};
|
||||
|
||||
const onSubmit = (values: EmailOrLdapLoginIdAndPassword) => {
|
||||
emit('submit', values);
|
||||
const onSubmit = (data: unknown) => {
|
||||
emit('submit', data as EmailOrLdapLoginIdAndPassword);
|
||||
};
|
||||
|
||||
const onSecondaryClick = () => {
|
||||
@@ -45,10 +45,10 @@ const {
|
||||
<div :class="$style.container">
|
||||
<Logo location="authView" :release-channel="releaseChannel" />
|
||||
<div v-if="subtitle" :class="$style.textContainer">
|
||||
<n8n-text size="large">{{ subtitle }}</n8n-text>
|
||||
<N8nText size="large">{{ subtitle }}</N8nText>
|
||||
</div>
|
||||
<div :class="$style.formContainer">
|
||||
<n8n-form-box
|
||||
<N8nFormBox
|
||||
v-bind="form"
|
||||
data-test-id="auth-form"
|
||||
:button-loading="formLoading"
|
||||
@@ -57,7 +57,7 @@ const {
|
||||
@update="onUpdate"
|
||||
>
|
||||
<SSOLogin v-if="withSso" />
|
||||
</n8n-form-box>
|
||||
</N8nFormBox>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -8,7 +8,7 @@ import { useI18n } from '@n8n/i18n';
|
||||
import { useToast } from '@/composables/useToast';
|
||||
import { useUsersStore } from '@/stores/users.store';
|
||||
|
||||
import type { IFormBoxConfig } from '@/Interface';
|
||||
import type { FormFieldValueUpdate, IFormBoxConfig } from '@/Interface';
|
||||
import { MFA_AUTHENTICATION_CODE_INPUT_MAX_LENGTH, VIEWS } from '@/constants';
|
||||
|
||||
const usersStore = useUsersStore();
|
||||
@@ -80,8 +80,8 @@ const onSubmit = async (values: { [key: string]: string }) => {
|
||||
loading.value = false;
|
||||
};
|
||||
|
||||
const onInput = (e: { name: string; value: string }) => {
|
||||
if (e.name === 'password') {
|
||||
const onInput = (e: FormFieldValueUpdate) => {
|
||||
if (e.name === 'password' && typeof e.value === 'string') {
|
||||
password.value = e.value;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import CredentialCard from '@/components/CredentialCard.vue';
|
||||
import ResourcesListLayout, {
|
||||
type BaseFilters,
|
||||
type Resource,
|
||||
} from '@/components/layouts/ResourcesListLayout.vue';
|
||||
import ResourcesListLayout from '@/components/layouts/ResourcesListLayout.vue';
|
||||
import type { BaseFilters, Resource } from '@/Interface';
|
||||
import ProjectHeader from '@/components/Projects/ProjectHeader.vue';
|
||||
import { useDocumentTitle } from '@/composables/useDocumentTitle';
|
||||
import { useProjectPages } from '@/composables/useProjectPages';
|
||||
|
||||
@@ -99,11 +99,13 @@ const onBackClick = () => {
|
||||
emit('onBackClick', MFA_FORM.MFA_RECOVERY_CODE);
|
||||
};
|
||||
|
||||
const onSubmit = async (form: { mfaCode: string; mfaRecoveryCode: string }) => {
|
||||
const onSubmit = (formData: unknown) => {
|
||||
const data = formData as { mfaCode: string; mfaRecoveryCode: string };
|
||||
|
||||
formError.value = !showRecoveryCodeForm.value
|
||||
? i18.baseText('mfa.code.invalid')
|
||||
: i18.baseText('mfa.recovery.invalid');
|
||||
emit('submit', form);
|
||||
emit('submit', data);
|
||||
};
|
||||
|
||||
const onInput = ({ target: { value, name } }: { target: { value: string; name: string } }) => {
|
||||
@@ -124,9 +126,12 @@ const onInput = ({ target: { value, name } }: { target: { value: string; name: s
|
||||
? { mfaCode: value, mfaRecoveryCode: '' }
|
||||
: { mfaCode: '', mfaRecoveryCode: value };
|
||||
|
||||
onSubmit(dataToSubmit)
|
||||
.catch(() => {})
|
||||
.finally(() => (verifyingMfaCode.value = false));
|
||||
try {
|
||||
onSubmit(dataToSubmit);
|
||||
} catch (e) {
|
||||
} finally {
|
||||
verifyingMfaCode.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const mfaRecoveryCodeFieldWithDefaults = () => {
|
||||
|
||||
@@ -102,9 +102,9 @@ const onAddMember = (userId: string) => {
|
||||
formData.value.relations.push(relation);
|
||||
};
|
||||
|
||||
const onRoleAction = (user: Partial<IUser>, role: string) => {
|
||||
const onRoleAction = (userId: string, role?: string) => {
|
||||
isDirty.value = true;
|
||||
const index = formData.value.relations.findIndex((r: ProjectRelation) => r.id === user.id);
|
||||
const index = formData.value.relations.findIndex((r: ProjectRelation) => r.id === userId);
|
||||
if (role === 'remove') {
|
||||
formData.value.relations.splice(index, 1);
|
||||
} else {
|
||||
@@ -394,7 +394,7 @@ onMounted(() => {
|
||||
:model-value="user?.role || projectRoles[0].role"
|
||||
size="small"
|
||||
data-test-id="projects-settings-user-role-select"
|
||||
@update:model-value="onRoleAction(user, $event)"
|
||||
@update:model-value="onRoleAction(user.id, $event)"
|
||||
>
|
||||
<N8nOption
|
||||
v-for="role in projectRoles"
|
||||
@@ -419,7 +419,7 @@ onMounted(() => {
|
||||
square
|
||||
icon="trash"
|
||||
data-test-id="project-user-remove"
|
||||
@click="onRoleAction(user, 'remove')"
|
||||
@click="onRoleAction(user.id, 'remove')"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -73,7 +73,7 @@ const getEmptyStateButtonText = computed(() => {
|
||||
const actionBoxConfig = computed(() => {
|
||||
return {
|
||||
calloutText: '',
|
||||
calloutTheme: '',
|
||||
calloutTheme: undefined,
|
||||
hideButton: false,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -106,7 +106,7 @@ const cellClassStyle = ({ row, column }: CellClassStyleMethodParams<TableRow>):
|
||||
return {};
|
||||
};
|
||||
|
||||
const onInput = (input: { name: string; value: string | number | boolean }) => {
|
||||
const onInput = (input: { name: string; value: string | number | boolean | null | undefined }) => {
|
||||
if (input.name === 'loginEnabled' && typeof input.value === 'boolean') {
|
||||
loginEnabled.value = input.value;
|
||||
}
|
||||
|
||||
@@ -194,7 +194,8 @@ async function saveUserSettings(params: UserBasicDetailsWithMfa) {
|
||||
}
|
||||
}
|
||||
|
||||
async function onSubmit(form: UserBasicDetailsForm) {
|
||||
async function onSubmit(data: Record<string, string | number | boolean | null | undefined>) {
|
||||
const form = data as UserBasicDetailsForm;
|
||||
const emailChanged = usersStore.currentUser?.email !== form.email;
|
||||
|
||||
if (usersStore.currentUser?.mfaEnabled && emailChanged) {
|
||||
|
||||
@@ -11,6 +11,7 @@ import type { SshKeyTypes } from '@/types/sourceControl.types';
|
||||
import type { TupleToUnion } from '@/utils/typeHelpers';
|
||||
import type { Rule, RuleGroup } from '@n8n/design-system/types';
|
||||
import { useI18n } from '@n8n/i18n';
|
||||
import type { Validatable } from '@n8n/design-system';
|
||||
import { computed, onMounted, reactive, ref } from 'vue';
|
||||
|
||||
const locale = useI18n();
|
||||
@@ -94,11 +95,11 @@ const onSave = async () => {
|
||||
loadingService.stopLoading();
|
||||
};
|
||||
|
||||
const onSelect = async (b: string) => {
|
||||
const onSelect = (b: Validatable) => {
|
||||
if (b === sourceControlStore.preferences.branchName) {
|
||||
return;
|
||||
}
|
||||
sourceControlStore.preferences.branchName = b;
|
||||
sourceControlStore.preferences.branchName = b as string;
|
||||
};
|
||||
|
||||
const goToUpgrade = () => {
|
||||
@@ -180,7 +181,8 @@ const refreshBranches = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
const onSelectSshKeyType = async (sshKeyType: TupleToUnion<SshKeyTypes>) => {
|
||||
const onSelectSshKeyType = (value: Validatable) => {
|
||||
const sshKeyType = value as TupleToUnion<SshKeyTypes>;
|
||||
if (sshKeyType === sourceControlStore.preferences.keyGeneratorType) {
|
||||
return;
|
||||
}
|
||||
@@ -215,7 +217,7 @@ const onSelectSshKeyType = async (sshKeyType: TupleToUnion<SshKeyTypes>) => {
|
||||
<n8n-form-input
|
||||
id="repoUrl"
|
||||
v-model="sourceControlStore.preferences.repositoryUrl"
|
||||
label
|
||||
label=""
|
||||
class="ml-0"
|
||||
name="repoUrl"
|
||||
validate-on-blur
|
||||
@@ -243,7 +245,7 @@ const onSelectSshKeyType = async (sshKeyType: TupleToUnion<SshKeyTypes>) => {
|
||||
v-if="!isConnected"
|
||||
id="keyGeneratorType"
|
||||
:class="$style.sshKeyTypeSelect"
|
||||
label
|
||||
label=""
|
||||
type="select"
|
||||
name="keyGeneratorType"
|
||||
data-test-id="source-control-ssh-key-type-select"
|
||||
@@ -303,7 +305,7 @@ const onSelectSshKeyType = async (sshKeyType: TupleToUnion<SshKeyTypes>) => {
|
||||
<div :class="$style.branchSelection">
|
||||
<n8n-form-input
|
||||
id="branchName"
|
||||
label
|
||||
label=""
|
||||
type="select"
|
||||
name="branchName"
|
||||
class="mb-s"
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
<script lang="ts" setup>
|
||||
import { ROLE, type Role } from '@n8n/api-types';
|
||||
import { EnterpriseEditionFeature, INVITE_USER_MODAL_KEY } from '@/constants';
|
||||
import type { IUser, IUserListAction, InvitableRoleName } from '@/Interface';
|
||||
import type { InvitableRoleName, IUser } from '@/Interface';
|
||||
import type { UserAction } from '@n8n/design-system';
|
||||
import { useToast } from '@/composables/useToast';
|
||||
import { useUIStore } from '@/stores/ui.store';
|
||||
import { useSettingsStore } from '@/stores/settings.store';
|
||||
@@ -31,6 +32,8 @@ const showUMSetupWarning = computed(() => {
|
||||
return hasPermission(['defaultUser']);
|
||||
});
|
||||
|
||||
const allUsers = computed(() => usersStore.allUsers);
|
||||
|
||||
onMounted(async () => {
|
||||
documentTitle.set(i18n.baseText('settings.users'));
|
||||
|
||||
@@ -39,7 +42,7 @@ onMounted(async () => {
|
||||
}
|
||||
});
|
||||
|
||||
const usersListActions = computed((): IUserListAction[] => {
|
||||
const usersListActions = computed((): Array<UserAction<IUser>> => {
|
||||
return [
|
||||
{
|
||||
label: i18n.baseText('settings.users.actions.copyInviteLink'),
|
||||
@@ -284,12 +287,12 @@ async function onRoleChange(user: IUser, newRoleName: UpdateGlobalRolePayload['n
|
||||
<!-- If there's more than 1 user it means the account quota was more than 1 in the past. So we need to allow instance owner to be able to delete users and transfer workflows.
|
||||
-->
|
||||
<div
|
||||
v-if="usersStore.usersLimitNotReached || usersStore.allUsers.length > 1"
|
||||
v-if="usersStore.usersLimitNotReached || allUsers.length > 1"
|
||||
:class="$style.usersContainer"
|
||||
>
|
||||
<n8n-users-list
|
||||
:actions="usersListActions"
|
||||
:users="usersStore.allUsers"
|
||||
:users="allUsers"
|
||||
:current-user-id="usersStore.currentUserId"
|
||||
:is-saml-login-enabled="ssoStore.isSamlLoginEnabled"
|
||||
@action="onUsersListAction"
|
||||
|
||||
@@ -134,8 +134,16 @@ onMounted(async () => {
|
||||
<div :class="$style.mainContent">
|
||||
<div v-if="loading || isFullTemplatesCollection(collection)" :class="$style.markdown">
|
||||
<n8n-markdown
|
||||
:content="isFullTemplatesCollection(collection) && collection.description"
|
||||
:images="isFullTemplatesCollection(collection) && collection.image"
|
||||
:content="
|
||||
isFullTemplatesCollection(collection) && collection.description
|
||||
? collection.description
|
||||
: ''
|
||||
"
|
||||
:images="
|
||||
isFullTemplatesCollection(collection) && collection.image
|
||||
? collection.image
|
||||
: undefined
|
||||
"
|
||||
:loading="loading"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -14,11 +14,8 @@ import { useUsersStore } from '@/stores/users.store';
|
||||
import { computed, onMounted, ref, useTemplateRef } from 'vue';
|
||||
import { useRoute, useRouter, type LocationQueryRaw } from 'vue-router';
|
||||
|
||||
import ResourcesListLayout, {
|
||||
type BaseFilters,
|
||||
type Resource,
|
||||
type VariableResource,
|
||||
} from '@/components/layouts/ResourcesListLayout.vue';
|
||||
import ResourcesListLayout from '@/components/layouts/ResourcesListLayout.vue';
|
||||
import type { BaseFilters, Resource, VariableResource } from '@/Interface';
|
||||
|
||||
import { usePageRedirectionHelper } from '@/composables/usePageRedirectionHelper';
|
||||
import { EnterpriseEditionFeature, MODAL_CONFIRM } from '@/constants';
|
||||
@@ -35,6 +32,7 @@ import {
|
||||
import { uid } from '@n8n/design-system/utils';
|
||||
import { useAsyncState } from '@vueuse/core';
|
||||
import pickBy from 'lodash/pickBy';
|
||||
import type { ComponentExposed } from 'vue-component-type-helpers';
|
||||
|
||||
const settingsStore = useSettingsStore();
|
||||
const environmentsStore = useEnvironmentsStore();
|
||||
@@ -47,7 +45,7 @@ const sourceControlStore = useSourceControlStore();
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
const layoutRef = useTemplateRef<InstanceType<typeof ResourcesListLayout>>('layoutRef');
|
||||
const layoutRef = useTemplateRef<ComponentExposed<typeof ResourcesListLayout>>('layoutRef');
|
||||
|
||||
const { showError } = useToast();
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ import { telemetry } from '@/plugins/telemetry';
|
||||
import { useRootStore } from '@n8n/stores/useRootStore';
|
||||
import { getResourcePermissions } from '@/permissions';
|
||||
import { usePageRedirectionHelper } from '@/composables/usePageRedirectionHelper';
|
||||
import type { IUser } from 'n8n-workflow';
|
||||
|
||||
type WorkflowHistoryActionRecord = {
|
||||
[K in Uppercase<WorkflowHistoryActionTypes[number]>]: Lowercase<K>;
|
||||
@@ -72,7 +73,7 @@ const editorRoute = computed(() => ({
|
||||
const workflowPermissions = computed(
|
||||
() => getResourcePermissions(workflowsStore.getWorkflowById(workflowId.value)?.scopes).workflow,
|
||||
);
|
||||
const actions = computed<UserAction[]>(() =>
|
||||
const actions = computed<Array<UserAction<IUser>>>(() =>
|
||||
workflowHistoryActionTypes.map((value) => ({
|
||||
label: i18n.baseText(`workflowHistory.item.actions.${value}`),
|
||||
disabled:
|
||||
|
||||
@@ -7,7 +7,7 @@ import type {
|
||||
Resource,
|
||||
SortingAndPaginationUpdates,
|
||||
WorkflowResource,
|
||||
} from '@/components/layouts/ResourcesListLayout.vue';
|
||||
} from '@/Interface';
|
||||
import ResourcesListLayout from '@/components/layouts/ResourcesListLayout.vue';
|
||||
import ProjectHeader from '@/components/Projects/ProjectHeader.vue';
|
||||
import WorkflowCard from '@/components/WorkflowCard.vue';
|
||||
@@ -33,7 +33,6 @@ import InsightsSummary from '@/features/insights/components/InsightsSummary.vue'
|
||||
import { useInsightsStore } from '@/features/insights/insights.store';
|
||||
import type {
|
||||
FolderListItem,
|
||||
IUser,
|
||||
UserAction,
|
||||
WorkflowListItem,
|
||||
WorkflowListResource,
|
||||
@@ -64,7 +63,7 @@ import {
|
||||
import type { PathItem } from '@n8n/design-system/components/N8nBreadcrumbs/Breadcrumbs.vue';
|
||||
import { createEventBus } from '@n8n/utils/event-bus';
|
||||
import debounce from 'lodash/debounce';
|
||||
import { PROJECT_ROOT } from 'n8n-workflow';
|
||||
import { type IUser, PROJECT_ROOT } from 'n8n-workflow';
|
||||
import { useTemplateRef, computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
||||
import { type LocationQueryRaw, useRoute, useRouter } from 'vue-router';
|
||||
|
||||
@@ -148,7 +147,7 @@ const showCardsBadge = ref(false);
|
||||
* 'onlyAvailableOn' is used to specify where the action should be available, if not specified it will be available on both
|
||||
*/
|
||||
const folderActions = computed<
|
||||
Array<UserAction & { onlyAvailableOn?: 'mainBreadcrumbs' | 'card' }>
|
||||
Array<UserAction<IUser> & { onlyAvailableOn?: 'mainBreadcrumbs' | 'card' }>
|
||||
>(() => [
|
||||
{
|
||||
label: i18n.baseText('generic.open'),
|
||||
@@ -183,16 +182,18 @@ const folderActions = computed<
|
||||
},
|
||||
]);
|
||||
|
||||
const folderCardActions = computed(() =>
|
||||
folderActions.value.filter(
|
||||
(action) => !action.onlyAvailableOn || action.onlyAvailableOn === 'card',
|
||||
),
|
||||
const folderCardActions = computed(
|
||||
(): Array<UserAction<IUser>> =>
|
||||
folderActions.value.filter(
|
||||
(action) => !action.onlyAvailableOn || action.onlyAvailableOn === 'card',
|
||||
),
|
||||
);
|
||||
|
||||
const mainBreadcrumbsActions = computed(() =>
|
||||
folderActions.value.filter(
|
||||
(action) => !action.onlyAvailableOn || action.onlyAvailableOn === 'mainBreadcrumbs',
|
||||
),
|
||||
const mainBreadcrumbsActions = computed(
|
||||
(): Array<UserAction<IUser>> =>
|
||||
folderActions.value.filter(
|
||||
(action) => !action.onlyAvailableOn || action.onlyAvailableOn === 'mainBreadcrumbs',
|
||||
),
|
||||
);
|
||||
|
||||
const readOnlyEnv = computed(() => sourceControlStore.preferences.branchReadOnly);
|
||||
@@ -1859,7 +1860,7 @@ const onNameSubmit = async (name: string) => {
|
||||
:personal-project="personalProject"
|
||||
resource-type="workflows"
|
||||
/>
|
||||
<n8n-action-box
|
||||
<N8nActionBox
|
||||
v-else-if="currentFolder"
|
||||
data-test-id="empty-folder-action-box"
|
||||
:heading="
|
||||
@@ -1878,7 +1879,7 @@ const onNameSubmit = async (name: string) => {
|
||||
? i18n.baseText('readOnlyEnv.cantAdd.workflow')
|
||||
: i18n.baseText('generic.missing.permissions')
|
||||
}}
|
||||
</template></n8n-action-box
|
||||
</template></N8nActionBox
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -73,6 +73,10 @@ const plugins = [
|
||||
components({
|
||||
dts: './src/components.d.ts',
|
||||
resolvers: [
|
||||
(componentName) => {
|
||||
if (componentName.startsWith('N8n'))
|
||||
return { name: componentName, from: '@n8n/design-system' };
|
||||
},
|
||||
iconsResolver({
|
||||
prefix: 'icon',
|
||||
}),
|
||||
|
||||
Reference in New Issue
Block a user