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:
Alex Grozav
2025-06-24 14:01:23 +03:00
committed by GitHub
parent 21ff173070
commit 20c63436d2
150 changed files with 1332 additions and 960 deletions

View File

@@ -8,6 +8,7 @@
"files": { "files": {
"ignore": [ "ignore": [
"**/.turbo", "**/.turbo",
"**/components.d.ts",
"**/coverage", "**/coverage",
"**/dist", "**/dist",
"**/package.json", "**/package.json",

View File

@@ -26,3 +26,6 @@ coverage
*.njsproj *.njsproj
*.sln *.sln
*.sw? *.sw?
# Auto-generated files
src/components.d.ts

View File

@@ -12,8 +12,8 @@
"typecheck": "vue-tsc --noEmit", "typecheck": "vue-tsc --noEmit",
"lint": "eslint . --ext .js,.ts,.vue --quiet", "lint": "eslint . --ext .js,.ts,.vue --quiet",
"lintfix": "eslint . --ext .js,.ts,.vue --fix", "lintfix": "eslint . --ext .js,.ts,.vue --fix",
"format": "biome format --write src .storybook && prettier --write src/ --ignore-path ../../.prettierignore", "format": "biome format --write src .storybook && prettier --write src/ --ignore-path ../../../../.prettierignore",
"format:check": "biome ci src .storybook && prettier --check src/ --ignore-path ../../.prettierignore", "format:check": "biome ci src .storybook && prettier --check src/ --ignore-path ../../../../.prettierignore",
"storybook": "storybook dev -p 6006 --no-open", "storybook": "storybook dev -p 6006 --no-open",
"build:storybook": "storybook build" "build:storybook": "storybook build"
}, },
@@ -36,6 +36,7 @@
} }
}, },
"dependencies": { "dependencies": {
"@n8n/design-system": "workspace:*",
"@vueuse/core": "catalog:frontend", "@vueuse/core": "catalog:frontend",
"highlight.js": "catalog:frontend", "highlight.js": "catalog:frontend",
"markdown-it-link-attributes": "^4.0.1", "markdown-it-link-attributes": "^4.0.1",

View File

@@ -4,7 +4,8 @@ import hljsJavascript from 'highlight.js/lib/languages/javascript';
import hljsXML from 'highlight.js/lib/languages/xml'; import hljsXML from 'highlight.js/lib/languages/xml';
import { computed, onMounted } from 'vue'; import { computed, onMounted } from 'vue';
import { Chat, ChatWindow } from '@n8n/chat/components'; import Chat from '@n8n/chat/components/Chat.vue';
import ChatWindow from '@n8n/chat/components/ChatWindow.vue';
import { useOptions } from '@n8n/chat/composables'; import { useOptions } from '@n8n/chat/composables';
defineProps({}); defineProps({});

View File

@@ -105,7 +105,7 @@ onMounted(async () => {
<template> <template>
<div ref="messageContainer" class="chat-message" :class="classes"> <div ref="messageContainer" class="chat-message" :class="classes">
<div v-if="$slots.beforeMessage" class="chat-message-actions"> <div v-if="!!$slots.beforeMessage" class="chat-message-actions">
<slot name="beforeMessage" v-bind="{ message }" /> <slot name="beforeMessage" v-bind="{ message }" />
</div> </div>
<slot> <slot>

View File

@@ -1,4 +1,5 @@
<script lang="ts" setup> <script lang="ts" setup>
import { N8nIcon, N8nText } from '@n8n/design-system';
import { ref, watch } from 'vue'; import { ref, watch } from 'vue';
import Message from '@n8n/chat/components/Message.vue'; import Message from '@n8n/chat/components/Message.vue';

View File

@@ -1 +1,17 @@
/// <reference types="vite/client" /> /// <reference types="vite/client" />
declare module 'markdown-it-task-lists' {
import type { PluginWithOptions } from 'markdown-it';
declare namespace markdownItTaskLists {
interface Config {
enabled?: boolean;
label?: boolean;
labelAfter?: boolean;
}
}
declare const markdownItTaskLists: PluginWithOptions<markdownItTaskLists.Config>;
export = markdownItTaskLists;
}

View File

@@ -1,6 +0,0 @@
declare module '*.vue' {
import { defineComponent } from 'vue';
const component: ReturnType<typeof defineComponent>;
export default component;
}

View File

@@ -6,6 +6,7 @@
"baseUrl": "src", "baseUrl": "src",
"target": "esnext", "target": "esnext",
"module": "esnext", "module": "esnext",
"moduleResolution": "bundler",
"allowJs": true, "allowJs": true,
"importHelpers": true, "importHelpers": true,
"incremental": false, "incremental": false,
@@ -13,7 +14,8 @@
"resolveJsonModule": true, "resolveJsonModule": true,
"types": ["vitest/globals"], "types": ["vitest/globals"],
"paths": { "paths": {
"@n8n/chat/*": ["./*"] "@n8n/chat/*": ["./*"],
"@n8n/design-system*": ["../../design-system/src*"]
}, },
"lib": ["esnext", "dom", "dom.iterable", "scripthost"], "lib": ["esnext", "dom", "dom.iterable", "scripthost"],
// TODO: remove all options below this line // TODO: remove all options below this line

View File

@@ -5,9 +5,12 @@ import vue from '@vitejs/plugin-vue';
import icons from 'unplugin-icons/vite'; import icons from 'unplugin-icons/vite';
import dts from 'vite-plugin-dts'; import dts from 'vite-plugin-dts';
import { vitestConfig } from '@n8n/vitest-config/frontend'; import { vitestConfig } from '@n8n/vitest-config/frontend';
import iconsResolver from 'unplugin-icons/resolver';
import components from 'unplugin-vue-components/vite';
const includeVue = process.env.INCLUDE_VUE === 'true'; const includeVue = process.env.INCLUDE_VUE === 'true';
const srcPath = resolve(__dirname, 'src'); const srcPath = resolve(__dirname, 'src');
const packagesDir = resolve(__dirname, '..', '..', '..');
// https://vitejs.dev/config/ // https://vitejs.dev/config/
export default mergeConfig( export default mergeConfig(
@@ -19,6 +22,18 @@ export default mergeConfig(
autoInstall: true, autoInstall: true,
}), }),
dts(), dts(),
components({
dts: './src/components.d.ts',
resolvers: [
(componentName) => {
if (componentName.startsWith('N8n'))
return { name: componentName, from: '@n8n/design-system' };
},
iconsResolver({
prefix: 'icon',
}),
],
}),
{ {
name: 'rename-css-file', name: 'rename-css-file',
closeBundle() { closeBundle() {
@@ -36,10 +51,24 @@ export default mergeConfig(
}, },
], ],
resolve: { resolve: {
alias: { alias: [
'@': srcPath, {
'@n8n/chat': srcPath, find: '@',
}, replacement: srcPath,
},
{
find: '@n8n/chat',
replacement: srcPath,
},
{
find: /^@n8n\/chat(.+)$/,
replacement: srcPath + '$1',
},
{
find: /^@n8n\/design-system(.+)$/,
replacement: resolve(packagesDir, 'frontend', '@n8n', 'design-system', 'src$1'),
},
],
}, },
define: { define: {
'process.env.NODE_ENV': process.env.NODE_ENV ? `"${process.env.NODE_ENV}"` : '"development"', 'process.env.NODE_ENV': process.env.NODE_ENV ? `"${process.env.NODE_ENV}"` : '"development"',

View File

@@ -16,6 +16,7 @@ module.exports = {
'@typescript-eslint/no-unsafe-member-access': 'warn', '@typescript-eslint/no-unsafe-member-access': 'warn',
'@typescript-eslint/prefer-optional-chain': 'warn', '@typescript-eslint/prefer-optional-chain': 'warn',
'@typescript-eslint/prefer-nullish-coalescing': 'warn', '@typescript-eslint/prefer-nullish-coalescing': 'warn',
'vue/no-undef-components': 'error',
}, },
overrides: [ overrides: [

View File

@@ -8,6 +8,7 @@
"clean": "rimraf dist .turbo", "clean": "rimraf dist .turbo",
"build": "vite build", "build": "vite build",
"typecheck": "vue-tsc --noEmit", "typecheck": "vue-tsc --noEmit",
"typecheck:watch": "vue-tsc --watch --noEmit",
"test": "vitest run", "test": "vitest run",
"test:dev": "vitest", "test:dev": "vitest",
"build:storybook": "storybook build", "build:storybook": "storybook build",

View File

@@ -18,6 +18,9 @@ import AssistantLoadingMessage from '../AskAssistantLoadingMessage/AssistantLoad
import AssistantText from '../AskAssistantText/AssistantText.vue'; import AssistantText from '../AskAssistantText/AssistantText.vue';
import BetaTag from '../BetaTag/BetaTag.vue'; import BetaTag from '../BetaTag/BetaTag.vue';
import InlineAskAssistantButton from '../InlineAskAssistantButton/InlineAskAssistantButton.vue'; import InlineAskAssistantButton from '../InlineAskAssistantButton/InlineAskAssistantButton.vue';
import N8nButton from '../N8nButton';
import N8nIcon from '../N8nIcon';
import N8nIconButton from '../N8nIconButton';
const { t } = useI18n(); const { t } = useI18n();
@@ -124,7 +127,7 @@ function onSubmitFeedback(feedback: string) {
<slot name="header" /> <slot name="header" />
</div> </div>
<div :class="$style.back" data-test-id="close-chat-button" @click="onClose"> <div :class="$style.back" data-test-id="close-chat-button" @click="onClose">
<n8n-icon icon="arrow-right" color="text-base" /> <N8nIcon icon="arrow-right" color="text-base" />
</div> </div>
</div> </div>
<div :class="$style.body"> <div :class="$style.body">
@@ -222,14 +225,14 @@ function onSubmitFeedback(feedback: string) {
{{ t('assistantChat.quickRepliesTitle') }} {{ t('assistantChat.quickRepliesTitle') }}
</div> </div>
<div v-for="opt in message.quickReplies" :key="opt.type" data-test-id="quick-replies"> <div v-for="opt in message.quickReplies" :key="opt.type" data-test-id="quick-replies">
<n8n-button <N8nButton
v-if="opt.text" v-if="opt.text"
type="secondary" type="secondary"
size="mini" size="mini"
@click="() => onQuickReply(opt)" @click="() => onQuickReply(opt)"
> >
{{ opt.text }} {{ opt.text }}
</n8n-button> </N8nButton>
</div> </div>
</div> </div>
</data> </data>
@@ -289,10 +292,10 @@ function onSubmitFeedback(feedback: string) {
@input.prevent="growInput" @input.prevent="growInput"
@keydown.stop @keydown.stop
/> />
<n8n-icon-button <N8nIconButton
:class="{ [$style.sendButton]: true }" :class="{ [$style.sendButton]: true }"
icon="paper-plane" icon="paper-plane"
type="text" :text="true"
size="large" size="large"
data-test-id="send-message-button" data-test-id="send-message-button"
:disabled="sendDisabled" :disabled="sendDisabled"

View File

@@ -69,6 +69,8 @@ exports[`AskAssistantChat > does not render retry button if no error is present
<n8n-icon-stub <n8n-icon-stub
color="text-base" color="text-base"
icon="arrow-right" icon="arrow-right"
size="medium"
spin="false"
/> />
</div> </div>
</div> </div>
@@ -175,12 +177,16 @@ exports[`AskAssistantChat > does not render retry button if no error is present
wrap="hard" wrap="hard"
/> />
<n8n-icon-button-stub <n8n-icon-button-stub
active="false"
class="sendButton" class="sendButton"
data-test-id="send-message-button" data-test-id="send-message-button"
disabled="true" disabled="true"
icon="paper-plane" icon="paper-plane"
loading="false"
outline="false"
size="large" size="large"
type="text" text="true"
type="primary"
/> />
</div> </div>
@@ -257,6 +263,8 @@ exports[`AskAssistantChat > renders chat with messages correctly 1`] = `
<n8n-icon-stub <n8n-icon-stub
color="text-base" color="text-base"
icon="arrow-right" icon="arrow-right"
size="medium"
spin="false"
/> />
</div> </div>
</div> </div>
@@ -601,11 +609,18 @@ exports[`AskAssistantChat > renders chat with messages correctly 1`] = `
class="actions" class="actions"
> >
<n8n-button-stub <n8n-button-stub
active="false"
block="false"
data-test-id="replace-code-button" data-test-id="replace-code-button"
disabled="false" disabled="false"
element="button"
icon="refresh" icon="refresh"
label=""
loading="false" loading="false"
outline="false"
size="mini" size="mini"
square="false"
text="false"
type="primary" type="primary"
/> />
</div> </div>
@@ -625,8 +640,9 @@ exports[`AskAssistantChat > renders chat with messages correctly 1`] = `
> >
<n8n-avatar-stub <n8n-avatar-stub
first-name="Kobi" colors="--color-primary,--color-secondary,--color-avatar-accent-1,--color-avatar-accent-2,--color-primary-tint-1"
last-name="Dog" firstname="Kobi"
lastname="Dog"
size="xsmall" size="xsmall"
/> />
<span> <span>
@@ -897,11 +913,18 @@ Testing more code
class="actions" class="actions"
> >
<n8n-button-stub <n8n-button-stub
active="false"
block="false"
data-test-id="replace-code-button" data-test-id="replace-code-button"
disabled="false" disabled="false"
element="button"
icon="refresh" icon="refresh"
label=""
loading="false" loading="false"
outline="false"
size="mini" size="mini"
square="false"
text="false"
type="primary" type="primary"
/> />
</div> </div>
@@ -921,7 +944,16 @@ Testing more code
data-test-id="quick-replies" data-test-id="quick-replies"
> >
<n8n-button-stub <n8n-button-stub
active="false"
block="false"
disabled="false"
element="button"
label=""
loading="false"
outline="false"
size="mini" size="mini"
square="false"
text="false"
type="secondary" type="secondary"
/> />
</div> </div>
@@ -929,7 +961,16 @@ Testing more code
data-test-id="quick-replies" data-test-id="quick-replies"
> >
<n8n-button-stub <n8n-button-stub
active="false"
block="false"
disabled="false"
element="button"
label=""
loading="false"
outline="false"
size="mini" size="mini"
square="false"
text="false"
type="secondary" type="secondary"
/> />
</div> </div>
@@ -954,12 +995,16 @@ Testing more code
wrap="hard" wrap="hard"
/> />
<n8n-icon-button-stub <n8n-icon-button-stub
active="false"
class="sendButton" class="sendButton"
data-test-id="send-message-button" data-test-id="send-message-button"
disabled="true" disabled="true"
icon="paper-plane" icon="paper-plane"
loading="false"
outline="false"
size="large" size="large"
type="text" text="true"
type="primary"
/> />
</div> </div>
@@ -1036,6 +1081,8 @@ exports[`AskAssistantChat > renders default placeholder chat correctly 1`] = `
<n8n-icon-stub <n8n-icon-stub
color="text-base" color="text-base"
icon="arrow-right" icon="arrow-right"
size="medium"
spin="false"
/> />
</div> </div>
</div> </div>
@@ -1135,12 +1182,16 @@ exports[`AskAssistantChat > renders default placeholder chat correctly 1`] = `
wrap="hard" wrap="hard"
/> />
<n8n-icon-button-stub <n8n-icon-button-stub
active="false"
class="sendButton" class="sendButton"
data-test-id="send-message-button" data-test-id="send-message-button"
disabled="true" disabled="true"
icon="paper-plane" icon="paper-plane"
loading="false"
outline="false"
size="large" size="large"
type="text" text="true"
type="primary"
/> />
</div> </div>
@@ -1217,6 +1268,8 @@ exports[`AskAssistantChat > renders end of session chat correctly 1`] = `
<n8n-icon-stub <n8n-icon-stub
color="text-base" color="text-base"
icon="arrow-right" icon="arrow-right"
size="medium"
spin="false"
/> />
</div> </div>
</div> </div>
@@ -1400,12 +1453,16 @@ exports[`AskAssistantChat > renders end of session chat correctly 1`] = `
wrap="hard" wrap="hard"
/> />
<n8n-icon-button-stub <n8n-icon-button-stub
active="false"
class="sendButton" class="sendButton"
data-test-id="send-message-button" data-test-id="send-message-button"
disabled="true" disabled="true"
icon="paper-plane" icon="paper-plane"
loading="false"
outline="false"
size="large" size="large"
type="text" text="true"
type="primary"
/> />
</div> </div>
@@ -1482,6 +1539,8 @@ exports[`AskAssistantChat > renders error message correctly with retry button 1`
<n8n-icon-stub <n8n-icon-stub
color="text-base" color="text-base"
icon="arrow-right" icon="arrow-right"
size="medium"
spin="false"
/> />
</div> </div>
</div> </div>
@@ -1558,13 +1617,23 @@ exports[`AskAssistantChat > renders error message correctly with retry button 1`
class="errorIcon" class="errorIcon"
icon="exclamation-triangle" icon="exclamation-triangle"
size="small" size="small"
spin="false"
/> />
This is an error message. This is an error message.
</p> </p>
<n8n-button-stub <n8n-button-stub
active="false"
block="false"
class="retryButton" class="retryButton"
data-test-id="error-retry-button" data-test-id="error-retry-button"
disabled="false"
element="button"
label=""
loading="false"
outline="false"
size="mini" size="mini"
square="false"
text="false"
type="secondary" type="secondary"
/> />
</div> </div>
@@ -1590,12 +1659,16 @@ exports[`AskAssistantChat > renders error message correctly with retry button 1`
wrap="hard" wrap="hard"
/> />
<n8n-icon-button-stub <n8n-icon-button-stub
active="false"
class="sendButton" class="sendButton"
data-test-id="send-message-button" data-test-id="send-message-button"
disabled="true" disabled="true"
icon="paper-plane" icon="paper-plane"
loading="false"
outline="false"
size="large" size="large"
type="text" text="true"
type="primary"
/> />
</div> </div>
@@ -1672,6 +1745,8 @@ exports[`AskAssistantChat > renders message with code snippet 1`] = `
<n8n-icon-stub <n8n-icon-stub
color="text-base" color="text-base"
icon="arrow-right" icon="arrow-right"
size="medium"
spin="false"
/> />
</div> </div>
</div> </div>
@@ -1848,12 +1923,16 @@ catch(e) {
wrap="hard" wrap="hard"
/> />
<n8n-icon-button-stub <n8n-icon-button-stub
active="false"
class="sendButton" class="sendButton"
data-test-id="send-message-button" data-test-id="send-message-button"
disabled="true" disabled="true"
icon="paper-plane" icon="paper-plane"
loading="false"
outline="false"
size="large" size="large"
type="text" text="true"
type="primary"
/> />
</div> </div>
@@ -1930,6 +2009,8 @@ exports[`AskAssistantChat > renders streaming chat correctly 1`] = `
<n8n-icon-stub <n8n-icon-stub
color="text-base" color="text-base"
icon="arrow-right" icon="arrow-right"
size="medium"
spin="false"
/> />
</div> </div>
</div> </div>
@@ -2039,12 +2120,16 @@ exports[`AskAssistantChat > renders streaming chat correctly 1`] = `
wrap="hard" wrap="hard"
/> />
<n8n-icon-button-stub <n8n-icon-button-stub
active="false"
class="sendButton" class="sendButton"
data-test-id="send-message-button" data-test-id="send-message-button"
disabled="true" disabled="true"
icon="paper-plane" icon="paper-plane"
loading="false"
outline="false"
size="large" size="large"
type="text" text="true"
type="primary"
/> />
</div> </div>

View File

@@ -4,6 +4,7 @@ import { computed } from 'vue';
import { useI18n } from '../../../composables/useI18n'; import { useI18n } from '../../../composables/useI18n';
import type { ChatUI } from '../../../types/assistant'; import type { ChatUI } from '../../../types/assistant';
import AssistantAvatar from '../../AskAssistantAvatar/AssistantAvatar.vue'; import AssistantAvatar from '../../AskAssistantAvatar/AssistantAvatar.vue';
import N8nAvatar from '../../N8nAvatar';
interface Props { interface Props {
message: ChatUI.AssistantMessage; message: ChatUI.AssistantMessage;
@@ -27,7 +28,7 @@ const isUserMessage = computed(() => props.message.role === 'user');
:class="{ [$style.roleName]: true, [$style.userSection]: !isUserMessage }" :class="{ [$style.roleName]: true, [$style.userSection]: !isUserMessage }"
> >
<template v-if="isUserMessage"> <template v-if="isUserMessage">
<n8n-avatar :first-name="user?.firstName" :last-name="user?.lastName" size="xsmall" /> <N8nAvatar :first-name="user?.firstName" :last-name="user?.lastName" size="xsmall" />
<span>{{ t('assistantChat.you') }}</span> <span>{{ t('assistantChat.you') }}</span>
</template> </template>
<template v-else> <template v-else>

View File

@@ -2,6 +2,8 @@
import BaseMessage from './BaseMessage.vue'; import BaseMessage from './BaseMessage.vue';
import { useI18n } from '../../../composables/useI18n'; import { useI18n } from '../../../composables/useI18n';
import type { ChatUI } from '../../../types/assistant'; import type { ChatUI } from '../../../types/assistant';
import N8nButton from '../../N8nButton';
import N8nIcon from '../../N8nIcon';
interface Props { interface Props {
message: ChatUI.ErrorMessage & { id: string; read: boolean }; message: ChatUI.ErrorMessage & { id: string; read: boolean };
@@ -20,10 +22,10 @@ const { t } = useI18n();
<BaseMessage :message="message" :is-first-of-role="isFirstOfRole" :user="user"> <BaseMessage :message="message" :is-first-of-role="isFirstOfRole" :user="user">
<div :class="$style.error" data-test-id="chat-message-system"> <div :class="$style.error" data-test-id="chat-message-system">
<p :class="$style.errorText"> <p :class="$style.errorText">
<n8n-icon icon="exclamation-triangle" size="small" :class="$style.errorIcon" /> <N8nIcon icon="exclamation-triangle" size="small" :class="$style.errorIcon" />
{{ message.content }} {{ message.content }}
</p> </p>
<n8n-button <N8nButton
v-if="message.retry" v-if="message.retry"
type="secondary" type="secondary"
size="mini" size="mini"
@@ -32,7 +34,7 @@ const { t } = useI18n();
@click="() => message.retry?.()" @click="() => message.retry?.()"
> >
{{ t('generic.retry') }} {{ t('generic.retry') }}
</n8n-button> </N8nButton>
</div> </div>
</BaseMessage> </BaseMessage>
</template> </template>

View File

@@ -6,6 +6,7 @@ import { useMarkdown } from './useMarkdown';
import { useI18n } from '../../../composables/useI18n'; import { useI18n } from '../../../composables/useI18n';
import type { ChatUI } from '../../../types/assistant'; import type { ChatUI } from '../../../types/assistant';
import BlinkingCursor from '../../BlinkingCursor/BlinkingCursor.vue'; import BlinkingCursor from '../../BlinkingCursor/BlinkingCursor.vue';
import N8nButton from '../../N8nButton';
interface Props { interface Props {
message: ChatUI.TextMessage & { id: string; read: boolean; quickReplies?: ChatUI.QuickReply[] }; message: ChatUI.TextMessage & { id: string; read: boolean; quickReplies?: ChatUI.QuickReply[] };
@@ -55,15 +56,15 @@ async function onCopyButtonClick(content: string, e: MouseEvent) {
data-test-id="assistant-code-snippet" data-test-id="assistant-code-snippet"
> >
<header v-if="isClipboardSupported"> <header v-if="isClipboardSupported">
<n8n-button <N8nButton
type="tertiary" type="tertiary"
text="true" :text="true"
size="mini" size="mini"
data-test-id="assistant-copy-snippet-button" data-test-id="assistant-copy-snippet-button"
@click="onCopyButtonClick(message.codeSnippet, $event)" @click="onCopyButtonClick(message.codeSnippet, $event)"
> >
{{ t('assistantChat.copy') }} {{ t('assistantChat.copy') }}
</n8n-button> </N8nButton>
</header> </header>
<div <div
v-n8n-html="renderMarkdown(message.codeSnippet).trim()" v-n8n-html="renderMarkdown(message.codeSnippet).trim()"

View File

@@ -5,6 +5,8 @@ import { useI18n } from '@n8n/design-system/composables/useI18n';
import BaseWorkflowMessage from './BaseWorkflowMessage.vue'; import BaseWorkflowMessage from './BaseWorkflowMessage.vue';
import type { ChatUI } from '../../../../types/assistant'; import type { ChatUI } from '../../../../types/assistant';
import N8nButton from '../../../N8nButton';
import N8nInput from '../../../N8nInput';
interface Props { interface Props {
message: ChatUI.RateWorkflowMessage & { id: string; read: boolean }; message: ChatUI.RateWorkflowMessage & { id: string; read: boolean };
@@ -48,7 +50,7 @@ function onSubmitFeedback() {
<div :class="$style.content"> <div :class="$style.content">
<p v-if="!showSuccess">{{ message.content }}</p> <p v-if="!showSuccess">{{ message.content }}</p>
<div v-if="!showFeedback && !showSuccess" :class="$style.buttons"> <div v-if="!showFeedback && !showSuccess" :class="$style.buttons">
<n8n-button <N8nButton
type="secondary" type="secondary"
size="small" size="small"
:label="t('assistantChat.builder.thumbsUp')" :label="t('assistantChat.builder.thumbsUp')"
@@ -56,7 +58,7 @@ function onSubmitFeedback() {
icon="thumbs-up" icon="thumbs-up"
@click="onRateButton('thumbsUp')" @click="onRateButton('thumbsUp')"
/> />
<n8n-button <N8nButton
type="secondary" type="secondary"
size="small" size="small"
data-test-id="message-thumbs-down-button" data-test-id="message-thumbs-down-button"
@@ -66,7 +68,7 @@ function onSubmitFeedback() {
/> />
</div> </div>
<div v-if="showFeedback" :class="$style.feedbackTextArea"> <div v-if="showFeedback" :class="$style.feedbackTextArea">
<n8n-input <N8nInput
v-model="feedback" v-model="feedback"
:class="$style.feedbackInput" :class="$style.feedbackInput"
type="textarea" type="textarea"
@@ -77,7 +79,7 @@ function onSubmitFeedback() {
:rows="5" :rows="5"
/> />
<div :class="$style.feedbackTextArea__footer"> <div :class="$style.feedbackTextArea__footer">
<n8n-button <N8nButton
native-type="submit" native-type="submit"
type="secondary" type="secondary"
size="small" size="small"
@@ -85,7 +87,7 @@ function onSubmitFeedback() {
@click="onSubmitFeedback" @click="onSubmitFeedback"
> >
{{ t('assistantChat.builder.submit') }} {{ t('assistantChat.builder.submit') }}
</n8n-button> </N8nButton>
</div> </div>
</div> </div>

View File

@@ -4,6 +4,9 @@ import { computed } from 'vue';
import { useI18n } from '@n8n/design-system/composables/useI18n'; import { useI18n } from '@n8n/design-system/composables/useI18n';
import N8nButton from '../N8nButton';
import N8nIcon from '../N8nIcon';
const MIN_LINES = 4; const MIN_LINES = 4;
interface Props { interface Props {
@@ -106,11 +109,11 @@ const diffs = computed(() => {
</div> </div>
<div :class="$style.actions"> <div :class="$style.actions">
<div v-if="error"> <div v-if="error">
<n8n-icon icon="exclamation-triangle" color="danger" class="mr-5xs" /> <N8nIcon icon="exclamation-triangle" color="danger" class="mr-5xs" />
<span :class="$style.infoText">{{ t('codeDiff.couldNotReplace') }}</span> <span :class="$style.infoText">{{ t('codeDiff.couldNotReplace') }}</span>
</div> </div>
<div v-else-if="replaced"> <div v-else-if="replaced">
<n8n-button <N8nButton
type="secondary" type="secondary"
size="mini" size="mini"
icon="undo" icon="undo"
@@ -118,13 +121,13 @@ const diffs = computed(() => {
@click="() => emit('undo')" @click="() => emit('undo')"
> >
{{ t('codeDiff.undo') }} {{ t('codeDiff.undo') }}
</n8n-button> </N8nButton>
<n8n-icon icon="check" color="success" class="ml-xs" /> <N8nIcon icon="check" color="success" class="ml-xs" />
<span :class="$style.infoText" data-test-id="code-replaced-message"> <span :class="$style.infoText" data-test-id="code-replaced-message">
{{ t('codeDiff.codeReplaced') }} {{ t('codeDiff.codeReplaced') }}
</span> </span>
</div> </div>
<n8n-button <N8nButton
v-else v-else
:type="replacing ? 'secondary' : 'primary'" :type="replacing ? 'secondary' : 'primary'"
size="mini" size="mini"
@@ -133,7 +136,7 @@ const diffs = computed(() => {
:disabled="!content || streaming" :disabled="!content || streaming"
:loading="replacing" :loading="replacing"
@click="() => emit('replace')" @click="() => emit('replace')"
>{{ replacing ? t('codeDiff.replacing') : t('codeDiff.replaceMyCode') }}</n8n-button >{{ replacing ? t('codeDiff.replacing') : t('codeDiff.replaceMyCode') }}</N8nButton
> >
</div> </div>
</div> </div>

View File

@@ -271,11 +271,18 @@ exports[`CodeDiff > renders code diff correctly 1`] = `
class="actions" class="actions"
> >
<n8n-button-stub <n8n-button-stub
active="false"
block="false"
data-test-id="replace-code-button" data-test-id="replace-code-button"
disabled="false" disabled="false"
element="button"
icon="refresh" icon="refresh"
label=""
loading="false" loading="false"
outline="false"
size="mini" size="mini"
square="false"
text="false"
type="primary" type="primary"
/> />
</div> </div>
@@ -537,6 +544,8 @@ exports[`CodeDiff > renders error state correctly 1`] = `
class="mr-5xs" class="mr-5xs"
color="danger" color="danger"
icon="exclamation-triangle" icon="exclamation-triangle"
size="medium"
spin="false"
/> />
<span <span
class="infoText" class="infoText"
@@ -800,15 +809,26 @@ exports[`CodeDiff > renders replaced code diff correctly 1`] = `
> >
<div> <div>
<n8n-button-stub <n8n-button-stub
active="false"
block="false"
data-test-id="undo-replace-button" data-test-id="undo-replace-button"
disabled="false"
element="button"
icon="undo" icon="undo"
label=""
loading="false"
outline="false"
size="mini" size="mini"
square="false"
text="false"
type="secondary" type="secondary"
/> />
<n8n-icon-stub <n8n-icon-stub
class="ml-xs" class="ml-xs"
color="success" color="success"
icon="check" icon="check"
size="medium"
spin="false"
/> />
<span <span
class="infoText" class="infoText"
@@ -1072,11 +1092,18 @@ exports[`CodeDiff > renders replacing code diff correctly 1`] = `
class="actions" class="actions"
> >
<n8n-button-stub <n8n-button-stub
active="false"
block="false"
data-test-id="replace-code-button" data-test-id="replace-code-button"
disabled="false" disabled="false"
element="button"
icon="refresh" icon="refresh"
label=""
loading="true" loading="true"
outline="false"
size="mini" size="mini"
square="false"
text="false"
type="secondary" type="secondary"
/> />
</div> </div>

View File

@@ -8,13 +8,13 @@ import N8nHeading from '../N8nHeading';
import N8nText from '../N8nText'; import N8nText from '../N8nText';
interface ActionBoxProps { interface ActionBoxProps {
emoji: string; emoji?: string;
heading: string; heading?: string;
buttonText?: string; buttonText?: string;
buttonType?: ButtonType; buttonType?: ButtonType;
buttonDisabled?: boolean; buttonDisabled?: boolean;
buttonIcon?: string; buttonIcon?: string;
description: string; description?: string;
calloutText?: string; calloutText?: string;
calloutTheme?: CalloutTheme; calloutTheme?: CalloutTheme;
calloutIcon?: string; calloutIcon?: string;

View File

@@ -8,10 +8,11 @@
import { ElDropdown, ElDropdownMenu, ElDropdownItem, type Placement } from 'element-plus'; import { ElDropdown, ElDropdownMenu, ElDropdownItem, type Placement } from 'element-plus';
import { ref, useCssModule, useAttrs, computed } from 'vue'; import { ref, useCssModule, useAttrs, computed } from 'vue';
import type { IconSize } from '@n8n/design-system/types/icon'; import type { ActionDropdownItem, IconSize, ButtonSize } from '@n8n/design-system/types';
import type { ActionDropdownItem } from '../../types'; import N8nBadge from '../N8nBadge';
import N8nIcon from '../N8nIcon'; import N8nIcon from '../N8nIcon';
import N8nIconButton from '../N8nIconButton';
import { N8nKeyboardShortcut } from '../N8nKeyboardShortcut'; import { N8nKeyboardShortcut } from '../N8nKeyboardShortcut';
const TRIGGER = ['click', 'hover'] as const; const TRIGGER = ['click', 'hover'] as const;
@@ -20,7 +21,7 @@ interface ActionDropdownProps {
items: ActionDropdownItem[]; items: ActionDropdownItem[];
placement?: Placement; placement?: Placement;
activatorIcon?: string; activatorIcon?: string;
activatorSize?: IconSize; activatorSize?: ButtonSize;
iconSize?: IconSize; iconSize?: IconSize;
trigger?: (typeof TRIGGER)[number]; trigger?: (typeof TRIGGER)[number];
hideArrow?: boolean; hideArrow?: boolean;
@@ -96,7 +97,7 @@ defineExpose({ open, close });
@visible-change="onVisibleChange" @visible-change="onVisibleChange"
> >
<slot v-if="$slots.activator" name="activator" /> <slot v-if="$slots.activator" name="activator" />
<n8n-icon-button <N8nIconButton
v-else v-else
type="tertiary" type="tertiary"
text text

View File

@@ -1,17 +1,18 @@
<script lang="ts" setup> <script lang="ts" setup generic="UserType extends IUser, Actions extends UserAction<UserType>[]">
import { ElDropdown, ElDropdownMenu, ElDropdownItem, type Placement } from 'element-plus'; import { ElDropdown, ElDropdownMenu, ElDropdownItem, type Placement } from 'element-plus';
import { ref } from 'vue'; import { ref } from 'vue';
import type { UserAction } from '@n8n/design-system/types'; import type { IUser, UserAction } from '@n8n/design-system/types';
import type { IconOrientation, IconSize } from '@n8n/design-system/types/icon'; import type { IconOrientation, IconSize } from '@n8n/design-system/types/icon';
import N8nIcon from '../N8nIcon'; import N8nIcon from '../N8nIcon';
import N8nLoading from '../N8nLoading';
const SIZE = ['mini', 'small', 'medium'] as const; const SIZE = ['mini', 'small', 'medium'] as const;
const THEME = ['default', 'dark'] as const; const THEME = ['default', 'dark'] as const;
interface ActionToggleProps { interface ActionToggleProps<UserType extends IUser, Actions extends Array<UserAction<UserType>>> {
actions?: UserAction[]; actions?: Actions;
placement?: Placement; placement?: Placement;
size?: (typeof SIZE)[number]; size?: (typeof SIZE)[number];
iconSize?: IconSize; iconSize?: IconSize;
@@ -24,8 +25,10 @@ interface ActionToggleProps {
trigger?: 'click' | 'hover'; trigger?: 'click' | 'hover';
} }
type ActionValue = Actions[number]['value'];
defineOptions({ name: 'N8nActionToggle' }); defineOptions({ name: 'N8nActionToggle' });
withDefaults(defineProps<ActionToggleProps>(), { withDefaults(defineProps<ActionToggleProps<UserType, Array<UserAction<UserType>>>>(), {
actions: () => [], actions: () => [],
placement: 'bottom', placement: 'bottom',
size: 'medium', size: 'medium',
@@ -42,9 +45,9 @@ withDefaults(defineProps<ActionToggleProps>(), {
const actionToggleRef = ref<InstanceType<typeof ElDropdown> | null>(null); const actionToggleRef = ref<InstanceType<typeof ElDropdown> | null>(null);
const emit = defineEmits<{ const emit = defineEmits<{
action: [value: string]; action: [value: ActionValue];
'visible-change': [value: boolean]; 'visible-change': [value: boolean];
'item-mouseup': [action: UserAction]; 'item-mouseup': [action: UserAction<UserType>];
}>(); }>();
const onCommand = (value: string) => emit('action', value); const onCommand = (value: string) => emit('action', value);
@@ -57,7 +60,7 @@ const openActionToggle = (isOpen: boolean) => {
} }
}; };
const onActionMouseUp = (action: UserAction) => { const onActionMouseUp = (action: UserAction<UserType>) => {
emit('item-mouseup', action); emit('item-mouseup', action);
actionToggleRef.value?.handleClose(); actionToggleRef.value?.handleClose();
}; };

View File

@@ -5,8 +5,8 @@ import Avatar from 'vue-boring-avatars';
import { getInitials } from '../../utils/labelUtil'; import { getInitials } from '../../utils/labelUtil';
interface AvatarProps { interface AvatarProps {
firstName?: string; firstName?: string | null;
lastName?: string; lastName?: string | null;
size?: 'xsmall' | 'small' | 'medium' | 'large'; size?: 'xsmall' | 'small' | 'medium' | 'large';
colors?: string[]; colors?: string[];
} }

View File

@@ -1,20 +1,10 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { TextSize } from '@n8n/design-system/types/text'; import type { TextSize, BadgeTheme } from '@n8n/design-system/types/';
import N8nText from '../N8nText'; import N8nText from '../N8nText';
const THEME = [
'default',
'success',
'warning',
'danger',
'primary',
'secondary',
'tertiary',
] as const;
interface BadgeProps { interface BadgeProps {
theme?: (typeof THEME)[number]; theme?: BadgeTheme;
size?: TextSize; size?: TextSize;
bold?: boolean; bold?: boolean;
showBorder?: boolean; showBorder?: boolean;

View File

@@ -1,6 +1,6 @@
import type { StoryFn } from '@storybook/vue3'; import type { StoryFn } from '@storybook/vue3';
import type { UserAction } from '@n8n/design-system/types'; import type { IUser, UserAction } from '@n8n/design-system/types';
import AsyncLoadingCacheDemo from './AsyncLoadingCacheDemo.vue'; import AsyncLoadingCacheDemo from './AsyncLoadingCacheDemo.vue';
import Breadcrumbs from './Breadcrumbs.vue'; import Breadcrumbs from './Breadcrumbs.vue';
@@ -125,7 +125,7 @@ SyncLoadingCacheTest.args = {
title: '[Demo] This will update the hidden items every time dropdown is opened', title: '[Demo] This will update the hidden items every time dropdown is opened',
}; };
const testActions: UserAction[] = [ const testActions: Array<UserAction<IUser>> = [
{ label: 'Create Folder', value: 'action1', disabled: false }, { label: 'Create Folder', value: 'action1', disabled: false },
{ label: 'Create Workflow', value: 'action2', disabled: false }, { label: 'Create Workflow', value: 'action2', disabled: false },
{ label: 'Rename', value: 'action3', disabled: false }, { label: 'Rename', value: 'action3', disabled: false },

View File

@@ -1,9 +1,13 @@
<script lang="ts" setup> <script lang="ts" setup generic="UserType extends IUser">
import { computed, ref, watch } from 'vue'; import { computed, ref, watch } from 'vue';
import type { UserAction } from '@n8n/design-system/types'; import type { IUser, UserAction } from '@n8n/design-system/types';
import N8nActionToggle from '../N8nActionToggle';
import N8nLink from '../N8nLink';
import N8nLoading from '../N8nLoading'; import N8nLoading from '../N8nLoading';
import N8nText from '../N8nText';
import N8nTooltip from '../N8nTooltip';
export type PathItem = { export type PathItem = {
id: string; id: string;
@@ -66,7 +70,7 @@ const dropdownDisabled = computed(() => {
return props.pathTruncated && !hasHiddenItems.value; return props.pathTruncated && !hasHiddenItems.value;
}); });
const hiddenItemActions = computed((): UserAction[] => { const hiddenItemActions = computed((): Array<UserAction<UserType>> => {
return loadedHiddenItems.value.map((item) => ({ return loadedHiddenItems.value.map((item) => ({
value: item.id, value: item.id,
label: item.label, label: item.label,
@@ -132,7 +136,7 @@ const emitItemHover = (id: string) => {
emit('itemHover', item); emit('itemHover', item);
}; };
const onHiddenItemMouseUp = (item: UserAction) => { const onHiddenItemMouseUp = (item: UserAction<UserType>) => {
const pathItem = [...props.items, ...loadedHiddenItems.value].find((i) => i.id === item.value); const pathItem = [...props.items, ...loadedHiddenItems.value].find((i) => i.id === item.value);
if (!pathItem || !props.dragActive) { if (!pathItem || !props.dragActive) {
return; return;
@@ -177,16 +181,13 @@ const handleTooltipClose = () => {
> >
<!-- Show interactive dropdown for larger versions --> <!-- Show interactive dropdown for larger versions -->
<div v-if="props.theme !== 'small'" :class="$style['hidden-items-menu']"> <div v-if="props.theme !== 'small'" :class="$style['hidden-items-menu']">
<n8n-action-toggle <N8nActionToggle
:actions="hiddenItemActions" :actions="hiddenItemActions"
:loading="isLoadingHiddenItems" :loading="isLoadingHiddenItems"
:loading-row-count="loadingSkeletonRows" :loading-row-count="loadingSkeletonRows"
:disabled="dropdownDisabled" :disabled="dropdownDisabled"
:class="$style['action-toggle']" :class="$style['action-toggle']"
:popper-class="{ :popper-class="`${$style['hidden-items-menu-popper']} ${dragActive ? $style.dragging : ''}`"
[$style['hidden-items-menu-popper']]: true,
[$style.dragging]: dragActive,
}"
:trigger="hiddenItemsTrigger" :trigger="hiddenItemsTrigger"
theme="dark" theme="dark"
placement="bottom" placement="bottom"
@@ -197,11 +198,11 @@ const handleTooltipClose = () => {
@action="emitItemSelected" @action="emitItemSelected"
@item-mouseup="onHiddenItemMouseUp" @item-mouseup="onHiddenItemMouseUp"
> >
<n8n-text :bold="true" :class="$style.dots">...</n8n-text> <N8nText :bold="true" :class="$style.dots">...</N8nText>
</n8n-action-toggle> </N8nActionToggle>
</div> </div>
<!-- Just a tooltip for smaller versions --> <!-- Just a tooltip for smaller versions -->
<n8n-tooltip <N8nTooltip
v-else v-else
:popper-class="$style.tooltip" :popper-class="$style.tooltip"
:disabled="dropdownDisabled" :disabled="dropdownDisabled"
@@ -222,12 +223,12 @@ const handleTooltipClose = () => {
</div> </div>
<div v-else :class="$style.tooltipContent"> <div v-else :class="$style.tooltipContent">
<div data-test-id="hidden-items-tooltip"> <div data-test-id="hidden-items-tooltip">
<n8n-text>{{ loadedHiddenItems.map((item) => item.label).join(' / ') }}</n8n-text> <N8nText>{{ loadedHiddenItems.map((item) => item.label).join(' / ') }}</N8nText>
</div> </div>
</div> </div>
</template> </template>
<span :class="$style['tooltip-ellipsis']">...</span> <span :class="$style['tooltip-ellipsis']">...</span>
</n8n-tooltip> </N8nTooltip>
</li> </li>
<li v-if="showEllipsis" :class="$style.separator">{{ separator }}</li> <li v-if="showEllipsis" :class="$style.separator">{{ separator }}</li>
<template v-for="(item, index) in items" :key="item.id"> <template v-for="(item, index) in items" :key="item.id">
@@ -245,8 +246,8 @@ const handleTooltipClose = () => {
@mouseenter="emitItemHover(item.id)" @mouseenter="emitItemHover(item.id)"
@mouseup="onItemMouseUp(item)" @mouseup="onItemMouseUp(item)"
> >
<n8n-link v-if="item.href" :href="item.href" theme="text">{{ item.label }}</n8n-link> <N8nLink v-if="item.href" :href="item.href" theme="text">{{ item.label }}</N8nLink>
<n8n-text v-else>{{ item.label }}</n8n-text> <N8nText v-else>{{ item.label }}</N8nText>
</li> </li>
<li v-if="index !== items.length - 1" :class="$style.separator"> <li v-if="index !== items.length - 1" :class="$style.separator">
{{ separator }} {{ separator }}

View File

@@ -1,6 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, useAttrs, useCssModule, watchEffect } from 'vue'; import { computed, useAttrs, useCssModule, watchEffect } from 'vue';
import type { IconSize } from '@n8n/design-system/types';
import type { ButtonProps } from '@n8n/design-system/types/button'; import type { ButtonProps } from '@n8n/design-system/types/button';
import N8nIcon from '../N8nIcon'; import N8nIcon from '../N8nIcon';
@@ -34,7 +35,10 @@ const ariaBusy = computed(() => (props.loading ? 'true' : undefined));
const ariaDisabled = computed(() => (props.disabled ? 'true' : undefined)); const ariaDisabled = computed(() => (props.disabled ? 'true' : undefined));
const isDisabled = computed(() => props.disabled || props.loading); const isDisabled = computed(() => props.disabled || props.loading);
const iconSize = computed(() => props.iconSize ?? (props.size === 'mini' ? 'xsmall' : props.size)); const iconSize = computed(
(): IconSize | undefined =>
props.iconSize ?? (props.size === 'xmini' || props.size === 'mini' ? 'xsmall' : props.size),
);
const classes = computed(() => { const classes = computed(() => {
return ( return (

View File

@@ -1,14 +1,11 @@
<script lang="ts" setup> <script lang="ts" setup>
import { computed, useCssModule } from 'vue'; import { computed, useCssModule } from 'vue';
import type { IconSize } from '@n8n/design-system/types/icon'; import type { IconSize, CalloutTheme } from '@n8n/design-system/types';
import N8nIcon from '../N8nIcon'; import N8nIcon from '../N8nIcon';
import N8nText from '../N8nText'; import N8nText from '../N8nText';
const THEMES = ['info', 'success', 'secondary', 'warning', 'danger', 'custom'] as const;
export type CalloutTheme = (typeof THEMES)[number];
const CALLOUT_DEFAULT_ICONS: Record<string, string> = { const CALLOUT_DEFAULT_ICONS: Record<string, string> = {
info: 'info-circle', info: 'info-circle',
success: 'check-circle', success: 'check-circle',

View File

@@ -1,3 +1,3 @@
import N8nCallout from './Callout.vue'; import N8nCallout from './Callout.vue';
export type { CalloutTheme } from './Callout.vue'; export type { CalloutTheme } from '../../types';
export default N8nCallout; export default N8nCallout;

View File

@@ -32,7 +32,7 @@ import type {
Updater, Updater,
} from '@tanstack/vue-table'; } from '@tanstack/vue-table';
import { createColumnHelper, FlexRender, getCoreRowModel, useVueTable } from '@tanstack/vue-table'; import { createColumnHelper, FlexRender, getCoreRowModel, useVueTable } from '@tanstack/vue-table';
import { ElCheckbox } from 'element-plus'; import { ElCheckbox, ElOption, ElSelect, ElSkeletonItem } from 'element-plus';
import get from 'lodash/get'; import get from 'lodash/get';
import { computed, h, ref, shallowRef, useSlots, watch } from 'vue'; import { computed, h, ref, shallowRef, useSlots, watch } from 'vue';
@@ -428,7 +428,7 @@ const table = useVueTable({
:key="coll.id" :key="coll.id"
class="el-skeleton is-animated" class="el-skeleton is-animated"
> >
<el-skeleton-item /> <ElSkeletonItem />
</td> </td>
</tr> </tr>
</template> </template>
@@ -470,14 +470,14 @@ const table = useVueTable({
</N8nPagination> </N8nPagination>
<div class="table-pagination__sizes"> <div class="table-pagination__sizes">
<div class="table-pagination__sizes__label">Page size</div> <div class="table-pagination__sizes__label">Page size</div>
<el-select <ElSelect
v-model.number="itemsPerPage" v-model.number="itemsPerPage"
class="table-pagination__sizes__select" class="table-pagination__sizes__select"
size="small" size="small"
:teleported="false" :teleported="false"
> >
<el-option v-for="item in pageSizes" :key="item" :label="item" :value="item" /> <ElOption v-for="item in pageSizes" :key="item" :label="item" :value="item" />
</el-select> </ElSelect>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -1,4 +1,4 @@
<script lang="ts" setup> <script lang="ts" setup generic="Item extends DatatableRow">
import { computed, ref } from 'vue'; import { computed, ref } from 'vue';
import { useI18n } from '../../composables/useI18n'; import { useI18n } from '../../composables/useI18n';
@@ -13,7 +13,7 @@ const ALL_ROWS = -1;
interface DatatableProps { interface DatatableProps {
columns: DatatableColumn[]; columns: DatatableColumn[];
rows: DatatableRow[]; rows: Item[];
currentPage?: number; currentPage?: number;
pagination?: boolean; pagination?: boolean;
rowsPerPage?: number; rowsPerPage?: number;
@@ -69,7 +69,7 @@ function onRowsPerPageChange(value: number) {
} }
} }
function getTdValue(row: DatatableRow, column: DatatableColumn) { function getTdValue(row: Item, column: DatatableColumn) {
return getValueByPath<DatatableRowDataType>(row, column.path); return getValueByPath<DatatableRowDataType>(row, column.path);
} }

View File

@@ -1,5 +1,10 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { IFormInput } from '@n8n/design-system/types'; import type {
FormFieldValue,
IFormInput,
FormFieldValueUpdate,
FormValues,
} from '@n8n/design-system/types';
import { createFormEventBus } from '../../utils'; import { createFormEventBus } from '../../utils';
import N8nButton from '../N8nButton'; import N8nButton from '../N8nButton';
@@ -17,12 +22,10 @@ interface FormBoxProps {
redirectLink?: string; redirectLink?: string;
} }
type Value = string | number | boolean | null | undefined;
defineOptions({ name: 'N8nFormBox' }); defineOptions({ name: 'N8nFormBox' });
withDefaults(defineProps<FormBoxProps>(), { withDefaults(defineProps<FormBoxProps>(), {
title: '', title: '',
inputs: () => [], inputs: (): IFormInput[] => [],
buttonLoading: false, buttonLoading: false,
redirectText: '', redirectText: '',
redirectLink: '', redirectLink: '',
@@ -30,13 +33,13 @@ withDefaults(defineProps<FormBoxProps>(), {
const formBus = createFormEventBus(); const formBus = createFormEventBus();
const emit = defineEmits<{ const emit = defineEmits<{
submit: [value: { [key: string]: Value }]; submit: [value: FormValues];
update: [value: { name: string; value: Value }]; update: [value: FormFieldValueUpdate];
secondaryClick: [value: Event]; secondaryClick: [value: Event];
}>(); }>();
const onUpdateModelValue = (e: { name: string; value: Value }) => emit('update', e); const onUpdateModelValue = (e: { name: string; value: FormFieldValue }) => emit('update', e);
const onSubmit = (e: { [key: string]: Value }) => emit('submit', e); const onSubmit = (e: { [key: string]: FormFieldValue }) => emit('submit', e);
const onButtonClick = () => formBus.emit('submit'); const onButtonClick = () => formBus.emit('submit');
const onSecondaryButtonClick = (event: Event) => emit('secondaryClick', event); const onSecondaryButtonClick = (event: Event) => emit('secondaryClick', event);
</script> </script>

View File

@@ -1,6 +1,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import { ElSwitch } from 'element-plus'; import { ElSwitch } from 'element-plus';
import { computed, reactive, onMounted, ref, watch, useSlots } from 'vue'; import { computed, reactive, onMounted, ref, watch } from 'vue';
import { getValidationError, VALIDATORS } from './validators'; import { getValidationError, VALIDATORS } from './validators';
import { t } from '../../locale'; import { t } from '../../locale';
@@ -19,6 +19,7 @@ import type {
import N8nCheckbox from '../N8nCheckbox'; import N8nCheckbox from '../N8nCheckbox';
import N8nInput from '../N8nInput'; import N8nInput from '../N8nInput';
import N8nInputLabel from '../N8nInputLabel'; import N8nInputLabel from '../N8nInputLabel';
import N8nLink from '../N8nLink';
import N8nOption from '../N8nOption'; import N8nOption from '../N8nOption';
import N8nSelect from '../N8nSelect'; import N8nSelect from '../N8nSelect';
@@ -77,8 +78,6 @@ const state = reactive({
isTyping: false, isTyping: false,
}); });
const slots = useSlots();
const inputRef = ref<HTMLTextAreaElement | null>(null); const inputRef = ref<HTMLTextAreaElement | null>(null);
function getInputValidationError(): ReturnType<IValidator['validate']> { function getInputValidationError(): ReturnType<IValidator['validate']> {
@@ -160,8 +159,6 @@ const validationError = computed<{ message: string } | null>(() => {
return null; return null;
}); });
const hasDefaultSlot = computed(() => !!slots.default);
const showErrors = computed( const showErrors = computed(
() => () =>
!!validationError.value && !!validationError.value &&
@@ -217,7 +214,7 @@ defineExpose({ inputRef });
:size="labelSize" :size="labelSize"
> >
<div :class="showErrors ? $style.errorInput : ''" @keydown.stop @keydown.enter.exact="onEnter"> <div :class="showErrors ? $style.errorInput : ''" @keydown.stop @keydown.enter.exact="onEnter">
<slot v-if="hasDefaultSlot" /> <slot v-if="$slots.default" />
<N8nSelect <N8nSelect
v-else-if="type === 'select' || type === 'multi-select'" v-else-if="type === 'select' || type === 'multi-select'"
ref="inputRef" ref="inputRef"
@@ -261,7 +258,7 @@ defineExpose({ inputRef });
</div> </div>
<div v-if="showErrors" :class="$style.errors"> <div v-if="showErrors" :class="$style.errors">
<span v-text="validationError?.message" /> <span v-text="validationError?.message" />
<n8n-link <N8nLink
v-if="documentationUrl && documentationText" v-if="documentationUrl && documentationText"
:to="documentationUrl" :to="documentationUrl"
:new-window="true" :new-window="true"
@@ -269,7 +266,7 @@ defineExpose({ inputRef });
theme="danger" theme="danger"
> >
{{ documentationText }} {{ documentationText }}
</n8n-link> </N8nLink>
</div> </div>
<div v-else-if="infoText" :class="$style.infoText"> <div v-else-if="infoText" :class="$style.infoText">
<span size="small" v-text="infoText" /> <span size="small" v-text="infoText" />

View File

@@ -1,24 +1,22 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, onMounted, reactive, ref, watch } from 'vue'; import { computed, onMounted, reactive, ref, watch } from 'vue';
import type { IFormInput } from '../../types'; import type { FormFieldValue, IFormInput, FormFieldValueUpdate, FormValues } from '../../types';
import type { FormEventBus } from '../../utils'; import type { FormEventBus } from '../../utils';
import { createFormEventBus } from '../../utils'; import { createFormEventBus } from '../../utils';
import N8nFormInput from '../N8nFormInput'; import N8nFormInput from '../N8nFormInput';
import N8nText from '../N8nText';
import ResizeObserver from '../ResizeObserver'; import ResizeObserver from '../ResizeObserver';
export type FormInputsProps = { export interface FormInputsProps {
inputs?: IFormInput[]; inputs: IFormInput[];
eventBus?: FormEventBus; eventBus?: FormEventBus;
columnView?: boolean; columnView?: boolean;
verticalSpacing?: '' | 'xs' | 's' | 'm' | 'l' | 'xl'; verticalSpacing?: '' | 'xs' | 's' | 'm' | 'l' | 'xl';
teleported?: boolean; teleported?: boolean;
}; }
type Value = string | number | boolean | null | undefined;
const props = withDefaults(defineProps<FormInputsProps>(), { const props = withDefaults(defineProps<FormInputsProps>(), {
inputs: () => [],
eventBus: createFormEventBus, eventBus: createFormEventBus,
columnView: false, columnView: false,
verticalSpacing: '', verticalSpacing: '',
@@ -26,14 +24,14 @@ const props = withDefaults(defineProps<FormInputsProps>(), {
}); });
const emit = defineEmits<{ const emit = defineEmits<{
update: [value: { name: string; value: Value }]; update: [value: FormFieldValueUpdate];
'update:modelValue': [value: Record<string, Value>]; 'update:modelValue': [value: FormValues];
submit: [value: Record<string, Value>]; submit: [value: FormValues];
ready: [value: boolean]; ready: [value: boolean];
}>(); }>();
const showValidationWarnings = ref(false); const showValidationWarnings = ref(false);
const values = reactive<Record<string, Value>>({}); const values = reactive<FormValues>({});
const validity = ref<Record<string, boolean>>({}); const validity = ref<Record<string, boolean>>({});
const filteredInputs = computed(() => { const filteredInputs = computed(() => {
@@ -50,7 +48,7 @@ watch(isReadyToSubmit, (ready) => {
emit('ready', ready); emit('ready', ready);
}); });
function onUpdateModelValue(name: string, value: Value) { function onUpdateModelValue(name: string, value: FormFieldValue) {
values[name] = value; values[name] = value;
emit('update', { name, value }); emit('update', { name, value });
emit('update:modelValue', values); emit('update:modelValue', values);
@@ -76,12 +74,15 @@ function onSubmit() {
return; return;
} }
const toSubmit = filteredInputs.value.reduce<Record<string, Value>>((valuesToSubmit, input) => { const toSubmit = filteredInputs.value.reduce<Record<string, FormFieldValue>>(
if (values[input.name]) { (valuesToSubmit, input) => {
valuesToSubmit[input.name] = values[input.name]; if (values[input.name]) {
} valuesToSubmit[input.name] = values[input.name];
return valuesToSubmit; }
}, {}); return valuesToSubmit;
},
{},
);
emit('submit', toSubmit); emit('submit', toSubmit);
} }
@@ -108,7 +109,7 @@ onMounted(() => {
:key="input.name" :key="input.name"
:class="{ [`mt-${verticalSpacing}`]: verticalSpacing && index > 0 }" :class="{ [`mt-${verticalSpacing}`]: verticalSpacing && index > 0 }"
> >
<n8n-text <N8nText
v-if="input.properties.type === 'info'" v-if="input.properties.type === 'info'"
color="text-base" color="text-base"
tag="div" tag="div"
@@ -117,7 +118,7 @@ onMounted(() => {
class="form-text" class="form-text"
> >
{{ input.properties.label }} {{ input.properties.label }}
</n8n-text> </N8nText>
<N8nFormInput <N8nFormInput
v-else v-else
v-bind="input.properties" v-bind="input.properties"
@@ -127,7 +128,7 @@ onMounted(() => {
:data-test-id="input.name" :data-test-id="input.name"
:show-validation-warnings="showValidationWarnings" :show-validation-warnings="showValidationWarnings"
:teleported="teleported" :teleported="teleported"
@update:model-value="(value: Value) => onUpdateModelValue(input.name, value)" @update:model-value="(value: FormFieldValue) => onUpdateModelValue(input.name, value)"
@validate="(value: boolean) => onValidate(input.name, value)" @validate="(value: boolean) => onValidate(input.name, value)"
@enter="onSubmit" @enter="onSubmit"
/> />

View File

@@ -60,6 +60,7 @@ describe('IconPicker', () => {
global: { global: {
plugins: [router], plugins: [router],
components, components,
stubs: ['N8nButton'],
}, },
}); });
const TEST_EMOJI_COUNT = 1962; const TEST_EMOJI_COUNT = 1962;
@@ -90,11 +91,12 @@ describe('IconPicker', () => {
global: { global: {
plugins: [router], plugins: [router],
components, components,
stubs: ['N8nButton'],
}, },
}); });
await userEvent.hover(getByTestId('icon-picker-button')); await userEvent.hover(getByTestId('icon-picker-button'));
expect(getByRole('tooltip').textContent).toBe(TOOLTIP); expect(getByRole('tooltip').textContent).toBe(TOOLTIP);
expect(getByTestId('icon-picker-button').dataset.icon).toBe(ICON); expect(getByTestId('icon-picker-button')).toHaveAttribute('icon', ICON);
}); });
it('renders emoji as default icon correctly', async () => { it('renders emoji as default icon correctly', async () => {
const ICON = '🔥'; const ICON = '🔥';
@@ -124,6 +126,7 @@ describe('IconPicker', () => {
global: { global: {
plugins: [router], plugins: [router],
components, components,
stubs: ['N8nButton'],
}, },
}); });
expect(queryByTestId('tab-icons')).not.toBeInTheDocument(); expect(queryByTestId('tab-icons')).not.toBeInTheDocument();
@@ -138,13 +141,14 @@ describe('IconPicker', () => {
global: { global: {
plugins: [router], plugins: [router],
components, components,
stubs: ['N8nButton'],
}, },
}); });
await fireEvent.click(getByTestId('icon-picker-button')); await fireEvent.click(getByTestId('icon-picker-button'));
// Select the first icon // Select the first icon
await fireEvent.click(getAllByTestId('icon-picker-icon')[0]); await fireEvent.click(getAllByTestId('icon-picker-icon')[0]);
// Icon should be selected and popup should be closed // Icon should be selected and popup should be closed
expect(getByTestId('icon-picker-button').dataset.icon).toBe(TEST_ICONS[0]); expect(getByTestId('icon-picker-button')).toHaveAttribute('icon', TEST_ICONS[0]);
expect(queryByTestId('icon-picker-popup')).toBeNull(); expect(queryByTestId('icon-picker-popup')).toBeNull();
expect(emitted()).toHaveProperty('update:modelValue'); expect(emitted()).toHaveProperty('update:modelValue');
// Should emit the selected icon // Should emit the selected icon

View File

@@ -6,6 +6,10 @@ import { isEmojiSupported } from 'is-emoji-supported';
import { ref, computed } from 'vue'; import { ref, computed } from 'vue';
import { useI18n } from '../../composables/useI18n'; import { useI18n } from '../../composables/useI18n';
import N8nButton from '../N8nButton';
import N8nIcon from '../N8nIcon';
import N8nIconButton from '../N8nIconButton';
import N8nTabs from '../N8nTabs';
import N8nTooltip from '../N8nTooltip'; import N8nTooltip from '../N8nTooltip';
/** /**

View File

@@ -6,13 +6,14 @@ import type { IconColor } from '@n8n/design-system/types/icon';
import N8nIcon from '../N8nIcon'; import N8nIcon from '../N8nIcon';
import N8nText from '../N8nText'; import N8nText from '../N8nText';
import N8nTooltip from '../N8nTooltip';
interface IAccordionItem { export interface IAccordionItem {
id: string; id: string;
label: string; label: string;
icon: string; icon: string;
iconColor?: IconColor; iconColor?: IconColor;
tooltip?: string; tooltip?: string | null;
} }
interface InfoAccordionProps { interface InfoAccordionProps {
@@ -69,12 +70,12 @@ const onTooltipClick = (item: string, event: MouseEvent) => emit('tooltipClick',
<!-- Info accordion can display list of items with icons or just a HTML description --> <!-- Info accordion can display list of items with icons or just a HTML description -->
<div v-if="items.length > 0" :class="$style.accordionItems"> <div v-if="items.length > 0" :class="$style.accordionItems">
<div v-for="item in items" :key="item.id" :class="$style.accordionItem"> <div v-for="item in items" :key="item.id" :class="$style.accordionItem">
<n8n-tooltip :disabled="!item.tooltip"> <N8nTooltip :disabled="!item.tooltip">
<template #content> <template #content>
<div v-n8n-html="item.tooltip" @click="onTooltipClick(item.id, $event)"></div> <div v-n8n-html="item.tooltip" @click="onTooltipClick(item.id, $event)"></div>
</template> </template>
<N8nIcon :icon="item.icon" :color="item.iconColor" size="small" class="mr-2xs" /> <N8nIcon :icon="item.icon" :color="item.iconColor" size="small" class="mr-2xs" />
</n8n-tooltip> </N8nTooltip>
<N8nText size="small" color="text-base">{{ item.label }}</N8nText> <N8nText size="small" color="text-base">{{ item.label }}</N8nText>
</div> </div>
</div> </div>

View File

@@ -8,7 +8,7 @@ import type { InputSize, InputType } from '@n8n/design-system/types/input';
import { uid } from '../../utils'; import { uid } from '../../utils';
interface InputProps { interface InputProps {
modelValue?: string | number; modelValue?: string | number | null;
type?: InputType; type?: InputType;
size?: InputSize; size?: InputSize;
placeholder?: string; placeholder?: string;

View File

@@ -12,7 +12,7 @@ import { escapeMarkdown, toggleCheckbox } from '../../utils/markdown';
import N8nLoading from '../N8nLoading'; import N8nLoading from '../N8nLoading';
interface IImage { interface IImage {
id: string; id: string | number;
url: string; url: string;
} }
@@ -24,7 +24,7 @@ interface Options {
} }
interface MarkdownProps { interface MarkdownProps {
content?: string; content?: string | null;
withMultiBreaks?: boolean; withMultiBreaks?: boolean;
images?: IImage[]; images?: IImage[];
loading?: boolean; loading?: boolean;

View File

@@ -1,6 +1,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import { ElSubMenu, ElMenuItem } from 'element-plus'; import { ElSubMenu, ElMenuItem } from 'element-plus';
import { computed, useCssModule } from 'vue'; import { computed, useCssModule, getCurrentInstance } from 'vue';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import { doesMenuItemMatchCurrentRoute } from './routerUtil'; import { doesMenuItemMatchCurrentRoute } from './routerUtil';
@@ -8,6 +8,7 @@ import type { IMenuItem } from '../../types';
import { getInitials } from '../../utils/labelUtil'; import { getInitials } from '../../utils/labelUtil';
import ConditionalRouterLink from '../ConditionalRouterLink'; import ConditionalRouterLink from '../ConditionalRouterLink';
import N8nIcon from '../N8nIcon'; import N8nIcon from '../N8nIcon';
import N8nSpinner from '../N8nSpinner';
import N8nTooltip from '../N8nTooltip'; import N8nTooltip from '../N8nTooltip';
interface MenuItemProps { interface MenuItemProps {
@@ -61,6 +62,9 @@ const isItemActive = (item: IMenuItem): boolean => {
Array.isArray(item.children) && item.children.some((child) => isActive(child)); Array.isArray(item.children) && item.children.some((child) => isActive(child));
return isActive(item) || hasActiveChild; return isActive(item) || hasActiveChild;
}; };
// Get self component to avoid dependency cycle
const N8nMenuItem = getCurrentInstance()?.type;
</script> </script>
<template> <template>

View File

@@ -1,12 +1,14 @@
<script lang="ts" setup> <script lang="ts" setup>
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
import type { Placement } from 'element-plus'; import type { Placement } from 'element-plus';
import { computed } from 'vue'; import { computed, getCurrentInstance } from 'vue';
import N8nTooltip from '../N8nTooltip'; import N8nTooltip from '../N8nTooltip';
type IconType = 'file' | 'icon' | 'unknown';
interface NodeIconProps { interface NodeIconProps {
type: 'file' | 'icon' | 'unknown'; type: IconType;
src?: string; src?: string;
name?: string; name?: string;
nodeTypeName?: string; nodeTypeName?: string;
@@ -16,7 +18,7 @@ interface NodeIconProps {
color?: string; color?: string;
showTooltip?: boolean; showTooltip?: boolean;
tooltipPosition?: Placement; tooltipPosition?: Placement;
badge?: { src: string; type: string }; badge?: { src: string; type: IconType };
} }
const props = withDefaults(defineProps<NodeIconProps>(), { const props = withDefaults(defineProps<NodeIconProps>(), {
@@ -69,6 +71,9 @@ const badgeStyleData = computed((): Record<string, string> => {
bottom: `-${Math.floor(size / 2)}px`, bottom: `-${Math.floor(size / 2)}px`,
}; };
}); });
// Get self component to avoid dependency cycle
const N8nNodeIcon = getCurrentInstance()?.type;
</script> </script>
<template> <template>
@@ -97,7 +102,7 @@ const badgeStyleData = computed((): Record<string, string> => {
<img v-if="type === 'file'" :src="src" :class="$style.nodeIconImage" /> <img v-if="type === 'file'" :src="src" :class="$style.nodeIconImage" />
<FontAwesomeIcon v-else :icon="`${name}`" :style="fontStyleData" /> <FontAwesomeIcon v-else :icon="`${name}`" :style="fontStyleData" />
<div v-if="badge" :class="$style.badge" :style="badgeStyleData"> <div v-if="badge" :class="$style.badge" :style="badgeStyleData">
<n8n-node-icon :type="badge.type" :src="badge.src" :size="badgeSize"></n8n-node-icon> <N8nNodeIcon :type="badge.type" :src="badge.src" :size="badgeSize" />
</div> </div>
</div> </div>
<div v-else :class="$style.nodeIconPlaceholder"> <div v-else :class="$style.nodeIconPlaceholder">

View File

@@ -1,4 +1,4 @@
<script lang="ts" setup generic="Value extends string"> <script lang="ts" setup generic="Value extends string | boolean">
import RadioButton from './RadioButton.vue'; import RadioButton from './RadioButton.vue';
interface RadioOption { interface RadioOption {
@@ -47,8 +47,9 @@ const onClick = (
> >
<RadioButton <RadioButton
v-for="option in options" v-for="option in options"
:key="option.value" :key="`${option.value}`"
v-bind="option" v-bind="option"
:value="`${option.value}`"
:active="modelValue === option.value" :active="modelValue === option.value"
:size="size" :size="size"
:disabled="disabled || option.disabled" :disabled="disabled || option.disabled"

View File

@@ -1,11 +1,13 @@
<script lang="ts" setup> <script lang="ts" setup generic="Key extends string, Item extends ItemWithKey<Key>">
import type { ComponentPublicInstance } from 'vue'; import type { ComponentPublicInstance } from 'vue';
import { computed, onMounted, onBeforeMount, ref, nextTick, watch } from 'vue'; import { computed, onMounted, onBeforeMount, ref, nextTick, watch } from 'vue';
import type { ItemWithKey } from '@n8n/design-system/types';
interface RecycleScrollerProps { interface RecycleScrollerProps {
itemSize: number; itemSize: number;
items: Array<Record<string, string>>; items: Item[];
itemKey: string; itemKey: Key;
offset?: number; offset?: number;
} }
@@ -24,18 +26,21 @@ const windowHeight = ref(0);
/** Cache */ /** Cache */
const itemSizeCache = ref<Record<string, number>>({}); const itemSizeCache = ref<Record<Item[Key], number>>({} as Record<Item[Key], number>);
const itemPositionCache = computed(() => { const itemPositionCache = computed(() => {
return props.items.reduce<Record<string, number>>((acc, item, index) => { return props.items.reduce<Record<Item[Key], number>>(
const key = item[props.itemKey]; (acc, item, index) => {
const prevItem = props.items[index - 1]; const key = item[props.itemKey];
const prevItemPosition = prevItem ? acc[prevItem[props.itemKey]] : 0; const prevItem = props.items[index - 1];
const prevItemSize = prevItem ? itemSizeCache.value[prevItem[props.itemKey]] : 0; const prevItemPosition = prevItem ? acc[prevItem[props.itemKey]] : 0;
const prevItemSize = prevItem ? itemSizeCache.value[prevItem[props.itemKey]] : 0;
acc[key] = prevItemPosition + prevItemSize; acc[key] = prevItemPosition + prevItemSize;
return acc; return acc;
}, {}); },
{} as Record<Item[Key], number>,
);
}); });
/** Indexes */ /** Indexes */
@@ -186,7 +191,7 @@ function onScroll() {
<div <div
v-for="item in visibleItems" v-for="item in visibleItems"
:key="item[itemKey]" :key="item[itemKey]"
:ref="(element) => (itemRefs[item[itemKey]] = element)" :ref="(element) => (itemRefs[`${item[itemKey]}`] = element)"
class="recycle-scroller-item" class="recycle-scroller-item"
> >
<slot :item="item" :update-item-size="onUpdateItemSize" /> <slot :item="item" :update-item-size="onUpdateItemSize" />

View File

@@ -8,6 +8,9 @@ describe('N8nRoute', () => {
props: { props: {
to: '/test', to: '/test',
}, },
global: {
stubs: ['RouterLink'],
},
}); });
expect(wrapper.html()).toMatchSnapshot(); expect(wrapper.html()).toMatchSnapshot();
}); });
@@ -18,6 +21,9 @@ describe('N8nRoute', () => {
to: '/test', to: '/test',
newWindow: true, newWindow: true,
}, },
global: {
stubs: ['RouterLink'],
},
}); });
expect(wrapper.html()).toMatchSnapshot(); expect(wrapper.html()).toMatchSnapshot();
}); });
@@ -27,6 +33,9 @@ describe('N8nRoute', () => {
props: { props: {
to: 'https://example.com/', to: 'https://example.com/',
}, },
global: {
stubs: ['RouterLink'],
},
}); });
expect(wrapper.html()).toMatchSnapshot(); expect(wrapper.html()).toMatchSnapshot();
}); });

View File

@@ -1,6 +1,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import { computed } from 'vue'; import { computed } from 'vue';
import { type RouteLocationRaw } from 'vue-router'; import { RouterLink, type RouteLocationRaw } from 'vue-router';
interface RouteProps { interface RouteProps {
to?: RouteLocationRaw | string; to?: RouteLocationRaw | string;
@@ -27,9 +27,9 @@ const openNewWindow = computed(() => !useRouterLink.value);
</script> </script>
<template> <template>
<router-link v-if="useRouterLink && to" :to="to" v-bind="$attrs"> <RouterLink v-if="useRouterLink && to" :to="to" role="link" v-bind="$attrs">
<slot></slot> <slot></slot>
</router-link> </RouterLink>
<a <a
v-else v-else
:href="to ? `${to}` : undefined" :href="to ? `${to}` : undefined"

View File

@@ -4,4 +4,4 @@ exports[`N8nRoute > should render external links 1`] = `"<a href="https://exampl
exports[`N8nRoute > should render internal links with newWindow=true 1`] = `"<a href="/test" target="_blank"></a>"`; exports[`N8nRoute > should render internal links with newWindow=true 1`] = `"<a href="/test" target="_blank"></a>"`;
exports[`N8nRoute > should render internal router links 1`] = `"<router-link to="/test"></router-link>"`; exports[`N8nRoute > should render internal router links 1`] = `"<router-link-stub to="/test" replace="false" custom="false" ariacurrentvalue="page" role="link"></router-link-stub>"`;

View File

@@ -2,6 +2,7 @@
import { computed } from 'vue'; import { computed } from 'vue';
import { useI18n } from '../../composables/useI18n'; import { useI18n } from '../../composables/useI18n';
import N8nIcon from '../N8nIcon';
const { t } = useI18n(); const { t } = useI18n();

View File

@@ -1,12 +1,12 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { TextSize } from '@n8n/design-system/types/text'; import type { IconSize } from '@n8n/design-system/types';
import N8nIcon from '../N8nIcon'; import N8nIcon from '../N8nIcon';
const TYPE = ['dots', 'ring'] as const; const TYPE = ['dots', 'ring'] as const;
interface SpinnerProps { interface SpinnerProps {
size?: Exclude<TextSize, 'mini' | 'xlarge'>; size?: IconSize;
type?: (typeof TYPE)[number]; type?: (typeof TYPE)[number];
} }

View File

@@ -1,26 +1,19 @@
<script lang="ts" setup generic="Value extends string | number"> <script lang="ts" setup generic="Value extends string | number">
import { onMounted, onUnmounted, ref } from 'vue'; import { onMounted, onUnmounted, ref } from 'vue';
import type { RouteLocationRaw } from 'vue-router'; import { RouterLink } from 'vue-router';
import type { TabOptions } from '../../types';
import N8nIcon from '../N8nIcon'; import N8nIcon from '../N8nIcon';
import N8nTooltip from '../N8nTooltip';
interface TabOptions {
value: Value;
label?: string;
icon?: string;
href?: string;
tooltip?: string;
align?: 'left' | 'right';
to?: RouteLocationRaw;
}
interface TabsProps { interface TabsProps {
modelValue?: Value; modelValue?: Value;
options?: TabOptions[]; options?: Array<TabOptions<Value>>;
size?: 'small' | 'medium'; size?: 'small' | 'medium';
} }
withDefaults(defineProps<TabsProps>(), { withDefaults(defineProps<TabsProps>(), {
modelValue: undefined,
options: () => [], options: () => [],
size: 'medium', size: 'medium',
}); });
@@ -108,14 +101,14 @@ const scrollRight = () => scroll(50);
</span> </span>
</div> </div>
</a> </a>
<router-link <RouterLink
v-else-if="option.to" v-else-if="option.to"
:to="option.to" :to="option.to"
:class="[$style.tab, { [$style.activeTab]: modelValue === option.value }]" :class="[$style.tab, { [$style.activeTab]: modelValue === option.value }]"
> >
<N8nIcon v-if="option.icon" :icon="option.icon" size="medium" /> <N8nIcon v-if="option.icon" :icon="option.icon" size="medium" />
<span v-if="option.label">{{ option.label }}</span> <span v-if="option.label">{{ option.label }}</span>
</router-link> </RouterLink>
<div <div
v-else v-else
:class="{ [$style.tab]: true, [$style.activeTab]: modelValue === option.value }" :class="{ [$style.tab]: true, [$style.activeTab]: modelValue === option.value }"

View File

@@ -27,7 +27,7 @@ const props = withDefaults(defineProps<TagsProp>(), {
const emit = defineEmits<{ const emit = defineEmits<{
expand: [value: boolean]; expand: [value: boolean];
'click:tag': [tagId: string, e: MouseEvent]; 'click:tag': [tagId: string, e: PointerEvent];
}>(); }>();
const { t } = useI18n(); const { t } = useI18n();

View File

@@ -1,5 +1,5 @@
<script lang="ts" setup generic="Value extends unknown = unknown"> <script lang="ts" setup generic="Value extends unknown = unknown">
import { computed, useCssModule } from 'vue'; import { computed, getCurrentInstance, useCssModule } from 'vue';
interface TreeProps { interface TreeProps {
value?: Record<string, Value>; value?: Record<string, Value>;
@@ -52,20 +52,23 @@ const getPath = (key: string): Array<string | number> => {
} }
return [...props.path, key]; return [...props.path, key];
}; };
// Get self component to avoid dependency cycle
const N8nTree = getCurrentInstance()?.type;
</script> </script>
<template> <template>
<div v-if="isObject(value)" class="n8n-tree"> <div v-if="isObject(value)" class="n8n-tree">
<div v-for="(label, i) in Object.keys(value)" :key="i" :class="classes"> <div v-for="(label, i) in Object.keys(value)" :key="i" :class="classes">
<div v-if="isSimple(value[label])" :class="$style.simple"> <div v-if="isSimple(value[label])" :class="$style.simple">
<slot v-if="$slots.label" name="label" :label="label" :path="getPath(label)" /> <slot v-if="!!$slots.label" name="label" :label="label" :path="getPath(label)" />
<span v-else>{{ label }}</span> <span v-else>{{ label }}</span>
<span>:</span> <span>:</span>
<slot v-if="$slots.value" name="value" :value="value[label]" /> <slot v-if="!!$slots.value" name="value" :value="value[label]" />
<span v-else>{{ value[label] }}</span> <span v-else>{{ value[label] }}</span>
</div> </div>
<div v-else> <div v-else>
<slot v-if="$slots.label" name="label" :label="label" :path="getPath(label)" /> <slot v-if="!!$slots.label" name="label" :label="label" :path="getPath(label)" />
<span v-else>{{ label }}</span> <span v-else>{{ label }}</span>
<N8nTree <N8nTree
v-if="isObject(value[label])" v-if="isObject(value[label])"
@@ -74,11 +77,11 @@ const getPath = (key: string): Array<string | number> => {
:value="value[label]" :value="value[label]"
:node-class="nodeClass" :node-class="nodeClass"
> >
<template v-if="$slots.label" #label="data"> <template v-if="!!$slots.label" #label="data">
<slot name="label" v-bind="data" /> <slot name="label" v-bind="data" />
</template> </template>
<template v-if="$slots.value" #value="data"> <template v-if="!!$slots.value" #value="data">
<slot name="value" v-bind="data" /> <slot name="value" v-bind="data" />
</template> </template>
</N8nTree> </N8nTree>

View File

@@ -7,9 +7,9 @@ import N8nBadge from '../N8nBadge';
import N8nText from '../N8nText'; import N8nText from '../N8nText';
interface UsersInfoProps { interface UsersInfoProps {
firstName?: string; firstName?: string | null;
lastName?: string; lastName?: string | null;
email?: string; email?: string | null;
isOwner?: boolean; isOwner?: boolean;
isPendingUser?: boolean; isPendingUser?: boolean;
isCurrentUser?: boolean; isCurrentUser?: boolean;

View File

@@ -74,7 +74,7 @@ const onBlur = () => emit('blur');
const onFocus = () => emit('focus'); const onFocus = () => emit('focus');
const getLabel = (user: IUser) => const getLabel = (user: IUser) =>
!user.fullName ? user.email : `${user.fullName} (${user.email})`; (!user.fullName ? user.email : `${user.fullName} (${user.email})`) ?? '';
</script> </script>
<template> <template>

View File

@@ -1,4 +1,5 @@
<script lang="ts" setup> <script lang="ts" setup>
import { ElDropdown, ElDropdownItem, ElDropdownMenu } from 'element-plus';
import { computed } from 'vue'; import { computed } from 'vue';
import type { IUser, UserStackGroups } from '@n8n/design-system/types'; import type { IUser, UserStackGroups } from '@n8n/design-system/types';
@@ -9,7 +10,7 @@ import N8nUserInfo from '../N8nUserInfo';
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
users: UserStackGroups; users: UserStackGroups;
currentUserEmail?: string; currentUserEmail?: string | null;
maxAvatars?: number; maxAvatars?: number;
dropdownTrigger?: 'hover' | 'click'; dropdownTrigger?: 'hover' | 'click';
}>(), }>(),
@@ -63,7 +64,7 @@ const menuHeight = computed(() => {
<template> <template>
<div class="user-stack" data-test-id="user-stack-container"> <div class="user-stack" data-test-id="user-stack-container">
<el-dropdown <ElDropdown
:trigger="$props.dropdownTrigger" :trigger="$props.dropdownTrigger"
:max-height="menuHeight" :max-height="menuHeight"
popper-class="user-stack-popper" popper-class="user-stack-popper"
@@ -81,14 +82,14 @@ const menuHeight = computed(() => {
<div v-if="hiddenUsersCount > 0" :class="$style.hiddenBadge">+{{ hiddenUsersCount }}</div> <div v-if="hiddenUsersCount > 0" :class="$style.hiddenBadge">+{{ hiddenUsersCount }}</div>
</div> </div>
<template #dropdown> <template #dropdown>
<el-dropdown-menu class="user-stack-list" data-test-id="user-stack-list"> <ElDropdownMenu class="user-stack-list" data-test-id="user-stack-list">
<div v-for="(groupUsers, index) in nonEmptyGroups" :key="index"> <div v-for="(groupUsers, index) in nonEmptyGroups" :key="index">
<div :class="$style.groupContainer"> <div :class="$style.groupContainer">
<el-dropdown-item> <ElDropdownItem>
<header v-if="groupCount > 1" :class="$style.groupName">{{ index }}</header> <header v-if="groupCount > 1" :class="$style.groupName">{{ index }}</header>
</el-dropdown-item> </ElDropdownItem>
<div :class="$style.groupUsers"> <div :class="$style.groupUsers">
<el-dropdown-item <ElDropdownItem
v-for="user in groupUsers" v-for="user in groupUsers"
:key="user.id" :key="user.id"
:data-test-id="`user-stack-info-${user.id}`" :data-test-id="`user-stack-info-${user.id}`"
@@ -98,13 +99,13 @@ const menuHeight = computed(() => {
v-bind="user" v-bind="user"
:is-current-user="user.email === props.currentUserEmail" :is-current-user="user.email === props.currentUserEmail"
/> />
</el-dropdown-item> </ElDropdownItem>
</div> </div>
</div> </div>
</div> </div>
</el-dropdown-menu> </ElDropdownMenu>
</template> </template>
</el-dropdown> </ElDropdown>
</div> </div>
</template> </template>

View File

@@ -1,4 +1,4 @@
<script lang="ts" setup> <script lang="ts" setup generic="UserType extends IUser = IUser">
import { computed } from 'vue'; import { computed } from 'vue';
import { useI18n } from '../../composables/useI18n'; import { useI18n } from '../../composables/useI18n';
@@ -8,10 +8,10 @@ import N8nBadge from '../N8nBadge';
import N8nUserInfo from '../N8nUserInfo'; import N8nUserInfo from '../N8nUserInfo';
interface UsersListProps { interface UsersListProps {
users: IUser[]; users: UserType[];
readonly?: boolean; readonly?: boolean;
currentUserId?: string; currentUserId?: string | null;
actions?: UserAction[]; actions?: Array<UserAction<UserType>>;
isSamlLoginEnabled?: boolean; isSamlLoginEnabled?: boolean;
} }
@@ -26,7 +26,7 @@ const props = withDefaults(defineProps<UsersListProps>(), {
const { t } = useI18n(); const { t } = useI18n();
const sortedUsers = computed(() => const sortedUsers = computed(() =>
[...props.users].sort((a: IUser, b: IUser) => { [...props.users].sort((a: UserType, b: UserType) => {
if (!a.email || !b.email) { if (!a.email || !b.email) {
throw new Error('Expected all users to have email'); throw new Error('Expected all users to have email');
} }
@@ -64,7 +64,7 @@ const sortedUsers = computed(() =>
); );
const defaultGuard = () => true; const defaultGuard = () => true;
const getActions = (user: IUser): UserAction[] => { const getActions = (user: UserType): Array<UserAction<UserType>> => {
if (user.isOwner) return []; if (user.isOwner) return [];
return props.actions.filter((action) => (action.guard ?? defaultGuard)(user)); return props.actions.filter((action) => (action.guard ?? defaultGuard)(user));
@@ -73,7 +73,7 @@ const getActions = (user: IUser): UserAction[] => {
const emit = defineEmits<{ const emit = defineEmits<{
action: [value: { action: string; userId: string }]; action: [value: { action: string; userId: string }];
}>(); }>();
const onUserAction = (user: IUser, action: string) => const onUserAction = (user: UserType, action: string) =>
emit('action', { emit('action', {
action, action,
userId: user.id, userId: user.id,
@@ -101,7 +101,7 @@ const onUserAction = (user: IUser, action: string) =>
<N8nActionToggle <N8nActionToggle
v-if=" v-if="
!user.isOwner && !user.isOwner &&
!['ldap'].includes(user.signInType) && user.signInType !== 'ldap' &&
!readonly && !readonly &&
getActions(user).length > 0 && getActions(user).length > 0 &&
actions.length > 0 actions.length > 0

View File

@@ -1,16 +1,11 @@
import type { Component, Plugin } from 'vue'; import type { Plugin } from 'vue';
import * as components from './components';
import * as directives from './directives'; import * as directives from './directives';
export interface N8nPluginOptions {} export interface N8nPluginOptions {}
export const N8nPlugin: Plugin<N8nPluginOptions> = { export const N8nPlugin: Plugin<N8nPluginOptions> = {
install: (app) => { install: (app) => {
for (const [name, component] of Object.entries(components)) {
app.component(name, component as unknown as Component);
}
for (const [name, directive] of Object.entries(directives)) { for (const [name, directive] of Object.entries(directives)) {
app.directive(name, directive); app.directive(name, directive);
} }

View File

@@ -0,0 +1,10 @@
const BADGE_THEME = [
'default',
'success',
'warning',
'danger',
'primary',
'secondary',
'tertiary',
] as const;
export type BadgeTheme = (typeof BADGE_THEME)[number];

View File

@@ -7,7 +7,7 @@ export type ButtonElement = (typeof BUTTON_ELEMENT)[number];
const BUTTON_TYPE = ['primary', 'secondary', 'tertiary', 'success', 'warning', 'danger'] as const; const BUTTON_TYPE = ['primary', 'secondary', 'tertiary', 'success', 'warning', 'danger'] as const;
export type ButtonType = (typeof BUTTON_TYPE)[number]; export type ButtonType = (typeof BUTTON_TYPE)[number];
const BUTTON_SIZE = ['mini', 'small', 'medium', 'large'] as const; const BUTTON_SIZE = ['xmini', 'mini', 'small', 'medium', 'large'] as const;
export type ButtonSize = (typeof BUTTON_SIZE)[number]; export type ButtonSize = (typeof BUTTON_SIZE)[number];
const BUTTON_NATIVE_TYPE = ['submit', 'reset', 'button'] as const; const BUTTON_NATIVE_TYPE = ['submit', 'reset', 'button'] as const;
@@ -21,7 +21,7 @@ export interface IconButtonProps {
loading?: boolean; loading?: boolean;
outline?: boolean; outline?: boolean;
size?: ButtonSize; size?: ButtonSize;
iconSize?: Exclude<IconSize, 'xlarge'>; iconSize?: IconSize;
text?: boolean; text?: boolean;
type?: ButtonType; type?: ButtonType;
nativeType?: ButtonNativeType; nativeType?: ButtonNativeType;

View File

@@ -0,0 +1,2 @@
const CALLOUT_THEMES = ['info', 'success', 'secondary', 'warning', 'danger', 'custom'] as const;
export type CalloutTheme = (typeof CALLOUT_THEMES)[number];

View File

@@ -4,7 +4,7 @@ export type DatatableRowDataType = string | number | boolean | null | undefined;
export interface DatatableRow { export interface DatatableRow {
id: string | number; id: string | number;
[key: string]: DatatableRowDataType | Record<string, DatatableRowDataType>; [key: string]: unknown;
} }
export interface DatatableColumn { export interface DatatableColumn {

View File

@@ -1,5 +1,13 @@
import type { N8nLocaleTranslateFnOptions } from '@n8n/design-system/types/i18n'; import type { N8nLocaleTranslateFnOptions } from '@n8n/design-system/types/i18n';
export type FormFieldValue = string | number | boolean | null | undefined;
export type FormInputsToFormValues<T extends IFormInput[], V> = {
[K in T[number]['name']]: V;
};
export type FormFieldValueUpdate = { name: string; value: FormFieldValue };
export type Rule = { name: string; config?: unknown }; export type Rule = { name: string; config?: unknown };
export type RuleGroup = { export type RuleGroup = {
@@ -65,6 +73,8 @@ export type IFormInput = {
export type IFormInputs = IFormInput[]; export type IFormInputs = IFormInput[];
export type FormValues = FormInputsToFormValues<IFormInput[], FormFieldValue>;
export type IFormBoxConfig = { export type IFormBoxConfig = {
title: string; title: string;
buttonText?: string; buttonText?: string;

View File

@@ -1,12 +1,19 @@
export * from './action-dropdown'; export * from './action-dropdown';
export * from './assistant';
export * from './badge';
export * from './button'; export * from './button';
export * from './callout';
export * from './datatable'; export * from './datatable';
export * from './form'; export * from './form';
export * from './i18n'; export * from './i18n';
export * from './icon';
export * from './input'; export * from './input';
export * from './menu';
export * from './select';
export * from './user';
export * from './keyboardshortcut'; export * from './keyboardshortcut';
export * from './menu';
export * from './node-creator-node'; export * from './node-creator-node';
export * from './recycle-scroller';
export * from './resize'; export * from './resize';
export * from './select';
export * from './tabs';
export * from './text';
export * from './user';

View File

@@ -0,0 +1,5 @@
export type ItemWithKey<Key extends string> = {
[K in Key]: string;
} & {
[key: string]: unknown;
};

View File

@@ -0,0 +1,11 @@
import type { RouteLocationRaw } from 'vue-router';
export interface TabOptions<Value extends string | number> {
value: Value;
label?: string;
icon?: string;
href?: string;
tooltip?: string;
align?: 'left' | 'right';
to?: RouteLocationRaw;
}

View File

@@ -1,23 +1,24 @@
export interface IUser { export type IUser = {
id: string; id: string;
firstName?: string; firstName?: string | null;
lastName?: string; lastName?: string | null;
fullName?: string; fullName?: string;
email?: string; role?: string;
isOwner: boolean; email?: string | null;
isPendingUser: boolean; signInType?: string;
isOwner?: boolean;
isPendingUser?: boolean;
inviteAcceptUrl?: string; inviteAcceptUrl?: string;
disabled: boolean; disabled?: boolean;
signInType: string; };
}
export interface UserAction { export interface UserAction<UserType extends IUser> {
label: string; label: string;
value: string; value: string;
disabled: boolean; disabled?: boolean;
type?: 'external-link'; type?: 'external-link';
tooltip?: string; tooltip?: string;
guard?: (user: IUser) => boolean; guard?: (user: UserType) => boolean;
} }
export type UserStackGroups = { [groupName: string]: IUser[] }; export type UserStackGroups = { [groupName: string]: IUser[] };

View File

@@ -86,6 +86,7 @@
"vue": "catalog:frontend", "vue": "catalog:frontend",
"vue-agile": "^2.0.0", "vue-agile": "^2.0.0",
"vue-chartjs": "^5.2.0", "vue-chartjs": "^5.2.0",
"vue-component-type-helpers": "^2.2.10",
"vue-github-button": "^3.1.3", "vue-github-button": "^3.1.3",
"vue-i18n": "catalog:frontend", "vue-i18n": "catalog:frontend",
"vue-json-pretty": "2.2.4", "vue-json-pretty": "2.2.4",

View File

@@ -293,6 +293,56 @@ export type BaseResource = {
name: string; 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< export type WorkflowListItem = Omit<
IWorkflowDb, IWorkflowDb,
'nodes' | 'connections' | 'settings' | 'pinData' | 'usedCredentials' | 'meta' 'nodes' | 'connections' | 'settings' | 'pinData' | 'usedCredentials' | 'meta'

View File

@@ -1,4 +1,4 @@
import type { Plugin } from 'vue'; import type { Component, Plugin } from 'vue';
import { render } from '@testing-library/vue'; import { render } from '@testing-library/vue';
import { i18nInstance } from '@n8n/i18n'; import { i18nInstance } from '@n8n/i18n';
import { GlobalComponentsPlugin } from '@/plugins/components'; import { GlobalComponentsPlugin } from '@/plugins/components';
@@ -10,6 +10,7 @@ import type { Telemetry } from '@/plugins/telemetry';
import vueJsonPretty from 'vue-json-pretty'; import vueJsonPretty from 'vue-json-pretty';
import merge from 'lodash/merge'; import merge from 'lodash/merge';
import type { TestingPinia } from '@pinia/testing'; import type { TestingPinia } from '@pinia/testing';
import * as components from '@n8n/design-system/components';
export type RenderComponent = Parameters<typeof render>[0]; export type RenderComponent = Parameters<typeof render>[0];
export type RenderOptions = Parameters<typeof render>[1] & { 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 = { const defaultOptions = {
global: { global: {
stubs: { stubs: {
@@ -38,6 +47,7 @@ const defaultOptions = {
GlobalComponentsPlugin, GlobalComponentsPlugin,
GlobalDirectivesPlugin, GlobalDirectivesPlugin,
TelemetryPlugin, TelemetryPlugin,
TestingGlobalComponentsPlugin,
], ],
}, },
}; };

View File

@@ -76,7 +76,7 @@ const getExpirationTime = (apiKey: ApiKey): string => {
<template #append> <template #append>
<div ref="cardActions" :class="$style.cardActions"> <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> </div>
</template> </template>
</n8n-card> </n8n-card>

View File

@@ -6,7 +6,7 @@ import Modal from '@/components/Modal.vue';
import { useUsersStore } from '@/stores/users.store'; import { useUsersStore } from '@/stores/users.store';
import { createFormEventBus } from '@n8n/design-system/utils'; import { createFormEventBus } from '@n8n/design-system/utils';
import { createEventBus } from '@n8n/utils/event-bus'; 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'; import { useI18n } from '@n8n/i18n';
const config = ref<IFormInputs | null>(null); const config = ref<IFormInputs | null>(null);
@@ -33,17 +33,14 @@ const passwordsMatch = (value: string | number | boolean | null | undefined) =>
return false; return false;
}; };
const onInput = (e: { name: string; value: string }) => { const onInput = (e: FormFieldValueUpdate) => {
if (e.name === 'password') { if (e.name === 'password' && typeof e.value === 'string') {
password.value = e.value; password.value = e.value;
} }
}; };
const onSubmit = async (values: { const onSubmit = async (data: FormValues) => {
currentPassword: string; const values = data as { currentPassword: string; password: string; mfaCode?: string };
password: string;
mfaCode?: string;
}) => {
try { try {
loading.value = true; loading.value = true;
await usersStore.updateCurrentUserPassword({ await usersStore.updateCurrentUserPassword({
@@ -143,6 +140,7 @@ onMounted(() => {
> >
<template #content> <template #content>
<n8n-form-inputs <n8n-form-inputs
v-if="config"
:inputs="config" :inputs="config"
:event-bus="formBus" :event-bus="formBus"
:column-view="true" :column-view="true"

View File

@@ -1,10 +1,11 @@
<script lang="ts" setup> <script lang="ts" setup>
import { useUIStore } from '@/stores/ui.store'; 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 { NPM_PACKAGE_DOCS_BASE_URL, COMMUNITY_PACKAGE_MANAGE_ACTIONS } from '@/constants';
import { useI18n } from '@n8n/i18n'; import { useI18n } from '@n8n/i18n';
import { useTelemetry } from '@/composables/useTelemetry'; import { useTelemetry } from '@/composables/useTelemetry';
import { useSettingsStore } from '@/stores/settings.store'; import { useSettingsStore } from '@/stores/settings.store';
import type { UserAction } from '@n8n/design-system';
interface Props { interface Props {
communityPackage?: PublicInstalledPackage | null; communityPackage?: PublicInstalledPackage | null;
@@ -22,7 +23,7 @@ const i18n = useI18n();
const telemetry = useTelemetry(); const telemetry = useTelemetry();
const settingsStore = useSettingsStore(); const settingsStore = useSettingsStore();
const packageActions = [ const packageActions: Array<UserAction<IUser>> = [
{ {
label: i18n.baseText('settings.communityNodes.viewDocsAction.label'), label: i18n.baseText('settings.communityNodes.viewDocsAction.label'),
value: COMMUNITY_PACKAGE_MANAGE_ACTIONS.VIEW_DOCS, value: COMMUNITY_PACKAGE_MANAGE_ACTIONS.VIEW_DOCS,

View File

@@ -12,7 +12,7 @@ import { useProjectsStore } from '@/stores/projects.store';
import ProjectCardBadge from '@/components/Projects/ProjectCardBadge.vue'; import ProjectCardBadge from '@/components/Projects/ProjectCardBadge.vue';
import { useI18n } from '@n8n/i18n'; import { useI18n } from '@n8n/i18n';
import { ResourceType } from '@/utils/projects.utils'; import { ResourceType } from '@/utils/projects.utils';
import type { CredentialsResource } from './layouts/ResourcesListLayout.vue'; import type { CredentialsResource } from '@/Interface';
const CREDENTIAL_LIST_ITEM_ACTIONS = { const CREDENTIAL_LIST_ITEM_ACTIONS = {
OPEN: 'open', OPEN: 'open',

View File

@@ -4,6 +4,7 @@ import type { BaseTextKey } from '@n8n/i18n';
import type { TestTableColumn } from '@/components/Evaluations.ee/shared/TestTableBase.vue'; import type { TestTableColumn } from '@/components/Evaluations.ee/shared/TestTableBase.vue';
import { useI18n } from '@n8n/i18n'; import { useI18n } from '@n8n/i18n';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import type { BadgeTheme } from '@n8n/design-system';
defineProps<{ defineProps<{
column: TestTableColumn<T>; column: TestTableColumn<T>;
@@ -39,7 +40,7 @@ const errorTooltipMap: Record<string, BaseTextKey> = {
}; };
// FIXME: move status logic to a parent component // FIXME: move status logic to a parent component
const statusThemeMap: Record<string, string> = { const statusThemeMap: Record<string, BadgeTheme> = {
new: 'default', new: 'default',
running: 'warning', running: 'warning',
evaluation_running: 'warning', evaluation_running: 'warning',

View File

@@ -206,7 +206,7 @@ defineExpose({ focus, select });
outline outline
type="tertiary" type="tertiary"
icon="external-link-alt" icon="external-link-alt"
size="xsmall" size="mini"
:class="$style['expression-editor-modal-opener']" :class="$style['expression-editor-modal-opener']"
data-test-id="expander" data-test-id="expander"
@click="emit('modal-opener-click')" @click="emit('modal-opener-click')"

View File

@@ -9,6 +9,7 @@ import { mockedStore } from '@/__tests__/utils';
import { useProjectsStore } from '@/stores/projects.store'; import { useProjectsStore } from '@/stores/projects.store';
import { ProjectTypes, type Project } from '@/types/projects.types'; import { ProjectTypes, type Project } from '@/types/projects.types';
import { useFoldersStore } from '@/stores/folders.store'; import { useFoldersStore } from '@/stores/folders.store';
import type { IUser } from 'n8n-workflow';
vi.mock('vue-router', async (importOriginal) => ({ vi.mock('vue-router', async (importOriginal) => ({
// eslint-disable-next-line @typescript-eslint/consistent-type-imports // eslint-disable-next-line @typescript-eslint/consistent-type-imports
@@ -41,7 +42,7 @@ const TEST_FOLDER_CHILD: FolderShortInfo = {
parentFolder: TEST_FOLDER.id, parentFolder: TEST_FOLDER.id,
}; };
const TEST_ACTIONS: UserAction[] = [ const TEST_ACTIONS: Array<UserAction<IUser>> = [
{ label: 'Action 1', value: 'action1', disabled: false }, { label: 'Action 1', value: 'action1', disabled: false },
{ label: 'Action 2', value: 'action2', disabled: true }, { label: 'Action 2', value: 'action2', disabled: true },
]; ];

View File

@@ -7,11 +7,12 @@ import { type PathItem } from '@n8n/design-system/components/N8nBreadcrumbs/Brea
import { computed, onBeforeUnmount, ref, watch } from 'vue'; import { computed, onBeforeUnmount, ref, watch } from 'vue';
import { useFoldersStore } from '@/stores/folders.store'; import { useFoldersStore } from '@/stores/folders.store';
import type { FolderPathItem, FolderShortInfo } from '@/Interface'; import type { FolderPathItem, FolderShortInfo } from '@/Interface';
import type { IUser } from 'n8n-workflow';
type Props = { type Props = {
// Current folder can be null when showing breadcrumbs for workflows in project root // Current folder can be null when showing breadcrumbs for workflows in project root
currentFolder?: FolderShortInfo | null; currentFolder?: FolderShortInfo | null;
actions?: UserAction[]; actions?: Array<UserAction<IUser>>;
hiddenItemsTrigger?: 'hover' | 'click'; hiddenItemsTrigger?: 'hover' | 'click';
currentFolderAsLink?: boolean; currentFolderAsLink?: boolean;
visibleLevels?: 1 | 2; visibleLevels?: 1 | 2;

View File

@@ -2,8 +2,8 @@ import { createComponentRenderer } from '@/__tests__/render';
import userEvent from '@testing-library/user-event'; import userEvent from '@testing-library/user-event';
import FolderCard from './FolderCard.vue'; import FolderCard from './FolderCard.vue';
import { createPinia, setActivePinia } from 'pinia'; import { createPinia, setActivePinia } from 'pinia';
import type { FolderResource } from '../layouts/ResourcesListLayout.vue'; import type { FolderResource, FolderPathItem, UserAction } from '@/Interface';
import type { FolderPathItem, UserAction } from '@/Interface'; import type { IUser } from 'n8n-workflow';
vi.mock('vue-router', () => { vi.mock('vue-router', () => {
const push = vi.fn(); const push = vi.fn();
@@ -54,7 +54,7 @@ const renderComponent = createComponentRenderer(FolderCard, {
actions: [ actions: [
{ label: 'Open', value: 'open', disabled: false }, { label: 'Open', value: 'open', disabled: false },
{ label: 'Delete', value: 'delete', disabled: false }, { label: 'Delete', value: 'delete', disabled: false },
] as const satisfies UserAction[], ] as const satisfies Array<UserAction<IUser>>,
breadcrumbs: DEFAULT_BREADCRUMBS, breadcrumbs: DEFAULT_BREADCRUMBS,
}, },
global: { global: {

View File

@@ -1,20 +1,20 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, ref } from 'vue'; import { computed, ref } from 'vue';
import { FOLDER_LIST_ITEM_ACTIONS } from './constants'; import { FOLDER_LIST_ITEM_ACTIONS } from './constants';
import type { FolderResource } from '../layouts/ResourcesListLayout.vue';
import { ProjectTypes, type Project } from '@/types/projects.types'; import { ProjectTypes, type Project } from '@/types/projects.types';
import { useI18n } from '@n8n/i18n'; import { useI18n } from '@n8n/i18n';
import { useRoute, useRouter } from 'vue-router'; import { useRoute, useRouter } from 'vue-router';
import { VIEWS } from '@/constants'; import { VIEWS } from '@/constants';
import type { UserAction } from '@/Interface'; import type { FolderResource, UserAction } from '@/Interface';
import { ResourceType } from '@/utils/projects.utils'; import { ResourceType } from '@/utils/projects.utils';
import type { PathItem } from '@n8n/design-system/components/N8nBreadcrumbs/Breadcrumbs.vue'; import type { PathItem } from '@n8n/design-system/components/N8nBreadcrumbs/Breadcrumbs.vue';
import { useFoldersStore } from '@/stores/folders.store'; import { useFoldersStore } from '@/stores/folders.store';
import { type IUser } from 'n8n-workflow';
type Props = { type Props = {
data: FolderResource; data: FolderResource;
personalProject: Project | null; personalProject: Project | null;
actions: UserAction[]; actions: Array<UserAction<IUser>>;
readOnly?: boolean; readOnly?: boolean;
showOwnershipBadge?: boolean; showOwnershipBadge?: boolean;
}; };
@@ -36,6 +36,7 @@ const emit = defineEmits<{
}>(); }>();
const hiddenBreadcrumbsItemsAsync = ref<Promise<PathItem[]>>(new Promise(() => {})); const hiddenBreadcrumbsItemsAsync = ref<Promise<PathItem[]>>(new Promise(() => {}));
const cachedHiddenBreadcrumbsItems = ref<PathItem[]>([]); const cachedHiddenBreadcrumbsItems = ref<PathItem[]>([]);
const resourceTypeLabel = computed(() => i18n.baseText('generic.folder').toLowerCase()); const resourceTypeLabel = computed(() => i18n.baseText('generic.folder').toLowerCase());

View File

@@ -118,7 +118,7 @@ const onClaimCreditsClicked = async () => {
}) })
}}</n8n-text }}</n8n-text
>&nbsp; >&nbsp;
<n8n-text size="small" bold="true"> <n8n-text size="small" :bold="true">
{{ i18n.baseText('freeAi.credits.callout.success.title.part2') }}</n8n-text {{ i18n.baseText('freeAi.credits.callout.success.title.part2') }}</n8n-text
> >
</n8n-callout> </n8n-callout>

View File

@@ -11,7 +11,7 @@ import {
NodeConnectionTypes, NodeConnectionTypes,
traverseNodeParameters, traverseNodeParameters,
} from 'n8n-workflow'; } 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 { computed, ref, watch } from 'vue';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { useTelemetry } from '@/composables/useTelemetry'; import { useTelemetry } from '@/composables/useTelemetry';
@@ -249,9 +249,11 @@ const onExecute = async () => {
}; };
// Add handler for tool selection change // Add handler for tool selection change
const onUpdate = (change: { name: string; value: string }) => { const onUpdate = (change: FormFieldValueUpdate) => {
if (change.name !== 'toolName') return; if (change.name !== 'toolName') return;
selectedTool.value = change.value; if (typeof change.value === 'string') {
selectedTool.value = change.value;
}
}; };
</script> </script>

View File

@@ -2,17 +2,18 @@
import { nextTick, ref } from 'vue'; import { nextTick, ref } from 'vue';
import { useToast } from '@/composables/useToast'; import { useToast } from '@/composables/useToast';
import { onClickOutside } from '@vueuse/core'; import { onClickOutside } from '@vueuse/core';
import type { InputType } from '@n8n/design-system';
interface Props { interface Props {
modelValue: string; modelValue: string;
subtitle?: string; subtitle?: string;
type: string; type: InputType;
readonly?: boolean; readonly?: boolean;
placeholder?: string; placeholder?: string;
maxlength?: number; maxlength?: number;
required?: boolean; required?: boolean;
autosize?: boolean | { minRows: number; maxRows: number }; autosize?: boolean | { minRows: number; maxRows: number };
inputType?: string; inputType?: InputType;
maxHeight?: string; maxHeight?: string;
} }
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {

View File

@@ -2,7 +2,13 @@
import { computed, onMounted, ref } from 'vue'; import { computed, onMounted, ref } from 'vue';
import { useToast } from '@/composables/useToast'; import { useToast } from '@/composables/useToast';
import Modal from './Modal.vue'; 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 { EnterpriseEditionFeature, VALID_EMAIL_REGEX, INVITE_USER_MODAL_KEY } from '@/constants';
import { ROLE } from '@n8n/api-types'; import { ROLE } from '@n8n/api-types';
import { useUsersStore } from '@/stores/users.store'; import { useUsersStore } from '@/stores/users.store';
@@ -127,11 +133,15 @@ const validateEmails = (value: string | number | boolean | null | undefined) =>
return false; return false;
}; };
function onInput(e: { name: string; value: InvitableRoleName }) { function isInvitableRoleName(val: unknown): val is InvitableRoleName {
if (e.name === 'emails') { 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; emails.value = e.value;
} }
if (e.name === 'role') { if (e.name === 'role' && isInvitableRoleName(e.value)) {
role.value = e.value; role.value = e.value;
} }
} }
@@ -312,7 +322,7 @@ function getEmail(email: string): string {
</n8n-users-list> </n8n-users-list>
</div> </div>
<n8n-form-inputs <n8n-form-inputs
v-else v-else-if="config"
:inputs="config" :inputs="config"
:event-bus="formBus" :event-bus="formBus"
:column-view="true" :column-view="true"

View File

@@ -16,8 +16,8 @@ const emit = defineEmits<{
'update:modelValue': [tab: MAIN_HEADER_TABS, event: MouseEvent]; 'update:modelValue': [tab: MAIN_HEADER_TABS, event: MouseEvent];
}>(); }>();
function onUpdateModelValue(tab: MAIN_HEADER_TABS, event: MouseEvent): void { function onUpdateModelValue(tab: string, event: MouseEvent): void {
emit('update:modelValue', tab, event); emit('update:modelValue', tab as MAIN_HEADER_TABS, event);
} }
</script> </script>

View File

@@ -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) { switch (action) {
case WORKFLOW_MENU_ACTIONS.DUPLICATE: { case WORKFLOW_MENU_ACTIONS.DUPLICATE: {
uiStore.openModalWithData({ uiStore.openModalWithData({

View File

@@ -25,6 +25,7 @@ import { usePageRedirectionHelper } from '@/composables/usePageRedirectionHelper
import { useGlobalEntityCreation } from '@/composables/useGlobalEntityCreation'; import { useGlobalEntityCreation } from '@/composables/useGlobalEntityCreation';
import { N8nNavigationDropdown, N8nTooltip, N8nLink, N8nIconButton } from '@n8n/design-system'; import { N8nNavigationDropdown, N8nTooltip, N8nLink, N8nIconButton } from '@n8n/design-system';
import type { IMenuItem } from '@n8n/design-system';
import { onClickOutside, type VueInstance } from '@vueuse/core'; import { onClickOutside, type VueInstance } from '@vueuse/core';
import Logo from './Logo/Logo.vue'; import Logo from './Logo/Logo.vue';
@@ -67,7 +68,7 @@ const userMenuItems = ref([
}, },
]); ]);
const mainMenuItems = computed(() => [ const mainMenuItems = computed<IMenuItem[]>(() => [
{ {
id: 'cloud-admin', id: 'cloud-admin',
position: 'bottom', position: 'bottom',

View File

@@ -8,7 +8,7 @@ import { LOCAL_STORAGE_MAIN_PANEL_RELATIVE_WIDTH, MAIN_NODE_PANEL_WIDTH } from '
import { useNDVStore } from '@/stores/ndv.store'; import { useNDVStore } from '@/stores/ndv.store';
import { ndvEventBus } from '@/event-bus'; import { ndvEventBus } from '@/event-bus';
import NDVFloatingNodes from '@/components/NDVFloatingNodes.vue'; 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 { ref, onMounted, onBeforeUnmount, computed, watch, nextTick } from 'vue';
import { useUIStore } from '@/stores/ui.store'; import { useUIStore } from '@/stores/ui.store';
import { useThrottleFn } from '@vueuse/core'; import { useThrottleFn } from '@vueuse/core';
@@ -151,8 +151,8 @@ const outputPanelRelativeTranslate = computed((): number => {
return currentRelativeLeftDelta > 0 ? currentRelativeLeftDelta : 0; return currentRelativeLeftDelta > 0 ? currentRelativeLeftDelta : 0;
}); });
const supportedResizeDirections = computed((): string[] => { const supportedResizeDirections = computed((): Direction[] => {
const supportedDirections = ['right']; const supportedDirections = ['right' as Direction];
if (props.isDraggable) supportedDirections.push('left'); if (props.isDraggable) supportedDirections.push('left');
return supportedDirections; return supportedDirections;

View File

@@ -99,10 +99,11 @@ describe('NDVSubConnections', () => {
<div class="connectionType"><span class="connectionLabel">Tools</span> <div class="connectionType"><span class="connectionLabel">Tools</span>
<div> <div>
<div class="connectedNodesWrapper" style="--nodes-length: 0;"> <div class="connectedNodesWrapper" style="--nodes-length: 0;">
<div class="plusButton"> <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>
<n8n-tooltip placement="top" teleported="true" offset="10" show-after="300" disabled="false"> <!--v-if-->
<n8n-icon-button size="medium" icon="plus" type="tertiary" data-test-id="add-subnode-ai_tool-0"></n8n-icon-button> </button>
</n8n-tooltip> <!--teleport start-->
<!--teleport end-->
</div> </div>
<!--v-if--> <!--v-if-->
</div> </div>

View File

@@ -26,10 +26,11 @@ import { useRunWorkflow } from '@/composables/useRunWorkflow';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { useI18n } from '@n8n/i18n'; import { useI18n } from '@n8n/i18n';
import { useTelemetry } from '@/composables/useTelemetry'; import { useTelemetry } from '@/composables/useTelemetry';
import { type IUpdateInformation } from '@/Interface'; import type { ButtonSize, IUpdateInformation } from '@/Interface';
import { generateCodeForAiTransform } from '@/components/ButtonParameter/utils'; import { generateCodeForAiTransform } from '@/components/ButtonParameter/utils';
import { needsAgentInput } from '@/utils/nodes/nodeTransforms'; import { needsAgentInput } from '@/utils/nodes/nodeTransforms';
import { useUIStore } from '@/stores/ui.store'; 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 NODE_TEST_STEP_POPUP_COUNT_KEY = 'N8N_NODE_TEST_STEP_POPUP_COUNT';
const MAX_POPUP_COUNT = 10; const MAX_POPUP_COUNT = 10;
@@ -41,8 +42,8 @@ const props = withDefaults(
telemetrySource: string; telemetrySource: string;
disabled?: boolean; disabled?: boolean;
label?: string; label?: string;
type?: string; type?: ButtonType;
size?: string; size?: ButtonSize;
transparent?: boolean; transparent?: boolean;
hideIcon?: boolean; hideIcon?: boolean;
tooltip?: string; tooltip?: string;

View File

@@ -119,7 +119,7 @@ const options = computed<ITab[]>(() => {
return options; return options;
}); });
function onTabSelect(tab: string) { function onTabSelect(tab: string | number) {
if (tab === 'docs' && props.nodeType) { if (tab === 'docs' && props.nodeType) {
void externalHooks.run('dataDisplay.onDocumentationUrlClick', { void externalHooks.run('dataDisplay.onDocumentationUrlClick', {
nodeType: props.nodeType, 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') { if (tab === 'communityNode' && (event.target as Element).localName === 'a') {
telemetry.track('user clicked cnr docs link', { source: 'node details view' }); telemetry.track('user clicked cnr docs link', { source: 'node details view' });
} }

View File

@@ -26,9 +26,9 @@ const emit = defineEmits<{
v-if="!isReadOnly" v-if="!isReadOnly"
type="tertiary" type="tertiary"
:class="['n8n-input', $style.overrideCloseButton]" :class="['n8n-input', $style.overrideCloseButton]"
outline="false" :outline="false"
icon="xmark" icon="xmark"
size="xsmall" size="mini"
@click="emit('close')" @click="emit('close')"
/> />
</div> </div>

View File

@@ -114,438 +114,441 @@ const isSaving = ref(false);
const userPermissions = computed(() => const userPermissions = computed(() =>
getResourcePermissions(usersStore.currentUser?.globalScopes), getResourcePermissions(usersStore.currentUser?.globalScopes),
); );
const survey = computed<IFormInputs>(() => [ const survey = computed<IFormInputs>(
{ () =>
name: COMPANY_TYPE_KEY, [
properties: { {
label: i18n.baseText('personalizationModal.whatBestDescribesYourCompany'), name: COMPANY_TYPE_KEY,
type: 'select', properties: {
placeholder: i18n.baseText('personalizationModal.select'), label: i18n.baseText('personalizationModal.whatBestDescribesYourCompany'),
options: [ type: 'select',
{ placeholder: i18n.baseText('personalizationModal.select'),
label: i18n.baseText('personalizationModal.saas'), options: [
value: SAAS_COMPANY_TYPE, {
}, label: i18n.baseText('personalizationModal.saas'),
{ value: SAAS_COMPANY_TYPE,
label: i18n.baseText('personalizationModal.eCommerce'), },
value: ECOMMERCE_COMPANY_TYPE, {
}, label: i18n.baseText('personalizationModal.eCommerce'),
value: ECOMMERCE_COMPANY_TYPE,
},
{ {
label: i18n.baseText('personalizationModal.digitalAgencyOrConsultant'), label: i18n.baseText('personalizationModal.digitalAgencyOrConsultant'),
value: DIGITAL_AGENCY_COMPANY_TYPE, 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'),
},
],
}, },
{ shouldDisplay(values): boolean {
value: EDUCATION_TYPE, const companyType = (values as IPersonalizationLatestVersion)[COMPANY_TYPE_KEY];
label: i18n.baseText('personalizationModal.education'), 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'),
}, },
{ shouldDisplay(values): boolean {
label: i18n.baseText('personalizationModal.imNotUsingN8nForWork'), const companyType = (values as IPersonalizationLatestVersion)[COMPANY_TYPE_KEY];
value: PERSONAL_COMPANY_TYPE, const companyIndustry = (values as IPersonalizationLatestVersion)[
COMPANY_INDUSTRY_EXTENDED_KEY
];
return (
companyType === OTHER_COMPANY_TYPE &&
!!companyIndustry &&
companyIndustry.includes(OTHER_INDUSTRY_OPTION)
);
}, },
], },
}, {
}, name: ROLE_KEY,
{ properties: {
name: COMPANY_INDUSTRY_EXTENDED_KEY, type: 'select',
properties: { label: i18n.baseText('personalizationModal.whichRoleBestDescribesYou'),
type: 'multi-select', placeholder: i18n.baseText('personalizationModal.select'),
label: i18n.baseText('personalizationModal.whichIndustriesIsYourCompanyIn'), options: [
placeholder: i18n.baseText('personalizationModal.select'), {
options: [ value: ROLE_BUSINESS_OWNER,
{ label: i18n.baseText('personalizationModal.businessOwner'),
value: FINANCE_INSURANCE_INDUSTRY, },
label: i18n.baseText('personalizationModal.financeOrInsurance'), {
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'),
},
],
}, },
{ shouldDisplay(values): boolean {
value: GOVERNMENT_INDUSTRY, const companyType = (values as IPersonalizationLatestVersion)[COMPANY_TYPE_KEY];
label: i18n.baseText('personalizationModal.government'), return companyType !== PERSONAL_COMPANY_TYPE;
}, },
{ },
value: HEALTHCARE_INDUSTRY, {
label: i18n.baseText('personalizationModal.healthcare'), name: ROLE_OTHER_KEY,
properties: {
placeholder: i18n.baseText('personalizationModal.specifyYourRole'),
}, },
{ shouldDisplay(values): boolean {
value: IT_INDUSTRY, const companyType = (values as IPersonalizationLatestVersion)[COMPANY_TYPE_KEY];
label: i18n.baseText('personalizationModal.it'), 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'),
},
],
}, },
{ shouldDisplay(values): boolean {
value: MSP_INDUSTRY, const companyType = (values as IPersonalizationLatestVersion)[COMPANY_TYPE_KEY];
label: i18n.baseText('personalizationModal.managedServiceProvider'), 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'),
}, },
{ shouldDisplay(values): boolean {
value: MEDIA_INDUSTRY, const companyType = (values as IPersonalizationLatestVersion)[COMPANY_TYPE_KEY];
label: i18n.baseText('personalizationModal.media'), 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,
},
],
}, },
{ shouldDisplay(values): boolean {
value: PHYSICAL_RETAIL_OR_SERVICES, const companyType = (values as IPersonalizationLatestVersion)[COMPANY_TYPE_KEY];
label: i18n.baseText('personalizationModal.physicalRetailOrServices'), 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'),
}, },
{ shouldDisplay(values): boolean {
value: SECURITY_INDUSTRY, const companyType = (values as IPersonalizationLatestVersion)[COMPANY_TYPE_KEY];
label: i18n.baseText('personalizationModal.security'), 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,
},
],
}, },
{ shouldDisplay(values): boolean {
value: OTHER_INDUSTRY_OPTION, const companyType = (values as IPersonalizationLatestVersion)[COMPANY_TYPE_KEY];
label: i18n.baseText('personalizationModal.otherPleaseSpecify'), return companyType !== PERSONAL_COMPANY_TYPE;
}, },
], },
}, {
shouldDisplay(values): boolean { name: COMPANY_SIZE_KEY,
const companyType = (values as IPersonalizationLatestVersion)[COMPANY_TYPE_KEY]; properties: {
return companyType === OTHER_COMPANY_TYPE; type: 'select',
}, label: i18n.baseText('personalizationModal.howBigIsYourCompany'),
}, placeholder: i18n.baseText('personalizationModal.select'),
{ options: [
name: OTHER_COMPANY_INDUSTRY_EXTENDED_KEY, {
properties: { label: i18n.baseText('personalizationModal.lessThan20People'),
placeholder: i18n.baseText('personalizationModal.specifyYourCompanysIndustry'), value: COMPANY_SIZE_20_OR_LESS,
}, },
shouldDisplay(values): boolean { {
const companyType = (values as IPersonalizationLatestVersion)[COMPANY_TYPE_KEY]; label: `20-99 ${i18n.baseText('personalizationModal.people')}`,
const companyIndustry = (values as IPersonalizationLatestVersion)[ value: COMPANY_SIZE_20_99,
COMPANY_INDUSTRY_EXTENDED_KEY },
]; {
return ( label: `100-499 ${i18n.baseText('personalizationModal.people')}`,
companyType === OTHER_COMPANY_TYPE && value: COMPANY_SIZE_100_499,
!!companyIndustry && },
companyIndustry.includes(OTHER_INDUSTRY_OPTION) {
); label: `500-999 ${i18n.baseText('personalizationModal.people')}`,
}, value: COMPANY_SIZE_500_999,
}, },
{ {
name: ROLE_KEY, label: `1000+ ${i18n.baseText('personalizationModal.people')}`,
properties: { value: COMPANY_SIZE_1000_OR_MORE,
type: 'select', },
label: i18n.baseText('personalizationModal.whichRoleBestDescribesYou'), {
placeholder: i18n.baseText('personalizationModal.select'), label: i18n.baseText('personalizationModal.imNotUsingN8nForWork'),
options: [ value: COMPANY_SIZE_PERSONAL_USE,
{ },
value: ROLE_BUSINESS_OWNER, ],
label: i18n.baseText('personalizationModal.businessOwner'),
}, },
{ shouldDisplay(values): boolean {
value: ROLE_CUSTOMER_SUPPORT, const companyType = (values as IPersonalizationLatestVersion)[COMPANY_TYPE_KEY];
label: i18n.baseText('personalizationModal.customerSupport'), 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'),
}, },
{ shouldDisplay(values): boolean {
value: ROLE_IT, const reportedSource = (values as IPersonalizationLatestVersion)[REPORTED_SOURCE_KEY];
label: i18n.baseText('personalizationModal.it'), return reportedSource === REPORTED_SOURCE_OTHER;
}, },
{ },
value: ROLE_ENGINEERING, ] as const,
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;
},
},
]);
const onSave = () => { const onSave = () => {
formBus.emit('submit'); formBus.emit('submit');
@@ -575,7 +578,7 @@ const closeDialog = () => {
} }
}; };
const onSubmit = async (values: IPersonalizationLatestVersion) => { const onSubmit = async (values: object) => {
isSaving.value = true; isSaving.value = true;
try { try {

View File

@@ -4,11 +4,7 @@ import { useI18n } from '@n8n/i18n';
import { ResourceType, splitName } from '@/utils/projects.utils'; import { ResourceType, splitName } from '@/utils/projects.utils';
import type { Project, ProjectIcon as BadgeIcon } from '@/types/projects.types'; import type { Project, ProjectIcon as BadgeIcon } from '@/types/projects.types';
import { ProjectTypes } from '@/types/projects.types'; import { ProjectTypes } from '@/types/projects.types';
import type { import type { CredentialsResource, FolderResource, WorkflowResource } from '@/Interface';
CredentialsResource,
FolderResource,
WorkflowResource,
} from '../layouts/ResourcesListLayout.vue';
import { VIEWS } from '@/constants'; import { VIEWS } from '@/constants';
type Props = { type Props = {

View File

@@ -1,15 +1,11 @@
<script lang="ts" setup> <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 { 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<{ defineProps<{
actions: Action[]; actions: Array<UserAction<IUser>>;
disabled?: boolean; disabled?: boolean;
type?: ButtonType; type?: ButtonType;
}>(); }>();
@@ -18,7 +14,7 @@ const emit = defineEmits<{
action: [id: string]; action: [id: string];
}>(); }>();
const actionToggleRef = ref<InstanceType<typeof N8nActionToggle> | null>(null); const actionToggleRef = useTemplateRef('actionToggleRef');
defineExpose({ defineExpose({
openActionToggle: (isOpen: boolean) => actionToggleRef.value?.openActionToggle(isOpen), openActionToggle: (isOpen: boolean) => actionToggleRef.value?.openActionToggle(isOpen),

View File

@@ -16,6 +16,7 @@ import ProjectCreateResource from '@/components/Projects/ProjectCreateResource.v
import { useSettingsStore } from '@/stores/settings.store'; import { useSettingsStore } from '@/stores/settings.store';
import { useProjectPages } from '@/composables/useProjectPages'; import { useProjectPages } from '@/composables/useProjectPages';
import { truncateTextToFitWidth } from '@/utils/formatters/textFormatter'; import { truncateTextToFitWidth } from '@/utils/formatters/textFormatter';
import type { IUser } from 'n8n-workflow';
const route = useRoute(); const route = useRoute();
const router = useRouter(); const router = useRouter();
@@ -96,7 +97,7 @@ const createWorkflowButton = computed(() => ({
})); }));
const menu = computed(() => { const menu = computed(() => {
const items: UserAction[] = [ const items: Array<UserAction<IUser>> = [
{ {
value: ACTION_TYPES.CREDENTIAL, value: ACTION_TYPES.CREDENTIAL,
label: i18n.baseText('projects.header.create.credential'), label: i18n.baseText('projects.header.create.credential'),

View File

@@ -240,7 +240,7 @@ onMounted(async () => {
v-for="p in filteredProjects" v-for="p in filteredProjects"
:key="p.id" :key="p.id"
:value="p.id" :value="p.id"
:label="p.name" :label="p.name ?? ''"
></N8nOption> ></N8nOption>
</N8nSelect> </N8nSelect>
<N8nText> <N8nText>

View File

@@ -43,10 +43,10 @@ const shared = computed<IMenuItem>(() => ({
}, },
})); }));
const getProjectMenuItem = (project: ProjectListItem) => ({ const getProjectMenuItem = (project: ProjectListItem): IMenuItem => ({
id: project.id, id: project.id,
label: project.name, label: project.name ?? '',
icon: project.icon, icon: project.icon as IMenuItem['icon'],
route: { route: {
to: { to: {
name: VIEWS.PROJECTS_WORKFLOWS, name: VIEWS.PROJECTS_WORKFLOWS,
@@ -70,6 +70,14 @@ const personalProject = computed<IMenuItem>(() => ({
const showAddFirstProject = computed( const showAddFirstProject = computed(
() => projectsStore.isTeamProjectFeatureEnabled && !displayProjects.value.length, () => projectsStore.isTeamProjectFeatureEnabled && !displayProjects.value.length,
); );
const activeTabId = computed(() => {
return (
(Array.isArray(projectsStore.projectNavActiveId)
? projectsStore.projectNavActiveId[0]
: projectsStore.projectNavActiveId) ?? undefined
);
});
</script> </script>
<template> <template>
@@ -78,7 +86,7 @@ const showAddFirstProject = computed(
<N8nMenuItem <N8nMenuItem
:item="home" :item="home"
:compact="props.collapsed" :compact="props.collapsed"
:active-tab="projectsStore.projectNavActiveId" :active-tab="activeTabId"
mode="tabs" mode="tabs"
data-test-id="project-home-menu-item" data-test-id="project-home-menu-item"
/> />
@@ -86,7 +94,7 @@ const showAddFirstProject = computed(
v-if="projectsStore.isTeamProjectFeatureEnabled || isFoldersFeatureEnabled" v-if="projectsStore.isTeamProjectFeatureEnabled || isFoldersFeatureEnabled"
:item="personalProject" :item="personalProject"
:compact="props.collapsed" :compact="props.collapsed"
:active-tab="projectsStore.projectNavActiveId" :active-tab="activeTabId"
mode="tabs" mode="tabs"
data-test-id="project-personal-menu-item" data-test-id="project-personal-menu-item"
/> />
@@ -94,7 +102,7 @@ const showAddFirstProject = computed(
v-if="projectsStore.isTeamProjectFeatureEnabled || isFoldersFeatureEnabled" v-if="projectsStore.isTeamProjectFeatureEnabled || isFoldersFeatureEnabled"
:item="shared" :item="shared"
:compact="props.collapsed" :compact="props.collapsed"
:active-tab="projectsStore.projectNavActiveId" :active-tab="activeTabId"
mode="tabs" mode="tabs"
data-test-id="project-shared-menu-item" data-test-id="project-shared-menu-item"
/> />
@@ -136,7 +144,7 @@ const showAddFirstProject = computed(
}" }"
:item="getProjectMenuItem(project)" :item="getProjectMenuItem(project)"
:compact="props.collapsed" :compact="props.collapsed"
:active-tab="projectsStore.projectNavActiveId" :active-tab="activeTabId"
mode="tabs" mode="tabs"
data-test-id="project-menu-item" data-test-id="project-menu-item"
/> />

View File

@@ -136,7 +136,7 @@ watch(
v-for="project in filteredProjects" v-for="project in filteredProjects"
:key="project.id" :key="project.id"
:value="project.id" :value="project.id"
:label="project.name" :label="project.name ?? ''"
> >
<ProjectSharingInfo :project="project" /> <ProjectSharingInfo :project="project" />
</N8nOption> </N8nOption>

View File

@@ -5,6 +5,7 @@ import { useRoute } from 'vue-router';
import { VIEWS } from '@/constants'; import { VIEWS } from '@/constants';
import { useI18n } from '@n8n/i18n'; import { useI18n } from '@n8n/i18n';
import type { BaseTextKey } from '@n8n/i18n'; import type { BaseTextKey } from '@n8n/i18n';
import type { TabOptions } from '@n8n/design-system';
type Props = { type Props = {
showSettings?: boolean; showSettings?: boolean;
@@ -23,6 +24,8 @@ const route = useRoute();
const selectedTab = ref<RouteRecordName | null | undefined>(''); const selectedTab = ref<RouteRecordName | null | undefined>('');
const selectedTabLabel = computed(() => (selectedTab.value ? String(selectedTab.value) : ''));
const projectId = computed(() => { const projectId = computed(() => {
return Array.isArray(route?.params?.projectId) return Array.isArray(route?.params?.projectId)
? route.params.projectId[0] ? route.params.projectId[0]
@@ -70,16 +73,16 @@ const createTab = (
label: BaseTextKey, label: BaseTextKey,
routeKey: string, routeKey: string,
routes: Record<string, { name: RouteRecordName; params?: Record<string, string | number> }>, routes: Record<string, { name: RouteRecordName; params?: Record<string, string | number> }>,
) => { ): TabOptions<string> => {
return { return {
label: locale.baseText(label), label: locale.baseText(label),
value: routes[routeKey].name, value: routes[routeKey].name as string,
to: routes[routeKey], to: routes[routeKey],
}; };
}; };
// Generate the tabs configuration // Generate the tabs configuration
const options = computed(() => { const options = computed<Array<TabOptions<string>>>(() => {
const routes = getRouteConfigs(); const routes = getRouteConfigs();
const tabs = [ const tabs = [
createTab('mainSidebar.workflows', 'workflows', routes), createTab('mainSidebar.workflows', 'workflows', routes),
@@ -93,7 +96,7 @@ const options = computed(() => {
if (props.showSettings) { if (props.showSettings) {
tabs.push({ tabs.push({
label: locale.baseText('projects.settings'), label: locale.baseText('projects.settings'),
value: VIEWS.PROJECT_SETTINGS, value: VIEWS.PROJECT_SETTINGS as string,
to: { name: VIEWS.PROJECT_SETTINGS, params: { projectId: projectId.value } }, to: { name: VIEWS.PROJECT_SETTINGS, params: { projectId: projectId.value } },
}); });
} }
@@ -110,8 +113,17 @@ watch(
}, },
{ immediate: true }, { immediate: true },
); );
function onSelectTab(value: string | number) {
selectedTab.value = value as RouteRecordName;
}
</script> </script>
<template> <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> </template>

View File

@@ -4,7 +4,7 @@ import Modal from '../Modal.vue';
import { PROMPT_MFA_CODE_MODAL_KEY } from '@/constants'; import { PROMPT_MFA_CODE_MODAL_KEY } from '@/constants';
import { useI18n } from '@n8n/i18n'; import { useI18n } from '@n8n/i18n';
import { promptMfaCodeBus } from '@/event-bus'; import { promptMfaCodeBus } from '@/event-bus';
import type { IFormInputs } from '@/Interface'; import { type IFormInput } from '@/Interface';
import { createFormEventBus } from '@n8n/design-system/utils'; import { createFormEventBus } from '@n8n/design-system/utils';
import { validate as validateUuid } from 'uuid'; import { validate as validateUuid } from 'uuid';
@@ -13,7 +13,7 @@ const i18n = useI18n();
const formBus = createFormEventBus(); const formBus = createFormEventBus();
const readyToSubmit = ref(false); const readyToSubmit = ref(false);
const formFields: IFormInputs = [ const formFields: IFormInput[] = [
{ {
name: 'mfaCodeOrMfaRecoveryCode', name: 'mfaCodeOrMfaRecoveryCode',
initialValue: '', initialValue: '',
@@ -25,9 +25,14 @@ const formFields: IFormInputs = [
required: true, 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)) { if (validateUuid(values.mfaCodeOrMfaRecoveryCode)) {
promptMfaCodeBus.emit('close', { promptMfaCodeBus.emit('close', {
mfaRecoveryCode: values.mfaCodeOrMfaRecoveryCode, mfaRecoveryCode: values.mfaCodeOrMfaRecoveryCode,

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