mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-15 17:16:45 +00:00
fix(editor): Fix broken types for globally defined components (no-changelog) (#16505)
Co-authored-by: Mutasem Aldmour <mutasem@n8n.io>
This commit is contained in:
@@ -8,6 +8,7 @@
|
||||
"files": {
|
||||
"ignore": [
|
||||
"**/.turbo",
|
||||
"**/components.d.ts",
|
||||
"**/coverage",
|
||||
"**/dist",
|
||||
"**/package.json",
|
||||
|
||||
3
packages/frontend/@n8n/chat/.gitignore
vendored
3
packages/frontend/@n8n/chat/.gitignore
vendored
@@ -26,3 +26,6 @@ coverage
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
# Auto-generated files
|
||||
src/components.d.ts
|
||||
|
||||
@@ -12,8 +12,8 @@
|
||||
"typecheck": "vue-tsc --noEmit",
|
||||
"lint": "eslint . --ext .js,.ts,.vue --quiet",
|
||||
"lintfix": "eslint . --ext .js,.ts,.vue --fix",
|
||||
"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": "biome format --write src .storybook && prettier --write src/ --ignore-path ../../../../.prettierignore",
|
||||
"format:check": "biome ci src .storybook && prettier --check src/ --ignore-path ../../../../.prettierignore",
|
||||
"storybook": "storybook dev -p 6006 --no-open",
|
||||
"build:storybook": "storybook build"
|
||||
},
|
||||
@@ -36,6 +36,7 @@
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@n8n/design-system": "workspace:*",
|
||||
"@vueuse/core": "catalog:frontend",
|
||||
"highlight.js": "catalog:frontend",
|
||||
"markdown-it-link-attributes": "^4.0.1",
|
||||
|
||||
@@ -4,7 +4,8 @@ import hljsJavascript from 'highlight.js/lib/languages/javascript';
|
||||
import hljsXML from 'highlight.js/lib/languages/xml';
|
||||
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';
|
||||
|
||||
defineProps({});
|
||||
|
||||
@@ -105,7 +105,7 @@ onMounted(async () => {
|
||||
|
||||
<template>
|
||||
<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 }" />
|
||||
</div>
|
||||
<slot>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts" setup>
|
||||
import { N8nIcon, N8nText } from '@n8n/design-system';
|
||||
import { ref, watch } from 'vue';
|
||||
|
||||
import Message from '@n8n/chat/components/Message.vue';
|
||||
|
||||
16
packages/frontend/@n8n/chat/src/env.d.ts
vendored
16
packages/frontend/@n8n/chat/src/env.d.ts
vendored
@@ -1 +1,17 @@
|
||||
/// <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;
|
||||
}
|
||||
|
||||
6
packages/frontend/@n8n/chat/src/shims.d.ts
vendored
6
packages/frontend/@n8n/chat/src/shims.d.ts
vendored
@@ -1,6 +0,0 @@
|
||||
declare module '*.vue' {
|
||||
import { defineComponent } from 'vue';
|
||||
|
||||
const component: ReturnType<typeof defineComponent>;
|
||||
export default component;
|
||||
}
|
||||
@@ -6,6 +6,7 @@
|
||||
"baseUrl": "src",
|
||||
"target": "esnext",
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowJs": true,
|
||||
"importHelpers": true,
|
||||
"incremental": false,
|
||||
@@ -13,7 +14,8 @@
|
||||
"resolveJsonModule": true,
|
||||
"types": ["vitest/globals"],
|
||||
"paths": {
|
||||
"@n8n/chat/*": ["./*"]
|
||||
"@n8n/chat/*": ["./*"],
|
||||
"@n8n/design-system*": ["../../design-system/src*"]
|
||||
},
|
||||
"lib": ["esnext", "dom", "dom.iterable", "scripthost"],
|
||||
// TODO: remove all options below this line
|
||||
|
||||
@@ -5,9 +5,12 @@ import vue from '@vitejs/plugin-vue';
|
||||
import icons from 'unplugin-icons/vite';
|
||||
import dts from 'vite-plugin-dts';
|
||||
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 srcPath = resolve(__dirname, 'src');
|
||||
const packagesDir = resolve(__dirname, '..', '..', '..');
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default mergeConfig(
|
||||
@@ -19,6 +22,18 @@ export default mergeConfig(
|
||||
autoInstall: true,
|
||||
}),
|
||||
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',
|
||||
closeBundle() {
|
||||
@@ -36,10 +51,24 @@ export default mergeConfig(
|
||||
},
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': srcPath,
|
||||
'@n8n/chat': srcPath,
|
||||
},
|
||||
alias: [
|
||||
{
|
||||
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: {
|
||||
'process.env.NODE_ENV': process.env.NODE_ENV ? `"${process.env.NODE_ENV}"` : '"development"',
|
||||
|
||||
@@ -16,6 +16,7 @@ module.exports = {
|
||||
'@typescript-eslint/no-unsafe-member-access': 'warn',
|
||||
'@typescript-eslint/prefer-optional-chain': 'warn',
|
||||
'@typescript-eslint/prefer-nullish-coalescing': 'warn',
|
||||
'vue/no-undef-components': 'error',
|
||||
},
|
||||
|
||||
overrides: [
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
"clean": "rimraf dist .turbo",
|
||||
"build": "vite build",
|
||||
"typecheck": "vue-tsc --noEmit",
|
||||
"typecheck:watch": "vue-tsc --watch --noEmit",
|
||||
"test": "vitest run",
|
||||
"test:dev": "vitest",
|
||||
"build:storybook": "storybook build",
|
||||
|
||||
@@ -18,6 +18,9 @@ import AssistantLoadingMessage from '../AskAssistantLoadingMessage/AssistantLoad
|
||||
import AssistantText from '../AskAssistantText/AssistantText.vue';
|
||||
import BetaTag from '../BetaTag/BetaTag.vue';
|
||||
import InlineAskAssistantButton from '../InlineAskAssistantButton/InlineAskAssistantButton.vue';
|
||||
import N8nButton from '../N8nButton';
|
||||
import N8nIcon from '../N8nIcon';
|
||||
import N8nIconButton from '../N8nIconButton';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
@@ -124,7 +127,7 @@ function onSubmitFeedback(feedback: string) {
|
||||
<slot name="header" />
|
||||
</div>
|
||||
<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 :class="$style.body">
|
||||
@@ -222,14 +225,14 @@ function onSubmitFeedback(feedback: string) {
|
||||
{{ t('assistantChat.quickRepliesTitle') }}
|
||||
</div>
|
||||
<div v-for="opt in message.quickReplies" :key="opt.type" data-test-id="quick-replies">
|
||||
<n8n-button
|
||||
<N8nButton
|
||||
v-if="opt.text"
|
||||
type="secondary"
|
||||
size="mini"
|
||||
@click="() => onQuickReply(opt)"
|
||||
>
|
||||
{{ opt.text }}
|
||||
</n8n-button>
|
||||
</N8nButton>
|
||||
</div>
|
||||
</div>
|
||||
</data>
|
||||
@@ -289,10 +292,10 @@ function onSubmitFeedback(feedback: string) {
|
||||
@input.prevent="growInput"
|
||||
@keydown.stop
|
||||
/>
|
||||
<n8n-icon-button
|
||||
<N8nIconButton
|
||||
:class="{ [$style.sendButton]: true }"
|
||||
icon="paper-plane"
|
||||
type="text"
|
||||
:text="true"
|
||||
size="large"
|
||||
data-test-id="send-message-button"
|
||||
:disabled="sendDisabled"
|
||||
|
||||
@@ -69,6 +69,8 @@ exports[`AskAssistantChat > does not render retry button if no error is present
|
||||
<n8n-icon-stub
|
||||
color="text-base"
|
||||
icon="arrow-right"
|
||||
size="medium"
|
||||
spin="false"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -175,12 +177,16 @@ exports[`AskAssistantChat > does not render retry button if no error is present
|
||||
wrap="hard"
|
||||
/>
|
||||
<n8n-icon-button-stub
|
||||
active="false"
|
||||
class="sendButton"
|
||||
data-test-id="send-message-button"
|
||||
disabled="true"
|
||||
icon="paper-plane"
|
||||
loading="false"
|
||||
outline="false"
|
||||
size="large"
|
||||
type="text"
|
||||
text="true"
|
||||
type="primary"
|
||||
/>
|
||||
|
||||
</div>
|
||||
@@ -257,6 +263,8 @@ exports[`AskAssistantChat > renders chat with messages correctly 1`] = `
|
||||
<n8n-icon-stub
|
||||
color="text-base"
|
||||
icon="arrow-right"
|
||||
size="medium"
|
||||
spin="false"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -601,11 +609,18 @@ exports[`AskAssistantChat > renders chat with messages correctly 1`] = `
|
||||
class="actions"
|
||||
>
|
||||
<n8n-button-stub
|
||||
active="false"
|
||||
block="false"
|
||||
data-test-id="replace-code-button"
|
||||
disabled="false"
|
||||
element="button"
|
||||
icon="refresh"
|
||||
label=""
|
||||
loading="false"
|
||||
outline="false"
|
||||
size="mini"
|
||||
square="false"
|
||||
text="false"
|
||||
type="primary"
|
||||
/>
|
||||
</div>
|
||||
@@ -625,8 +640,9 @@ exports[`AskAssistantChat > renders chat with messages correctly 1`] = `
|
||||
>
|
||||
|
||||
<n8n-avatar-stub
|
||||
first-name="Kobi"
|
||||
last-name="Dog"
|
||||
colors="--color-primary,--color-secondary,--color-avatar-accent-1,--color-avatar-accent-2,--color-primary-tint-1"
|
||||
firstname="Kobi"
|
||||
lastname="Dog"
|
||||
size="xsmall"
|
||||
/>
|
||||
<span>
|
||||
@@ -897,11 +913,18 @@ Testing more code
|
||||
class="actions"
|
||||
>
|
||||
<n8n-button-stub
|
||||
active="false"
|
||||
block="false"
|
||||
data-test-id="replace-code-button"
|
||||
disabled="false"
|
||||
element="button"
|
||||
icon="refresh"
|
||||
label=""
|
||||
loading="false"
|
||||
outline="false"
|
||||
size="mini"
|
||||
square="false"
|
||||
text="false"
|
||||
type="primary"
|
||||
/>
|
||||
</div>
|
||||
@@ -921,7 +944,16 @@ Testing more code
|
||||
data-test-id="quick-replies"
|
||||
>
|
||||
<n8n-button-stub
|
||||
active="false"
|
||||
block="false"
|
||||
disabled="false"
|
||||
element="button"
|
||||
label=""
|
||||
loading="false"
|
||||
outline="false"
|
||||
size="mini"
|
||||
square="false"
|
||||
text="false"
|
||||
type="secondary"
|
||||
/>
|
||||
</div>
|
||||
@@ -929,7 +961,16 @@ Testing more code
|
||||
data-test-id="quick-replies"
|
||||
>
|
||||
<n8n-button-stub
|
||||
active="false"
|
||||
block="false"
|
||||
disabled="false"
|
||||
element="button"
|
||||
label=""
|
||||
loading="false"
|
||||
outline="false"
|
||||
size="mini"
|
||||
square="false"
|
||||
text="false"
|
||||
type="secondary"
|
||||
/>
|
||||
</div>
|
||||
@@ -954,12 +995,16 @@ Testing more code
|
||||
wrap="hard"
|
||||
/>
|
||||
<n8n-icon-button-stub
|
||||
active="false"
|
||||
class="sendButton"
|
||||
data-test-id="send-message-button"
|
||||
disabled="true"
|
||||
icon="paper-plane"
|
||||
loading="false"
|
||||
outline="false"
|
||||
size="large"
|
||||
type="text"
|
||||
text="true"
|
||||
type="primary"
|
||||
/>
|
||||
|
||||
</div>
|
||||
@@ -1036,6 +1081,8 @@ exports[`AskAssistantChat > renders default placeholder chat correctly 1`] = `
|
||||
<n8n-icon-stub
|
||||
color="text-base"
|
||||
icon="arrow-right"
|
||||
size="medium"
|
||||
spin="false"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1135,12 +1182,16 @@ exports[`AskAssistantChat > renders default placeholder chat correctly 1`] = `
|
||||
wrap="hard"
|
||||
/>
|
||||
<n8n-icon-button-stub
|
||||
active="false"
|
||||
class="sendButton"
|
||||
data-test-id="send-message-button"
|
||||
disabled="true"
|
||||
icon="paper-plane"
|
||||
loading="false"
|
||||
outline="false"
|
||||
size="large"
|
||||
type="text"
|
||||
text="true"
|
||||
type="primary"
|
||||
/>
|
||||
|
||||
</div>
|
||||
@@ -1217,6 +1268,8 @@ exports[`AskAssistantChat > renders end of session chat correctly 1`] = `
|
||||
<n8n-icon-stub
|
||||
color="text-base"
|
||||
icon="arrow-right"
|
||||
size="medium"
|
||||
spin="false"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1400,12 +1453,16 @@ exports[`AskAssistantChat > renders end of session chat correctly 1`] = `
|
||||
wrap="hard"
|
||||
/>
|
||||
<n8n-icon-button-stub
|
||||
active="false"
|
||||
class="sendButton"
|
||||
data-test-id="send-message-button"
|
||||
disabled="true"
|
||||
icon="paper-plane"
|
||||
loading="false"
|
||||
outline="false"
|
||||
size="large"
|
||||
type="text"
|
||||
text="true"
|
||||
type="primary"
|
||||
/>
|
||||
|
||||
</div>
|
||||
@@ -1482,6 +1539,8 @@ exports[`AskAssistantChat > renders error message correctly with retry button 1`
|
||||
<n8n-icon-stub
|
||||
color="text-base"
|
||||
icon="arrow-right"
|
||||
size="medium"
|
||||
spin="false"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1558,13 +1617,23 @@ exports[`AskAssistantChat > renders error message correctly with retry button 1`
|
||||
class="errorIcon"
|
||||
icon="exclamation-triangle"
|
||||
size="small"
|
||||
spin="false"
|
||||
/>
|
||||
This is an error message.
|
||||
</p>
|
||||
<n8n-button-stub
|
||||
active="false"
|
||||
block="false"
|
||||
class="retryButton"
|
||||
data-test-id="error-retry-button"
|
||||
disabled="false"
|
||||
element="button"
|
||||
label=""
|
||||
loading="false"
|
||||
outline="false"
|
||||
size="mini"
|
||||
square="false"
|
||||
text="false"
|
||||
type="secondary"
|
||||
/>
|
||||
</div>
|
||||
@@ -1590,12 +1659,16 @@ exports[`AskAssistantChat > renders error message correctly with retry button 1`
|
||||
wrap="hard"
|
||||
/>
|
||||
<n8n-icon-button-stub
|
||||
active="false"
|
||||
class="sendButton"
|
||||
data-test-id="send-message-button"
|
||||
disabled="true"
|
||||
icon="paper-plane"
|
||||
loading="false"
|
||||
outline="false"
|
||||
size="large"
|
||||
type="text"
|
||||
text="true"
|
||||
type="primary"
|
||||
/>
|
||||
|
||||
</div>
|
||||
@@ -1672,6 +1745,8 @@ exports[`AskAssistantChat > renders message with code snippet 1`] = `
|
||||
<n8n-icon-stub
|
||||
color="text-base"
|
||||
icon="arrow-right"
|
||||
size="medium"
|
||||
spin="false"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1848,12 +1923,16 @@ catch(e) {
|
||||
wrap="hard"
|
||||
/>
|
||||
<n8n-icon-button-stub
|
||||
active="false"
|
||||
class="sendButton"
|
||||
data-test-id="send-message-button"
|
||||
disabled="true"
|
||||
icon="paper-plane"
|
||||
loading="false"
|
||||
outline="false"
|
||||
size="large"
|
||||
type="text"
|
||||
text="true"
|
||||
type="primary"
|
||||
/>
|
||||
|
||||
</div>
|
||||
@@ -1930,6 +2009,8 @@ exports[`AskAssistantChat > renders streaming chat correctly 1`] = `
|
||||
<n8n-icon-stub
|
||||
color="text-base"
|
||||
icon="arrow-right"
|
||||
size="medium"
|
||||
spin="false"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -2039,12 +2120,16 @@ exports[`AskAssistantChat > renders streaming chat correctly 1`] = `
|
||||
wrap="hard"
|
||||
/>
|
||||
<n8n-icon-button-stub
|
||||
active="false"
|
||||
class="sendButton"
|
||||
data-test-id="send-message-button"
|
||||
disabled="true"
|
||||
icon="paper-plane"
|
||||
loading="false"
|
||||
outline="false"
|
||||
size="large"
|
||||
type="text"
|
||||
text="true"
|
||||
type="primary"
|
||||
/>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { computed } from 'vue';
|
||||
import { useI18n } from '../../../composables/useI18n';
|
||||
import type { ChatUI } from '../../../types/assistant';
|
||||
import AssistantAvatar from '../../AskAssistantAvatar/AssistantAvatar.vue';
|
||||
import N8nAvatar from '../../N8nAvatar';
|
||||
|
||||
interface Props {
|
||||
message: ChatUI.AssistantMessage;
|
||||
@@ -27,7 +28,7 @@ const isUserMessage = computed(() => props.message.role === 'user');
|
||||
:class="{ [$style.roleName]: true, [$style.userSection]: !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>
|
||||
</template>
|
||||
<template v-else>
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
import BaseMessage from './BaseMessage.vue';
|
||||
import { useI18n } from '../../../composables/useI18n';
|
||||
import type { ChatUI } from '../../../types/assistant';
|
||||
import N8nButton from '../../N8nButton';
|
||||
import N8nIcon from '../../N8nIcon';
|
||||
|
||||
interface Props {
|
||||
message: ChatUI.ErrorMessage & { id: string; read: boolean };
|
||||
@@ -20,10 +22,10 @@ const { t } = useI18n();
|
||||
<BaseMessage :message="message" :is-first-of-role="isFirstOfRole" :user="user">
|
||||
<div :class="$style.error" data-test-id="chat-message-system">
|
||||
<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 }}
|
||||
</p>
|
||||
<n8n-button
|
||||
<N8nButton
|
||||
v-if="message.retry"
|
||||
type="secondary"
|
||||
size="mini"
|
||||
@@ -32,7 +34,7 @@ const { t } = useI18n();
|
||||
@click="() => message.retry?.()"
|
||||
>
|
||||
{{ t('generic.retry') }}
|
||||
</n8n-button>
|
||||
</N8nButton>
|
||||
</div>
|
||||
</BaseMessage>
|
||||
</template>
|
||||
|
||||
@@ -6,6 +6,7 @@ import { useMarkdown } from './useMarkdown';
|
||||
import { useI18n } from '../../../composables/useI18n';
|
||||
import type { ChatUI } from '../../../types/assistant';
|
||||
import BlinkingCursor from '../../BlinkingCursor/BlinkingCursor.vue';
|
||||
import N8nButton from '../../N8nButton';
|
||||
|
||||
interface Props {
|
||||
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"
|
||||
>
|
||||
<header v-if="isClipboardSupported">
|
||||
<n8n-button
|
||||
<N8nButton
|
||||
type="tertiary"
|
||||
text="true"
|
||||
:text="true"
|
||||
size="mini"
|
||||
data-test-id="assistant-copy-snippet-button"
|
||||
@click="onCopyButtonClick(message.codeSnippet, $event)"
|
||||
>
|
||||
{{ t('assistantChat.copy') }}
|
||||
</n8n-button>
|
||||
</N8nButton>
|
||||
</header>
|
||||
<div
|
||||
v-n8n-html="renderMarkdown(message.codeSnippet).trim()"
|
||||
|
||||
@@ -5,6 +5,8 @@ import { useI18n } from '@n8n/design-system/composables/useI18n';
|
||||
|
||||
import BaseWorkflowMessage from './BaseWorkflowMessage.vue';
|
||||
import type { ChatUI } from '../../../../types/assistant';
|
||||
import N8nButton from '../../../N8nButton';
|
||||
import N8nInput from '../../../N8nInput';
|
||||
|
||||
interface Props {
|
||||
message: ChatUI.RateWorkflowMessage & { id: string; read: boolean };
|
||||
@@ -48,7 +50,7 @@ function onSubmitFeedback() {
|
||||
<div :class="$style.content">
|
||||
<p v-if="!showSuccess">{{ message.content }}</p>
|
||||
<div v-if="!showFeedback && !showSuccess" :class="$style.buttons">
|
||||
<n8n-button
|
||||
<N8nButton
|
||||
type="secondary"
|
||||
size="small"
|
||||
:label="t('assistantChat.builder.thumbsUp')"
|
||||
@@ -56,7 +58,7 @@ function onSubmitFeedback() {
|
||||
icon="thumbs-up"
|
||||
@click="onRateButton('thumbsUp')"
|
||||
/>
|
||||
<n8n-button
|
||||
<N8nButton
|
||||
type="secondary"
|
||||
size="small"
|
||||
data-test-id="message-thumbs-down-button"
|
||||
@@ -66,7 +68,7 @@ function onSubmitFeedback() {
|
||||
/>
|
||||
</div>
|
||||
<div v-if="showFeedback" :class="$style.feedbackTextArea">
|
||||
<n8n-input
|
||||
<N8nInput
|
||||
v-model="feedback"
|
||||
:class="$style.feedbackInput"
|
||||
type="textarea"
|
||||
@@ -77,7 +79,7 @@ function onSubmitFeedback() {
|
||||
:rows="5"
|
||||
/>
|
||||
<div :class="$style.feedbackTextArea__footer">
|
||||
<n8n-button
|
||||
<N8nButton
|
||||
native-type="submit"
|
||||
type="secondary"
|
||||
size="small"
|
||||
@@ -85,7 +87,7 @@ function onSubmitFeedback() {
|
||||
@click="onSubmitFeedback"
|
||||
>
|
||||
{{ t('assistantChat.builder.submit') }}
|
||||
</n8n-button>
|
||||
</N8nButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -4,6 +4,9 @@ import { computed } from 'vue';
|
||||
|
||||
import { useI18n } from '@n8n/design-system/composables/useI18n';
|
||||
|
||||
import N8nButton from '../N8nButton';
|
||||
import N8nIcon from '../N8nIcon';
|
||||
|
||||
const MIN_LINES = 4;
|
||||
|
||||
interface Props {
|
||||
@@ -106,11 +109,11 @@ const diffs = computed(() => {
|
||||
</div>
|
||||
<div :class="$style.actions">
|
||||
<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>
|
||||
</div>
|
||||
<div v-else-if="replaced">
|
||||
<n8n-button
|
||||
<N8nButton
|
||||
type="secondary"
|
||||
size="mini"
|
||||
icon="undo"
|
||||
@@ -118,13 +121,13 @@ const diffs = computed(() => {
|
||||
@click="() => emit('undo')"
|
||||
>
|
||||
{{ t('codeDiff.undo') }}
|
||||
</n8n-button>
|
||||
<n8n-icon icon="check" color="success" class="ml-xs" />
|
||||
</N8nButton>
|
||||
<N8nIcon icon="check" color="success" class="ml-xs" />
|
||||
<span :class="$style.infoText" data-test-id="code-replaced-message">
|
||||
{{ t('codeDiff.codeReplaced') }}
|
||||
</span>
|
||||
</div>
|
||||
<n8n-button
|
||||
<N8nButton
|
||||
v-else
|
||||
:type="replacing ? 'secondary' : 'primary'"
|
||||
size="mini"
|
||||
@@ -133,7 +136,7 @@ const diffs = computed(() => {
|
||||
:disabled="!content || streaming"
|
||||
:loading="replacing"
|
||||
@click="() => emit('replace')"
|
||||
>{{ replacing ? t('codeDiff.replacing') : t('codeDiff.replaceMyCode') }}</n8n-button
|
||||
>{{ replacing ? t('codeDiff.replacing') : t('codeDiff.replaceMyCode') }}</N8nButton
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -271,11 +271,18 @@ exports[`CodeDiff > renders code diff correctly 1`] = `
|
||||
class="actions"
|
||||
>
|
||||
<n8n-button-stub
|
||||
active="false"
|
||||
block="false"
|
||||
data-test-id="replace-code-button"
|
||||
disabled="false"
|
||||
element="button"
|
||||
icon="refresh"
|
||||
label=""
|
||||
loading="false"
|
||||
outline="false"
|
||||
size="mini"
|
||||
square="false"
|
||||
text="false"
|
||||
type="primary"
|
||||
/>
|
||||
</div>
|
||||
@@ -537,6 +544,8 @@ exports[`CodeDiff > renders error state correctly 1`] = `
|
||||
class="mr-5xs"
|
||||
color="danger"
|
||||
icon="exclamation-triangle"
|
||||
size="medium"
|
||||
spin="false"
|
||||
/>
|
||||
<span
|
||||
class="infoText"
|
||||
@@ -800,15 +809,26 @@ exports[`CodeDiff > renders replaced code diff correctly 1`] = `
|
||||
>
|
||||
<div>
|
||||
<n8n-button-stub
|
||||
active="false"
|
||||
block="false"
|
||||
data-test-id="undo-replace-button"
|
||||
disabled="false"
|
||||
element="button"
|
||||
icon="undo"
|
||||
label=""
|
||||
loading="false"
|
||||
outline="false"
|
||||
size="mini"
|
||||
square="false"
|
||||
text="false"
|
||||
type="secondary"
|
||||
/>
|
||||
<n8n-icon-stub
|
||||
class="ml-xs"
|
||||
color="success"
|
||||
icon="check"
|
||||
size="medium"
|
||||
spin="false"
|
||||
/>
|
||||
<span
|
||||
class="infoText"
|
||||
@@ -1072,11 +1092,18 @@ exports[`CodeDiff > renders replacing code diff correctly 1`] = `
|
||||
class="actions"
|
||||
>
|
||||
<n8n-button-stub
|
||||
active="false"
|
||||
block="false"
|
||||
data-test-id="replace-code-button"
|
||||
disabled="false"
|
||||
element="button"
|
||||
icon="refresh"
|
||||
label=""
|
||||
loading="true"
|
||||
outline="false"
|
||||
size="mini"
|
||||
square="false"
|
||||
text="false"
|
||||
type="secondary"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -8,13 +8,13 @@ import N8nHeading from '../N8nHeading';
|
||||
import N8nText from '../N8nText';
|
||||
|
||||
interface ActionBoxProps {
|
||||
emoji: string;
|
||||
heading: string;
|
||||
emoji?: string;
|
||||
heading?: string;
|
||||
buttonText?: string;
|
||||
buttonType?: ButtonType;
|
||||
buttonDisabled?: boolean;
|
||||
buttonIcon?: string;
|
||||
description: string;
|
||||
description?: string;
|
||||
calloutText?: string;
|
||||
calloutTheme?: CalloutTheme;
|
||||
calloutIcon?: string;
|
||||
|
||||
@@ -8,10 +8,11 @@
|
||||
import { ElDropdown, ElDropdownMenu, ElDropdownItem, type Placement } from 'element-plus';
|
||||
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 N8nIconButton from '../N8nIconButton';
|
||||
import { N8nKeyboardShortcut } from '../N8nKeyboardShortcut';
|
||||
|
||||
const TRIGGER = ['click', 'hover'] as const;
|
||||
@@ -20,7 +21,7 @@ interface ActionDropdownProps {
|
||||
items: ActionDropdownItem[];
|
||||
placement?: Placement;
|
||||
activatorIcon?: string;
|
||||
activatorSize?: IconSize;
|
||||
activatorSize?: ButtonSize;
|
||||
iconSize?: IconSize;
|
||||
trigger?: (typeof TRIGGER)[number];
|
||||
hideArrow?: boolean;
|
||||
@@ -96,7 +97,7 @@ defineExpose({ open, close });
|
||||
@visible-change="onVisibleChange"
|
||||
>
|
||||
<slot v-if="$slots.activator" name="activator" />
|
||||
<n8n-icon-button
|
||||
<N8nIconButton
|
||||
v-else
|
||||
type="tertiary"
|
||||
text
|
||||
|
||||
@@ -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 { 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 N8nIcon from '../N8nIcon';
|
||||
import N8nLoading from '../N8nLoading';
|
||||
|
||||
const SIZE = ['mini', 'small', 'medium'] as const;
|
||||
const THEME = ['default', 'dark'] as const;
|
||||
|
||||
interface ActionToggleProps {
|
||||
actions?: UserAction[];
|
||||
interface ActionToggleProps<UserType extends IUser, Actions extends Array<UserAction<UserType>>> {
|
||||
actions?: Actions;
|
||||
placement?: Placement;
|
||||
size?: (typeof SIZE)[number];
|
||||
iconSize?: IconSize;
|
||||
@@ -24,8 +25,10 @@ interface ActionToggleProps {
|
||||
trigger?: 'click' | 'hover';
|
||||
}
|
||||
|
||||
type ActionValue = Actions[number]['value'];
|
||||
|
||||
defineOptions({ name: 'N8nActionToggle' });
|
||||
withDefaults(defineProps<ActionToggleProps>(), {
|
||||
withDefaults(defineProps<ActionToggleProps<UserType, Array<UserAction<UserType>>>>(), {
|
||||
actions: () => [],
|
||||
placement: 'bottom',
|
||||
size: 'medium',
|
||||
@@ -42,9 +45,9 @@ withDefaults(defineProps<ActionToggleProps>(), {
|
||||
const actionToggleRef = ref<InstanceType<typeof ElDropdown> | null>(null);
|
||||
|
||||
const emit = defineEmits<{
|
||||
action: [value: string];
|
||||
action: [value: ActionValue];
|
||||
'visible-change': [value: boolean];
|
||||
'item-mouseup': [action: UserAction];
|
||||
'item-mouseup': [action: UserAction<UserType>];
|
||||
}>();
|
||||
|
||||
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);
|
||||
actionToggleRef.value?.handleClose();
|
||||
};
|
||||
|
||||
@@ -5,8 +5,8 @@ import Avatar from 'vue-boring-avatars';
|
||||
import { getInitials } from '../../utils/labelUtil';
|
||||
|
||||
interface AvatarProps {
|
||||
firstName?: string;
|
||||
lastName?: string;
|
||||
firstName?: string | null;
|
||||
lastName?: string | null;
|
||||
size?: 'xsmall' | 'small' | 'medium' | 'large';
|
||||
colors?: string[];
|
||||
}
|
||||
|
||||
@@ -1,20 +1,10 @@
|
||||
<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';
|
||||
|
||||
const THEME = [
|
||||
'default',
|
||||
'success',
|
||||
'warning',
|
||||
'danger',
|
||||
'primary',
|
||||
'secondary',
|
||||
'tertiary',
|
||||
] as const;
|
||||
|
||||
interface BadgeProps {
|
||||
theme?: (typeof THEME)[number];
|
||||
theme?: BadgeTheme;
|
||||
size?: TextSize;
|
||||
bold?: boolean;
|
||||
showBorder?: boolean;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
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 Breadcrumbs from './Breadcrumbs.vue';
|
||||
@@ -125,7 +125,7 @@ SyncLoadingCacheTest.args = {
|
||||
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 Workflow', value: 'action2', disabled: false },
|
||||
{ label: 'Rename', value: 'action3', disabled: false },
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
<script lang="ts" setup>
|
||||
<script lang="ts" setup generic="UserType extends IUser">
|
||||
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 N8nText from '../N8nText';
|
||||
import N8nTooltip from '../N8nTooltip';
|
||||
|
||||
export type PathItem = {
|
||||
id: string;
|
||||
@@ -66,7 +70,7 @@ const dropdownDisabled = computed(() => {
|
||||
return props.pathTruncated && !hasHiddenItems.value;
|
||||
});
|
||||
|
||||
const hiddenItemActions = computed((): UserAction[] => {
|
||||
const hiddenItemActions = computed((): Array<UserAction<UserType>> => {
|
||||
return loadedHiddenItems.value.map((item) => ({
|
||||
value: item.id,
|
||||
label: item.label,
|
||||
@@ -132,7 +136,7 @@ const emitItemHover = (id: string) => {
|
||||
emit('itemHover', item);
|
||||
};
|
||||
|
||||
const onHiddenItemMouseUp = (item: UserAction) => {
|
||||
const onHiddenItemMouseUp = (item: UserAction<UserType>) => {
|
||||
const pathItem = [...props.items, ...loadedHiddenItems.value].find((i) => i.id === item.value);
|
||||
if (!pathItem || !props.dragActive) {
|
||||
return;
|
||||
@@ -177,16 +181,13 @@ const handleTooltipClose = () => {
|
||||
>
|
||||
<!-- Show interactive dropdown for larger versions -->
|
||||
<div v-if="props.theme !== 'small'" :class="$style['hidden-items-menu']">
|
||||
<n8n-action-toggle
|
||||
<N8nActionToggle
|
||||
:actions="hiddenItemActions"
|
||||
:loading="isLoadingHiddenItems"
|
||||
:loading-row-count="loadingSkeletonRows"
|
||||
:disabled="dropdownDisabled"
|
||||
:class="$style['action-toggle']"
|
||||
:popper-class="{
|
||||
[$style['hidden-items-menu-popper']]: true,
|
||||
[$style.dragging]: dragActive,
|
||||
}"
|
||||
:popper-class="`${$style['hidden-items-menu-popper']} ${dragActive ? $style.dragging : ''}`"
|
||||
:trigger="hiddenItemsTrigger"
|
||||
theme="dark"
|
||||
placement="bottom"
|
||||
@@ -197,11 +198,11 @@ const handleTooltipClose = () => {
|
||||
@action="emitItemSelected"
|
||||
@item-mouseup="onHiddenItemMouseUp"
|
||||
>
|
||||
<n8n-text :bold="true" :class="$style.dots">...</n8n-text>
|
||||
</n8n-action-toggle>
|
||||
<N8nText :bold="true" :class="$style.dots">...</N8nText>
|
||||
</N8nActionToggle>
|
||||
</div>
|
||||
<!-- Just a tooltip for smaller versions -->
|
||||
<n8n-tooltip
|
||||
<N8nTooltip
|
||||
v-else
|
||||
:popper-class="$style.tooltip"
|
||||
:disabled="dropdownDisabled"
|
||||
@@ -222,12 +223,12 @@ const handleTooltipClose = () => {
|
||||
</div>
|
||||
<div v-else :class="$style.tooltipContent">
|
||||
<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>
|
||||
</template>
|
||||
<span :class="$style['tooltip-ellipsis']">...</span>
|
||||
</n8n-tooltip>
|
||||
</N8nTooltip>
|
||||
</li>
|
||||
<li v-if="showEllipsis" :class="$style.separator">{{ separator }}</li>
|
||||
<template v-for="(item, index) in items" :key="item.id">
|
||||
@@ -245,8 +246,8 @@ const handleTooltipClose = () => {
|
||||
@mouseenter="emitItemHover(item.id)"
|
||||
@mouseup="onItemMouseUp(item)"
|
||||
>
|
||||
<n8n-link v-if="item.href" :href="item.href" theme="text">{{ item.label }}</n8n-link>
|
||||
<n8n-text v-else>{{ item.label }}</n8n-text>
|
||||
<N8nLink v-if="item.href" :href="item.href" theme="text">{{ item.label }}</N8nLink>
|
||||
<N8nText v-else>{{ item.label }}</N8nText>
|
||||
</li>
|
||||
<li v-if="index !== items.length - 1" :class="$style.separator">
|
||||
{{ separator }}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
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 N8nIcon from '../N8nIcon';
|
||||
@@ -34,7 +35,10 @@ const ariaBusy = computed(() => (props.loading ? 'true' : undefined));
|
||||
const ariaDisabled = computed(() => (props.disabled ? 'true' : undefined));
|
||||
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(() => {
|
||||
return (
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
<script lang="ts" setup>
|
||||
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 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> = {
|
||||
info: 'info-circle',
|
||||
success: 'check-circle',
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
import N8nCallout from './Callout.vue';
|
||||
export type { CalloutTheme } from './Callout.vue';
|
||||
export type { CalloutTheme } from '../../types';
|
||||
export default N8nCallout;
|
||||
|
||||
@@ -32,7 +32,7 @@ import type {
|
||||
Updater,
|
||||
} 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 { computed, h, ref, shallowRef, useSlots, watch } from 'vue';
|
||||
|
||||
@@ -428,7 +428,7 @@ const table = useVueTable({
|
||||
:key="coll.id"
|
||||
class="el-skeleton is-animated"
|
||||
>
|
||||
<el-skeleton-item />
|
||||
<ElSkeletonItem />
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
@@ -470,14 +470,14 @@ const table = useVueTable({
|
||||
</N8nPagination>
|
||||
<div class="table-pagination__sizes">
|
||||
<div class="table-pagination__sizes__label">Page size</div>
|
||||
<el-select
|
||||
<ElSelect
|
||||
v-model.number="itemsPerPage"
|
||||
class="table-pagination__sizes__select"
|
||||
size="small"
|
||||
:teleported="false"
|
||||
>
|
||||
<el-option v-for="item in pageSizes" :key="item" :label="item" :value="item" />
|
||||
</el-select>
|
||||
<ElOption v-for="item in pageSizes" :key="item" :label="item" :value="item" />
|
||||
</ElSelect>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<script lang="ts" setup>
|
||||
<script lang="ts" setup generic="Item extends DatatableRow">
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { useI18n } from '../../composables/useI18n';
|
||||
@@ -13,7 +13,7 @@ const ALL_ROWS = -1;
|
||||
|
||||
interface DatatableProps {
|
||||
columns: DatatableColumn[];
|
||||
rows: DatatableRow[];
|
||||
rows: Item[];
|
||||
currentPage?: number;
|
||||
pagination?: boolean;
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
<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 N8nButton from '../N8nButton';
|
||||
@@ -17,12 +22,10 @@ interface FormBoxProps {
|
||||
redirectLink?: string;
|
||||
}
|
||||
|
||||
type Value = string | number | boolean | null | undefined;
|
||||
|
||||
defineOptions({ name: 'N8nFormBox' });
|
||||
withDefaults(defineProps<FormBoxProps>(), {
|
||||
title: '',
|
||||
inputs: () => [],
|
||||
inputs: (): IFormInput[] => [],
|
||||
buttonLoading: false,
|
||||
redirectText: '',
|
||||
redirectLink: '',
|
||||
@@ -30,13 +33,13 @@ withDefaults(defineProps<FormBoxProps>(), {
|
||||
|
||||
const formBus = createFormEventBus();
|
||||
const emit = defineEmits<{
|
||||
submit: [value: { [key: string]: Value }];
|
||||
update: [value: { name: string; value: Value }];
|
||||
submit: [value: FormValues];
|
||||
update: [value: FormFieldValueUpdate];
|
||||
secondaryClick: [value: Event];
|
||||
}>();
|
||||
|
||||
const onUpdateModelValue = (e: { name: string; value: Value }) => emit('update', e);
|
||||
const onSubmit = (e: { [key: string]: Value }) => emit('submit', e);
|
||||
const onUpdateModelValue = (e: { name: string; value: FormFieldValue }) => emit('update', e);
|
||||
const onSubmit = (e: { [key: string]: FormFieldValue }) => emit('submit', e);
|
||||
const onButtonClick = () => formBus.emit('submit');
|
||||
const onSecondaryButtonClick = (event: Event) => emit('secondaryClick', event);
|
||||
</script>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts" setup>
|
||||
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 { t } from '../../locale';
|
||||
@@ -19,6 +19,7 @@ import type {
|
||||
import N8nCheckbox from '../N8nCheckbox';
|
||||
import N8nInput from '../N8nInput';
|
||||
import N8nInputLabel from '../N8nInputLabel';
|
||||
import N8nLink from '../N8nLink';
|
||||
import N8nOption from '../N8nOption';
|
||||
import N8nSelect from '../N8nSelect';
|
||||
|
||||
@@ -77,8 +78,6 @@ const state = reactive({
|
||||
isTyping: false,
|
||||
});
|
||||
|
||||
const slots = useSlots();
|
||||
|
||||
const inputRef = ref<HTMLTextAreaElement | null>(null);
|
||||
|
||||
function getInputValidationError(): ReturnType<IValidator['validate']> {
|
||||
@@ -160,8 +159,6 @@ const validationError = computed<{ message: string } | null>(() => {
|
||||
return null;
|
||||
});
|
||||
|
||||
const hasDefaultSlot = computed(() => !!slots.default);
|
||||
|
||||
const showErrors = computed(
|
||||
() =>
|
||||
!!validationError.value &&
|
||||
@@ -217,7 +214,7 @@ defineExpose({ inputRef });
|
||||
:size="labelSize"
|
||||
>
|
||||
<div :class="showErrors ? $style.errorInput : ''" @keydown.stop @keydown.enter.exact="onEnter">
|
||||
<slot v-if="hasDefaultSlot" />
|
||||
<slot v-if="$slots.default" />
|
||||
<N8nSelect
|
||||
v-else-if="type === 'select' || type === 'multi-select'"
|
||||
ref="inputRef"
|
||||
@@ -261,7 +258,7 @@ defineExpose({ inputRef });
|
||||
</div>
|
||||
<div v-if="showErrors" :class="$style.errors">
|
||||
<span v-text="validationError?.message" />
|
||||
<n8n-link
|
||||
<N8nLink
|
||||
v-if="documentationUrl && documentationText"
|
||||
:to="documentationUrl"
|
||||
:new-window="true"
|
||||
@@ -269,7 +266,7 @@ defineExpose({ inputRef });
|
||||
theme="danger"
|
||||
>
|
||||
{{ documentationText }}
|
||||
</n8n-link>
|
||||
</N8nLink>
|
||||
</div>
|
||||
<div v-else-if="infoText" :class="$style.infoText">
|
||||
<span size="small" v-text="infoText" />
|
||||
|
||||
@@ -1,24 +1,22 @@
|
||||
<script setup lang="ts">
|
||||
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 { createFormEventBus } from '../../utils';
|
||||
import N8nFormInput from '../N8nFormInput';
|
||||
import N8nText from '../N8nText';
|
||||
import ResizeObserver from '../ResizeObserver';
|
||||
|
||||
export type FormInputsProps = {
|
||||
inputs?: IFormInput[];
|
||||
export interface FormInputsProps {
|
||||
inputs: IFormInput[];
|
||||
eventBus?: FormEventBus;
|
||||
columnView?: boolean;
|
||||
verticalSpacing?: '' | 'xs' | 's' | 'm' | 'l' | 'xl';
|
||||
teleported?: boolean;
|
||||
};
|
||||
|
||||
type Value = string | number | boolean | null | undefined;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<FormInputsProps>(), {
|
||||
inputs: () => [],
|
||||
eventBus: createFormEventBus,
|
||||
columnView: false,
|
||||
verticalSpacing: '',
|
||||
@@ -26,14 +24,14 @@ const props = withDefaults(defineProps<FormInputsProps>(), {
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
update: [value: { name: string; value: Value }];
|
||||
'update:modelValue': [value: Record<string, Value>];
|
||||
submit: [value: Record<string, Value>];
|
||||
update: [value: FormFieldValueUpdate];
|
||||
'update:modelValue': [value: FormValues];
|
||||
submit: [value: FormValues];
|
||||
ready: [value: boolean];
|
||||
}>();
|
||||
|
||||
const showValidationWarnings = ref(false);
|
||||
const values = reactive<Record<string, Value>>({});
|
||||
const values = reactive<FormValues>({});
|
||||
const validity = ref<Record<string, boolean>>({});
|
||||
|
||||
const filteredInputs = computed(() => {
|
||||
@@ -50,7 +48,7 @@ watch(isReadyToSubmit, (ready) => {
|
||||
emit('ready', ready);
|
||||
});
|
||||
|
||||
function onUpdateModelValue(name: string, value: Value) {
|
||||
function onUpdateModelValue(name: string, value: FormFieldValue) {
|
||||
values[name] = value;
|
||||
emit('update', { name, value });
|
||||
emit('update:modelValue', values);
|
||||
@@ -76,12 +74,15 @@ function onSubmit() {
|
||||
return;
|
||||
}
|
||||
|
||||
const toSubmit = filteredInputs.value.reduce<Record<string, Value>>((valuesToSubmit, input) => {
|
||||
if (values[input.name]) {
|
||||
valuesToSubmit[input.name] = values[input.name];
|
||||
}
|
||||
return valuesToSubmit;
|
||||
}, {});
|
||||
const toSubmit = filteredInputs.value.reduce<Record<string, FormFieldValue>>(
|
||||
(valuesToSubmit, input) => {
|
||||
if (values[input.name]) {
|
||||
valuesToSubmit[input.name] = values[input.name];
|
||||
}
|
||||
return valuesToSubmit;
|
||||
},
|
||||
{},
|
||||
);
|
||||
|
||||
emit('submit', toSubmit);
|
||||
}
|
||||
@@ -108,7 +109,7 @@ onMounted(() => {
|
||||
:key="input.name"
|
||||
:class="{ [`mt-${verticalSpacing}`]: verticalSpacing && index > 0 }"
|
||||
>
|
||||
<n8n-text
|
||||
<N8nText
|
||||
v-if="input.properties.type === 'info'"
|
||||
color="text-base"
|
||||
tag="div"
|
||||
@@ -117,7 +118,7 @@ onMounted(() => {
|
||||
class="form-text"
|
||||
>
|
||||
{{ input.properties.label }}
|
||||
</n8n-text>
|
||||
</N8nText>
|
||||
<N8nFormInput
|
||||
v-else
|
||||
v-bind="input.properties"
|
||||
@@ -127,7 +128,7 @@ onMounted(() => {
|
||||
:data-test-id="input.name"
|
||||
:show-validation-warnings="showValidationWarnings"
|
||||
: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)"
|
||||
@enter="onSubmit"
|
||||
/>
|
||||
|
||||
@@ -60,6 +60,7 @@ describe('IconPicker', () => {
|
||||
global: {
|
||||
plugins: [router],
|
||||
components,
|
||||
stubs: ['N8nButton'],
|
||||
},
|
||||
});
|
||||
const TEST_EMOJI_COUNT = 1962;
|
||||
@@ -90,11 +91,12 @@ describe('IconPicker', () => {
|
||||
global: {
|
||||
plugins: [router],
|
||||
components,
|
||||
stubs: ['N8nButton'],
|
||||
},
|
||||
});
|
||||
await userEvent.hover(getByTestId('icon-picker-button'));
|
||||
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 () => {
|
||||
const ICON = '🔥';
|
||||
@@ -124,6 +126,7 @@ describe('IconPicker', () => {
|
||||
global: {
|
||||
plugins: [router],
|
||||
components,
|
||||
stubs: ['N8nButton'],
|
||||
},
|
||||
});
|
||||
expect(queryByTestId('tab-icons')).not.toBeInTheDocument();
|
||||
@@ -138,13 +141,14 @@ describe('IconPicker', () => {
|
||||
global: {
|
||||
plugins: [router],
|
||||
components,
|
||||
stubs: ['N8nButton'],
|
||||
},
|
||||
});
|
||||
await fireEvent.click(getByTestId('icon-picker-button'));
|
||||
// Select the first icon
|
||||
await fireEvent.click(getAllByTestId('icon-picker-icon')[0]);
|
||||
// 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(emitted()).toHaveProperty('update:modelValue');
|
||||
// Should emit the selected icon
|
||||
|
||||
@@ -6,6 +6,10 @@ import { isEmojiSupported } from 'is-emoji-supported';
|
||||
import { ref, computed } from 'vue';
|
||||
|
||||
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';
|
||||
|
||||
/**
|
||||
|
||||
@@ -6,13 +6,14 @@ import type { IconColor } from '@n8n/design-system/types/icon';
|
||||
|
||||
import N8nIcon from '../N8nIcon';
|
||||
import N8nText from '../N8nText';
|
||||
import N8nTooltip from '../N8nTooltip';
|
||||
|
||||
interface IAccordionItem {
|
||||
export interface IAccordionItem {
|
||||
id: string;
|
||||
label: string;
|
||||
icon: string;
|
||||
iconColor?: IconColor;
|
||||
tooltip?: string;
|
||||
tooltip?: string | null;
|
||||
}
|
||||
|
||||
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 -->
|
||||
<div v-if="items.length > 0" :class="$style.accordionItems">
|
||||
<div v-for="item in items" :key="item.id" :class="$style.accordionItem">
|
||||
<n8n-tooltip :disabled="!item.tooltip">
|
||||
<N8nTooltip :disabled="!item.tooltip">
|
||||
<template #content>
|
||||
<div v-n8n-html="item.tooltip" @click="onTooltipClick(item.id, $event)"></div>
|
||||
</template>
|
||||
<N8nIcon :icon="item.icon" :color="item.iconColor" size="small" class="mr-2xs" />
|
||||
</n8n-tooltip>
|
||||
</N8nTooltip>
|
||||
<N8nText size="small" color="text-base">{{ item.label }}</N8nText>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -8,7 +8,7 @@ import type { InputSize, InputType } from '@n8n/design-system/types/input';
|
||||
import { uid } from '../../utils';
|
||||
|
||||
interface InputProps {
|
||||
modelValue?: string | number;
|
||||
modelValue?: string | number | null;
|
||||
type?: InputType;
|
||||
size?: InputSize;
|
||||
placeholder?: string;
|
||||
|
||||
@@ -12,7 +12,7 @@ import { escapeMarkdown, toggleCheckbox } from '../../utils/markdown';
|
||||
import N8nLoading from '../N8nLoading';
|
||||
|
||||
interface IImage {
|
||||
id: string;
|
||||
id: string | number;
|
||||
url: string;
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ interface Options {
|
||||
}
|
||||
|
||||
interface MarkdownProps {
|
||||
content?: string;
|
||||
content?: string | null;
|
||||
withMultiBreaks?: boolean;
|
||||
images?: IImage[];
|
||||
loading?: boolean;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts" setup>
|
||||
import { ElSubMenu, ElMenuItem } from 'element-plus';
|
||||
import { computed, useCssModule } from 'vue';
|
||||
import { computed, useCssModule, getCurrentInstance } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
|
||||
import { doesMenuItemMatchCurrentRoute } from './routerUtil';
|
||||
@@ -8,6 +8,7 @@ import type { IMenuItem } from '../../types';
|
||||
import { getInitials } from '../../utils/labelUtil';
|
||||
import ConditionalRouterLink from '../ConditionalRouterLink';
|
||||
import N8nIcon from '../N8nIcon';
|
||||
import N8nSpinner from '../N8nSpinner';
|
||||
import N8nTooltip from '../N8nTooltip';
|
||||
|
||||
interface MenuItemProps {
|
||||
@@ -61,6 +62,9 @@ const isItemActive = (item: IMenuItem): boolean => {
|
||||
Array.isArray(item.children) && item.children.some((child) => isActive(child));
|
||||
return isActive(item) || hasActiveChild;
|
||||
};
|
||||
|
||||
// Get self component to avoid dependency cycle
|
||||
const N8nMenuItem = getCurrentInstance()?.type;
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
<script lang="ts" setup>
|
||||
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
|
||||
import type { Placement } from 'element-plus';
|
||||
import { computed } from 'vue';
|
||||
import { computed, getCurrentInstance } from 'vue';
|
||||
|
||||
import N8nTooltip from '../N8nTooltip';
|
||||
|
||||
type IconType = 'file' | 'icon' | 'unknown';
|
||||
|
||||
interface NodeIconProps {
|
||||
type: 'file' | 'icon' | 'unknown';
|
||||
type: IconType;
|
||||
src?: string;
|
||||
name?: string;
|
||||
nodeTypeName?: string;
|
||||
@@ -16,7 +18,7 @@ interface NodeIconProps {
|
||||
color?: string;
|
||||
showTooltip?: boolean;
|
||||
tooltipPosition?: Placement;
|
||||
badge?: { src: string; type: string };
|
||||
badge?: { src: string; type: IconType };
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<NodeIconProps>(), {
|
||||
@@ -69,6 +71,9 @@ const badgeStyleData = computed((): Record<string, string> => {
|
||||
bottom: `-${Math.floor(size / 2)}px`,
|
||||
};
|
||||
});
|
||||
|
||||
// Get self component to avoid dependency cycle
|
||||
const N8nNodeIcon = getCurrentInstance()?.type;
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -97,7 +102,7 @@ const badgeStyleData = computed((): Record<string, string> => {
|
||||
<img v-if="type === 'file'" :src="src" :class="$style.nodeIconImage" />
|
||||
<FontAwesomeIcon v-else :icon="`${name}`" :style="fontStyleData" />
|
||||
<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 v-else :class="$style.nodeIconPlaceholder">
|
||||
|
||||
@@ -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';
|
||||
|
||||
interface RadioOption {
|
||||
@@ -47,8 +47,9 @@ const onClick = (
|
||||
>
|
||||
<RadioButton
|
||||
v-for="option in options"
|
||||
:key="option.value"
|
||||
:key="`${option.value}`"
|
||||
v-bind="option"
|
||||
:value="`${option.value}`"
|
||||
:active="modelValue === option.value"
|
||||
:size="size"
|
||||
:disabled="disabled || option.disabled"
|
||||
|
||||
@@ -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 { computed, onMounted, onBeforeMount, ref, nextTick, watch } from 'vue';
|
||||
|
||||
import type { ItemWithKey } from '@n8n/design-system/types';
|
||||
|
||||
interface RecycleScrollerProps {
|
||||
itemSize: number;
|
||||
items: Array<Record<string, string>>;
|
||||
itemKey: string;
|
||||
items: Item[];
|
||||
itemKey: Key;
|
||||
offset?: number;
|
||||
}
|
||||
|
||||
@@ -24,18 +26,21 @@ const windowHeight = ref(0);
|
||||
|
||||
/** Cache */
|
||||
|
||||
const itemSizeCache = ref<Record<string, number>>({});
|
||||
const itemSizeCache = ref<Record<Item[Key], number>>({} as Record<Item[Key], number>);
|
||||
const itemPositionCache = computed(() => {
|
||||
return props.items.reduce<Record<string, number>>((acc, item, index) => {
|
||||
const key = item[props.itemKey];
|
||||
const prevItem = props.items[index - 1];
|
||||
const prevItemPosition = prevItem ? acc[prevItem[props.itemKey]] : 0;
|
||||
const prevItemSize = prevItem ? itemSizeCache.value[prevItem[props.itemKey]] : 0;
|
||||
return props.items.reduce<Record<Item[Key], number>>(
|
||||
(acc, item, index) => {
|
||||
const key = item[props.itemKey];
|
||||
const prevItem = props.items[index - 1];
|
||||
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 */
|
||||
@@ -186,7 +191,7 @@ function onScroll() {
|
||||
<div
|
||||
v-for="item in visibleItems"
|
||||
:key="item[itemKey]"
|
||||
:ref="(element) => (itemRefs[item[itemKey]] = element)"
|
||||
:ref="(element) => (itemRefs[`${item[itemKey]}`] = element)"
|
||||
class="recycle-scroller-item"
|
||||
>
|
||||
<slot :item="item" :update-item-size="onUpdateItemSize" />
|
||||
|
||||
@@ -8,6 +8,9 @@ describe('N8nRoute', () => {
|
||||
props: {
|
||||
to: '/test',
|
||||
},
|
||||
global: {
|
||||
stubs: ['RouterLink'],
|
||||
},
|
||||
});
|
||||
expect(wrapper.html()).toMatchSnapshot();
|
||||
});
|
||||
@@ -18,6 +21,9 @@ describe('N8nRoute', () => {
|
||||
to: '/test',
|
||||
newWindow: true,
|
||||
},
|
||||
global: {
|
||||
stubs: ['RouterLink'],
|
||||
},
|
||||
});
|
||||
expect(wrapper.html()).toMatchSnapshot();
|
||||
});
|
||||
@@ -27,6 +33,9 @@ describe('N8nRoute', () => {
|
||||
props: {
|
||||
to: 'https://example.com/',
|
||||
},
|
||||
global: {
|
||||
stubs: ['RouterLink'],
|
||||
},
|
||||
});
|
||||
expect(wrapper.html()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
import { type RouteLocationRaw } from 'vue-router';
|
||||
import { RouterLink, type RouteLocationRaw } from 'vue-router';
|
||||
|
||||
interface RouteProps {
|
||||
to?: RouteLocationRaw | string;
|
||||
@@ -27,9 +27,9 @@ const openNewWindow = computed(() => !useRouterLink.value);
|
||||
</script>
|
||||
|
||||
<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>
|
||||
</router-link>
|
||||
</RouterLink>
|
||||
<a
|
||||
v-else
|
||||
:href="to ? `${to}` : undefined"
|
||||
|
||||
@@ -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 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>"`;
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { computed } from 'vue';
|
||||
|
||||
import { useI18n } from '../../composables/useI18n';
|
||||
import N8nIcon from '../N8nIcon';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<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';
|
||||
|
||||
const TYPE = ['dots', 'ring'] as const;
|
||||
|
||||
interface SpinnerProps {
|
||||
size?: Exclude<TextSize, 'mini' | 'xlarge'>;
|
||||
size?: IconSize;
|
||||
type?: (typeof TYPE)[number];
|
||||
}
|
||||
|
||||
|
||||
@@ -1,26 +1,19 @@
|
||||
<script lang="ts" setup generic="Value extends string | number">
|
||||
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';
|
||||
|
||||
interface TabOptions {
|
||||
value: Value;
|
||||
label?: string;
|
||||
icon?: string;
|
||||
href?: string;
|
||||
tooltip?: string;
|
||||
align?: 'left' | 'right';
|
||||
to?: RouteLocationRaw;
|
||||
}
|
||||
import N8nTooltip from '../N8nTooltip';
|
||||
|
||||
interface TabsProps {
|
||||
modelValue?: Value;
|
||||
options?: TabOptions[];
|
||||
options?: Array<TabOptions<Value>>;
|
||||
size?: 'small' | 'medium';
|
||||
}
|
||||
|
||||
withDefaults(defineProps<TabsProps>(), {
|
||||
modelValue: undefined,
|
||||
options: () => [],
|
||||
size: 'medium',
|
||||
});
|
||||
@@ -108,14 +101,14 @@ const scrollRight = () => scroll(50);
|
||||
</span>
|
||||
</div>
|
||||
</a>
|
||||
<router-link
|
||||
<RouterLink
|
||||
v-else-if="option.to"
|
||||
:to="option.to"
|
||||
:class="[$style.tab, { [$style.activeTab]: modelValue === option.value }]"
|
||||
>
|
||||
<N8nIcon v-if="option.icon" :icon="option.icon" size="medium" />
|
||||
<span v-if="option.label">{{ option.label }}</span>
|
||||
</router-link>
|
||||
</RouterLink>
|
||||
<div
|
||||
v-else
|
||||
:class="{ [$style.tab]: true, [$style.activeTab]: modelValue === option.value }"
|
||||
|
||||
@@ -27,7 +27,7 @@ const props = withDefaults(defineProps<TagsProp>(), {
|
||||
|
||||
const emit = defineEmits<{
|
||||
expand: [value: boolean];
|
||||
'click:tag': [tagId: string, e: MouseEvent];
|
||||
'click:tag': [tagId: string, e: PointerEvent];
|
||||
}>();
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts" setup generic="Value extends unknown = unknown">
|
||||
import { computed, useCssModule } from 'vue';
|
||||
import { computed, getCurrentInstance, useCssModule } from 'vue';
|
||||
|
||||
interface TreeProps {
|
||||
value?: Record<string, Value>;
|
||||
@@ -52,20 +52,23 @@ const getPath = (key: string): Array<string | number> => {
|
||||
}
|
||||
return [...props.path, key];
|
||||
};
|
||||
|
||||
// Get self component to avoid dependency cycle
|
||||
const N8nTree = getCurrentInstance()?.type;
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="isObject(value)" class="n8n-tree">
|
||||
<div v-for="(label, i) in Object.keys(value)" :key="i" :class="classes">
|
||||
<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>:</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>
|
||||
</div>
|
||||
<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>
|
||||
<N8nTree
|
||||
v-if="isObject(value[label])"
|
||||
@@ -74,11 +77,11 @@ const getPath = (key: string): Array<string | number> => {
|
||||
:value="value[label]"
|
||||
:node-class="nodeClass"
|
||||
>
|
||||
<template v-if="$slots.label" #label="data">
|
||||
<template v-if="!!$slots.label" #label="data">
|
||||
<slot name="label" v-bind="data" />
|
||||
</template>
|
||||
|
||||
<template v-if="$slots.value" #value="data">
|
||||
<template v-if="!!$slots.value" #value="data">
|
||||
<slot name="value" v-bind="data" />
|
||||
</template>
|
||||
</N8nTree>
|
||||
|
||||
@@ -7,9 +7,9 @@ import N8nBadge from '../N8nBadge';
|
||||
import N8nText from '../N8nText';
|
||||
|
||||
interface UsersInfoProps {
|
||||
firstName?: string;
|
||||
lastName?: string;
|
||||
email?: string;
|
||||
firstName?: string | null;
|
||||
lastName?: string | null;
|
||||
email?: string | null;
|
||||
isOwner?: boolean;
|
||||
isPendingUser?: boolean;
|
||||
isCurrentUser?: boolean;
|
||||
|
||||
@@ -74,7 +74,7 @@ const onBlur = () => emit('blur');
|
||||
const onFocus = () => emit('focus');
|
||||
|
||||
const getLabel = (user: IUser) =>
|
||||
!user.fullName ? user.email : `${user.fullName} (${user.email})`;
|
||||
(!user.fullName ? user.email : `${user.fullName} (${user.email})`) ?? '';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts" setup>
|
||||
import { ElDropdown, ElDropdownItem, ElDropdownMenu } from 'element-plus';
|
||||
import { computed } from 'vue';
|
||||
|
||||
import type { IUser, UserStackGroups } from '@n8n/design-system/types';
|
||||
@@ -9,7 +10,7 @@ import N8nUserInfo from '../N8nUserInfo';
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
users: UserStackGroups;
|
||||
currentUserEmail?: string;
|
||||
currentUserEmail?: string | null;
|
||||
maxAvatars?: number;
|
||||
dropdownTrigger?: 'hover' | 'click';
|
||||
}>(),
|
||||
@@ -63,7 +64,7 @@ const menuHeight = computed(() => {
|
||||
|
||||
<template>
|
||||
<div class="user-stack" data-test-id="user-stack-container">
|
||||
<el-dropdown
|
||||
<ElDropdown
|
||||
:trigger="$props.dropdownTrigger"
|
||||
:max-height="menuHeight"
|
||||
popper-class="user-stack-popper"
|
||||
@@ -81,14 +82,14 @@ const menuHeight = computed(() => {
|
||||
<div v-if="hiddenUsersCount > 0" :class="$style.hiddenBadge">+{{ hiddenUsersCount }}</div>
|
||||
</div>
|
||||
<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 :class="$style.groupContainer">
|
||||
<el-dropdown-item>
|
||||
<ElDropdownItem>
|
||||
<header v-if="groupCount > 1" :class="$style.groupName">{{ index }}</header>
|
||||
</el-dropdown-item>
|
||||
</ElDropdownItem>
|
||||
<div :class="$style.groupUsers">
|
||||
<el-dropdown-item
|
||||
<ElDropdownItem
|
||||
v-for="user in groupUsers"
|
||||
:key="user.id"
|
||||
:data-test-id="`user-stack-info-${user.id}`"
|
||||
@@ -98,13 +99,13 @@ const menuHeight = computed(() => {
|
||||
v-bind="user"
|
||||
:is-current-user="user.email === props.currentUserEmail"
|
||||
/>
|
||||
</el-dropdown-item>
|
||||
</ElDropdownItem>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-dropdown-menu>
|
||||
</ElDropdownMenu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</ElDropdown>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<script lang="ts" setup>
|
||||
<script lang="ts" setup generic="UserType extends IUser = IUser">
|
||||
import { computed } from 'vue';
|
||||
|
||||
import { useI18n } from '../../composables/useI18n';
|
||||
@@ -8,10 +8,10 @@ import N8nBadge from '../N8nBadge';
|
||||
import N8nUserInfo from '../N8nUserInfo';
|
||||
|
||||
interface UsersListProps {
|
||||
users: IUser[];
|
||||
users: UserType[];
|
||||
readonly?: boolean;
|
||||
currentUserId?: string;
|
||||
actions?: UserAction[];
|
||||
currentUserId?: string | null;
|
||||
actions?: Array<UserAction<UserType>>;
|
||||
isSamlLoginEnabled?: boolean;
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ const props = withDefaults(defineProps<UsersListProps>(), {
|
||||
const { t } = useI18n();
|
||||
|
||||
const sortedUsers = computed(() =>
|
||||
[...props.users].sort((a: IUser, b: IUser) => {
|
||||
[...props.users].sort((a: UserType, b: UserType) => {
|
||||
if (!a.email || !b.email) {
|
||||
throw new Error('Expected all users to have email');
|
||||
}
|
||||
@@ -64,7 +64,7 @@ const sortedUsers = computed(() =>
|
||||
);
|
||||
|
||||
const defaultGuard = () => true;
|
||||
const getActions = (user: IUser): UserAction[] => {
|
||||
const getActions = (user: UserType): Array<UserAction<UserType>> => {
|
||||
if (user.isOwner) return [];
|
||||
|
||||
return props.actions.filter((action) => (action.guard ?? defaultGuard)(user));
|
||||
@@ -73,7 +73,7 @@ const getActions = (user: IUser): UserAction[] => {
|
||||
const emit = defineEmits<{
|
||||
action: [value: { action: string; userId: string }];
|
||||
}>();
|
||||
const onUserAction = (user: IUser, action: string) =>
|
||||
const onUserAction = (user: UserType, action: string) =>
|
||||
emit('action', {
|
||||
action,
|
||||
userId: user.id,
|
||||
@@ -101,7 +101,7 @@ const onUserAction = (user: IUser, action: string) =>
|
||||
<N8nActionToggle
|
||||
v-if="
|
||||
!user.isOwner &&
|
||||
!['ldap'].includes(user.signInType) &&
|
||||
user.signInType !== 'ldap' &&
|
||||
!readonly &&
|
||||
getActions(user).length > 0 &&
|
||||
actions.length > 0
|
||||
|
||||
@@ -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';
|
||||
|
||||
export interface N8nPluginOptions {}
|
||||
|
||||
export const N8nPlugin: Plugin<N8nPluginOptions> = {
|
||||
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)) {
|
||||
app.directive(name, directive);
|
||||
}
|
||||
|
||||
10
packages/frontend/@n8n/design-system/src/types/badge.ts
Normal file
10
packages/frontend/@n8n/design-system/src/types/badge.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
const BADGE_THEME = [
|
||||
'default',
|
||||
'success',
|
||||
'warning',
|
||||
'danger',
|
||||
'primary',
|
||||
'secondary',
|
||||
'tertiary',
|
||||
] as const;
|
||||
export type BadgeTheme = (typeof BADGE_THEME)[number];
|
||||
@@ -7,7 +7,7 @@ export type ButtonElement = (typeof BUTTON_ELEMENT)[number];
|
||||
const BUTTON_TYPE = ['primary', 'secondary', 'tertiary', 'success', 'warning', 'danger'] as const;
|
||||
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];
|
||||
|
||||
const BUTTON_NATIVE_TYPE = ['submit', 'reset', 'button'] as const;
|
||||
@@ -21,7 +21,7 @@ export interface IconButtonProps {
|
||||
loading?: boolean;
|
||||
outline?: boolean;
|
||||
size?: ButtonSize;
|
||||
iconSize?: Exclude<IconSize, 'xlarge'>;
|
||||
iconSize?: IconSize;
|
||||
text?: boolean;
|
||||
type?: ButtonType;
|
||||
nativeType?: ButtonNativeType;
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
const CALLOUT_THEMES = ['info', 'success', 'secondary', 'warning', 'danger', 'custom'] as const;
|
||||
export type CalloutTheme = (typeof CALLOUT_THEMES)[number];
|
||||
@@ -4,7 +4,7 @@ export type DatatableRowDataType = string | number | boolean | null | undefined;
|
||||
|
||||
export interface DatatableRow {
|
||||
id: string | number;
|
||||
[key: string]: DatatableRowDataType | Record<string, DatatableRowDataType>;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface DatatableColumn {
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
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 RuleGroup = {
|
||||
@@ -65,6 +73,8 @@ export type IFormInput = {
|
||||
|
||||
export type IFormInputs = IFormInput[];
|
||||
|
||||
export type FormValues = FormInputsToFormValues<IFormInput[], FormFieldValue>;
|
||||
|
||||
export type IFormBoxConfig = {
|
||||
title: string;
|
||||
buttonText?: string;
|
||||
|
||||
@@ -1,12 +1,19 @@
|
||||
export * from './action-dropdown';
|
||||
export * from './assistant';
|
||||
export * from './badge';
|
||||
export * from './button';
|
||||
export * from './callout';
|
||||
export * from './datatable';
|
||||
export * from './form';
|
||||
export * from './i18n';
|
||||
export * from './icon';
|
||||
export * from './input';
|
||||
export * from './menu';
|
||||
export * from './select';
|
||||
export * from './user';
|
||||
export * from './keyboardshortcut';
|
||||
export * from './menu';
|
||||
export * from './node-creator-node';
|
||||
export * from './recycle-scroller';
|
||||
export * from './resize';
|
||||
export * from './select';
|
||||
export * from './tabs';
|
||||
export * from './text';
|
||||
export * from './user';
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
export type ItemWithKey<Key extends string> = {
|
||||
[K in Key]: string;
|
||||
} & {
|
||||
[key: string]: unknown;
|
||||
};
|
||||
11
packages/frontend/@n8n/design-system/src/types/tabs.ts
Normal file
11
packages/frontend/@n8n/design-system/src/types/tabs.ts
Normal 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;
|
||||
}
|
||||
@@ -1,23 +1,24 @@
|
||||
export interface IUser {
|
||||
export type IUser = {
|
||||
id: string;
|
||||
firstName?: string;
|
||||
lastName?: string;
|
||||
firstName?: string | null;
|
||||
lastName?: string | null;
|
||||
fullName?: string;
|
||||
email?: string;
|
||||
isOwner: boolean;
|
||||
isPendingUser: boolean;
|
||||
role?: string;
|
||||
email?: string | null;
|
||||
signInType?: string;
|
||||
isOwner?: boolean;
|
||||
isPendingUser?: boolean;
|
||||
inviteAcceptUrl?: string;
|
||||
disabled: boolean;
|
||||
signInType: string;
|
||||
}
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
export interface UserAction {
|
||||
export interface UserAction<UserType extends IUser> {
|
||||
label: string;
|
||||
value: string;
|
||||
disabled: boolean;
|
||||
disabled?: boolean;
|
||||
type?: 'external-link';
|
||||
tooltip?: string;
|
||||
guard?: (user: IUser) => boolean;
|
||||
guard?: (user: UserType) => boolean;
|
||||
}
|
||||
|
||||
export type UserStackGroups = { [groupName: string]: IUser[] };
|
||||
|
||||
@@ -86,6 +86,7 @@
|
||||
"vue": "catalog:frontend",
|
||||
"vue-agile": "^2.0.0",
|
||||
"vue-chartjs": "^5.2.0",
|
||||
"vue-component-type-helpers": "^2.2.10",
|
||||
"vue-github-button": "^3.1.3",
|
||||
"vue-i18n": "catalog:frontend",
|
||||
"vue-json-pretty": "2.2.4",
|
||||
|
||||
@@ -293,6 +293,56 @@ export type BaseResource = {
|
||||
name: string;
|
||||
};
|
||||
|
||||
export type FolderResource = BaseFolderItem & {
|
||||
resourceType: 'folder';
|
||||
};
|
||||
|
||||
export type WorkflowResource = BaseResource & {
|
||||
resourceType: 'workflow';
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
active: boolean;
|
||||
isArchived: boolean;
|
||||
homeProject?: ProjectSharingData;
|
||||
scopes?: Scope[];
|
||||
tags?: ITag[] | string[];
|
||||
sharedWithProjects?: ProjectSharingData[];
|
||||
readOnly: boolean;
|
||||
parentFolder?: ResourceParentFolder;
|
||||
};
|
||||
|
||||
export type VariableResource = BaseResource & {
|
||||
resourceType: 'variable';
|
||||
key?: string;
|
||||
value?: string;
|
||||
};
|
||||
|
||||
export type CredentialsResource = BaseResource & {
|
||||
resourceType: 'credential';
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
type: string;
|
||||
homeProject?: ProjectSharingData;
|
||||
scopes?: Scope[];
|
||||
sharedWithProjects?: ProjectSharingData[];
|
||||
readOnly: boolean;
|
||||
needsSetup: boolean;
|
||||
};
|
||||
|
||||
export type Resource = WorkflowResource | FolderResource | CredentialsResource | VariableResource;
|
||||
|
||||
export type BaseFilters = {
|
||||
search: string;
|
||||
homeProject: string;
|
||||
[key: string]: boolean | string | string[];
|
||||
};
|
||||
|
||||
export type SortingAndPaginationUpdates = {
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
sort?: string;
|
||||
};
|
||||
|
||||
export type WorkflowListItem = Omit<
|
||||
IWorkflowDb,
|
||||
'nodes' | 'connections' | 'settings' | 'pinData' | 'usedCredentials' | 'meta'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Plugin } from 'vue';
|
||||
import type { Component, Plugin } from 'vue';
|
||||
import { render } from '@testing-library/vue';
|
||||
import { i18nInstance } from '@n8n/i18n';
|
||||
import { GlobalComponentsPlugin } from '@/plugins/components';
|
||||
@@ -10,6 +10,7 @@ import type { Telemetry } from '@/plugins/telemetry';
|
||||
import vueJsonPretty from 'vue-json-pretty';
|
||||
import merge from 'lodash/merge';
|
||||
import type { TestingPinia } from '@pinia/testing';
|
||||
import * as components from '@n8n/design-system/components';
|
||||
|
||||
export type RenderComponent = Parameters<typeof render>[0];
|
||||
export type RenderOptions = Parameters<typeof render>[1] & {
|
||||
@@ -25,6 +26,14 @@ const TelemetryPlugin: Plugin<{}> = {
|
||||
},
|
||||
};
|
||||
|
||||
const TestingGlobalComponentsPlugin: Plugin<{}> = {
|
||||
install(app) {
|
||||
for (const [name, component] of Object.entries(components)) {
|
||||
app.component(name, component as unknown as Component);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
const defaultOptions = {
|
||||
global: {
|
||||
stubs: {
|
||||
@@ -38,6 +47,7 @@ const defaultOptions = {
|
||||
GlobalComponentsPlugin,
|
||||
GlobalDirectivesPlugin,
|
||||
TelemetryPlugin,
|
||||
TestingGlobalComponentsPlugin,
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -76,7 +76,7 @@ const getExpirationTime = (apiKey: ApiKey): string => {
|
||||
|
||||
<template #append>
|
||||
<div ref="cardActions" :class="$style.cardActions">
|
||||
<n8n-action-toggle :actions="ACTION_LIST" theme="dark" @action="onAction" />
|
||||
<N8nActionToggle :actions="ACTION_LIST" theme="dark" @action="onAction" />
|
||||
</div>
|
||||
</template>
|
||||
</n8n-card>
|
||||
|
||||
@@ -6,7 +6,7 @@ import Modal from '@/components/Modal.vue';
|
||||
import { useUsersStore } from '@/stores/users.store';
|
||||
import { createFormEventBus } from '@n8n/design-system/utils';
|
||||
import { createEventBus } from '@n8n/utils/event-bus';
|
||||
import type { IFormInputs, IFormInput } from '@/Interface';
|
||||
import type { IFormInputs, IFormInput, FormFieldValueUpdate, FormValues } from '@/Interface';
|
||||
import { useI18n } from '@n8n/i18n';
|
||||
|
||||
const config = ref<IFormInputs | null>(null);
|
||||
@@ -33,17 +33,14 @@ const passwordsMatch = (value: string | number | boolean | null | undefined) =>
|
||||
return false;
|
||||
};
|
||||
|
||||
const onInput = (e: { name: string; value: string }) => {
|
||||
if (e.name === 'password') {
|
||||
const onInput = (e: FormFieldValueUpdate) => {
|
||||
if (e.name === 'password' && typeof e.value === 'string') {
|
||||
password.value = e.value;
|
||||
}
|
||||
};
|
||||
|
||||
const onSubmit = async (values: {
|
||||
currentPassword: string;
|
||||
password: string;
|
||||
mfaCode?: string;
|
||||
}) => {
|
||||
const onSubmit = async (data: FormValues) => {
|
||||
const values = data as { currentPassword: string; password: string; mfaCode?: string };
|
||||
try {
|
||||
loading.value = true;
|
||||
await usersStore.updateCurrentUserPassword({
|
||||
@@ -143,6 +140,7 @@ onMounted(() => {
|
||||
>
|
||||
<template #content>
|
||||
<n8n-form-inputs
|
||||
v-if="config"
|
||||
:inputs="config"
|
||||
:event-bus="formBus"
|
||||
:column-view="true"
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
<script lang="ts" setup>
|
||||
import { useUIStore } from '@/stores/ui.store';
|
||||
import type { PublicInstalledPackage } from 'n8n-workflow';
|
||||
import type { IUser, PublicInstalledPackage } from 'n8n-workflow';
|
||||
import { NPM_PACKAGE_DOCS_BASE_URL, COMMUNITY_PACKAGE_MANAGE_ACTIONS } from '@/constants';
|
||||
import { useI18n } from '@n8n/i18n';
|
||||
import { useTelemetry } from '@/composables/useTelemetry';
|
||||
import { useSettingsStore } from '@/stores/settings.store';
|
||||
import type { UserAction } from '@n8n/design-system';
|
||||
|
||||
interface Props {
|
||||
communityPackage?: PublicInstalledPackage | null;
|
||||
@@ -22,7 +23,7 @@ const i18n = useI18n();
|
||||
const telemetry = useTelemetry();
|
||||
const settingsStore = useSettingsStore();
|
||||
|
||||
const packageActions = [
|
||||
const packageActions: Array<UserAction<IUser>> = [
|
||||
{
|
||||
label: i18n.baseText('settings.communityNodes.viewDocsAction.label'),
|
||||
value: COMMUNITY_PACKAGE_MANAGE_ACTIONS.VIEW_DOCS,
|
||||
|
||||
@@ -12,7 +12,7 @@ import { useProjectsStore } from '@/stores/projects.store';
|
||||
import ProjectCardBadge from '@/components/Projects/ProjectCardBadge.vue';
|
||||
import { useI18n } from '@n8n/i18n';
|
||||
import { ResourceType } from '@/utils/projects.utils';
|
||||
import type { CredentialsResource } from './layouts/ResourcesListLayout.vue';
|
||||
import type { CredentialsResource } from '@/Interface';
|
||||
|
||||
const CREDENTIAL_LIST_ITEM_ACTIONS = {
|
||||
OPEN: 'open',
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { BaseTextKey } from '@n8n/i18n';
|
||||
import type { TestTableColumn } from '@/components/Evaluations.ee/shared/TestTableBase.vue';
|
||||
import { useI18n } from '@n8n/i18n';
|
||||
import { useRouter } from 'vue-router';
|
||||
import type { BadgeTheme } from '@n8n/design-system';
|
||||
|
||||
defineProps<{
|
||||
column: TestTableColumn<T>;
|
||||
@@ -39,7 +40,7 @@ const errorTooltipMap: Record<string, BaseTextKey> = {
|
||||
};
|
||||
|
||||
// FIXME: move status logic to a parent component
|
||||
const statusThemeMap: Record<string, string> = {
|
||||
const statusThemeMap: Record<string, BadgeTheme> = {
|
||||
new: 'default',
|
||||
running: 'warning',
|
||||
evaluation_running: 'warning',
|
||||
|
||||
@@ -206,7 +206,7 @@ defineExpose({ focus, select });
|
||||
outline
|
||||
type="tertiary"
|
||||
icon="external-link-alt"
|
||||
size="xsmall"
|
||||
size="mini"
|
||||
:class="$style['expression-editor-modal-opener']"
|
||||
data-test-id="expander"
|
||||
@click="emit('modal-opener-click')"
|
||||
|
||||
@@ -9,6 +9,7 @@ import { mockedStore } from '@/__tests__/utils';
|
||||
import { useProjectsStore } from '@/stores/projects.store';
|
||||
import { ProjectTypes, type Project } from '@/types/projects.types';
|
||||
import { useFoldersStore } from '@/stores/folders.store';
|
||||
import type { IUser } from 'n8n-workflow';
|
||||
|
||||
vi.mock('vue-router', async (importOriginal) => ({
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
|
||||
@@ -41,7 +42,7 @@ const TEST_FOLDER_CHILD: FolderShortInfo = {
|
||||
parentFolder: TEST_FOLDER.id,
|
||||
};
|
||||
|
||||
const TEST_ACTIONS: UserAction[] = [
|
||||
const TEST_ACTIONS: Array<UserAction<IUser>> = [
|
||||
{ label: 'Action 1', value: 'action1', disabled: false },
|
||||
{ label: 'Action 2', value: 'action2', disabled: true },
|
||||
];
|
||||
|
||||
@@ -7,11 +7,12 @@ import { type PathItem } from '@n8n/design-system/components/N8nBreadcrumbs/Brea
|
||||
import { computed, onBeforeUnmount, ref, watch } from 'vue';
|
||||
import { useFoldersStore } from '@/stores/folders.store';
|
||||
import type { FolderPathItem, FolderShortInfo } from '@/Interface';
|
||||
import type { IUser } from 'n8n-workflow';
|
||||
|
||||
type Props = {
|
||||
// Current folder can be null when showing breadcrumbs for workflows in project root
|
||||
currentFolder?: FolderShortInfo | null;
|
||||
actions?: UserAction[];
|
||||
actions?: Array<UserAction<IUser>>;
|
||||
hiddenItemsTrigger?: 'hover' | 'click';
|
||||
currentFolderAsLink?: boolean;
|
||||
visibleLevels?: 1 | 2;
|
||||
|
||||
@@ -2,8 +2,8 @@ import { createComponentRenderer } from '@/__tests__/render';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import FolderCard from './FolderCard.vue';
|
||||
import { createPinia, setActivePinia } from 'pinia';
|
||||
import type { FolderResource } from '../layouts/ResourcesListLayout.vue';
|
||||
import type { FolderPathItem, UserAction } from '@/Interface';
|
||||
import type { FolderResource, FolderPathItem, UserAction } from '@/Interface';
|
||||
import type { IUser } from 'n8n-workflow';
|
||||
|
||||
vi.mock('vue-router', () => {
|
||||
const push = vi.fn();
|
||||
@@ -54,7 +54,7 @@ const renderComponent = createComponentRenderer(FolderCard, {
|
||||
actions: [
|
||||
{ label: 'Open', value: 'open', disabled: false },
|
||||
{ label: 'Delete', value: 'delete', disabled: false },
|
||||
] as const satisfies UserAction[],
|
||||
] as const satisfies Array<UserAction<IUser>>,
|
||||
breadcrumbs: DEFAULT_BREADCRUMBS,
|
||||
},
|
||||
global: {
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue';
|
||||
import { FOLDER_LIST_ITEM_ACTIONS } from './constants';
|
||||
import type { FolderResource } from '../layouts/ResourcesListLayout.vue';
|
||||
import { ProjectTypes, type Project } from '@/types/projects.types';
|
||||
import { useI18n } from '@n8n/i18n';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { VIEWS } from '@/constants';
|
||||
import type { UserAction } from '@/Interface';
|
||||
import type { FolderResource, UserAction } from '@/Interface';
|
||||
import { ResourceType } from '@/utils/projects.utils';
|
||||
import type { PathItem } from '@n8n/design-system/components/N8nBreadcrumbs/Breadcrumbs.vue';
|
||||
import { useFoldersStore } from '@/stores/folders.store';
|
||||
import { type IUser } from 'n8n-workflow';
|
||||
|
||||
type Props = {
|
||||
data: FolderResource;
|
||||
personalProject: Project | null;
|
||||
actions: UserAction[];
|
||||
actions: Array<UserAction<IUser>>;
|
||||
readOnly?: boolean;
|
||||
showOwnershipBadge?: boolean;
|
||||
};
|
||||
@@ -36,6 +36,7 @@ const emit = defineEmits<{
|
||||
}>();
|
||||
|
||||
const hiddenBreadcrumbsItemsAsync = ref<Promise<PathItem[]>>(new Promise(() => {}));
|
||||
|
||||
const cachedHiddenBreadcrumbsItems = ref<PathItem[]>([]);
|
||||
|
||||
const resourceTypeLabel = computed(() => i18n.baseText('generic.folder').toLowerCase());
|
||||
|
||||
@@ -118,7 +118,7 @@ const onClaimCreditsClicked = async () => {
|
||||
})
|
||||
}}</n8n-text
|
||||
>
|
||||
<n8n-text size="small" bold="true">
|
||||
<n8n-text size="small" :bold="true">
|
||||
{{ i18n.baseText('freeAi.credits.callout.success.title.part2') }}</n8n-text
|
||||
>
|
||||
</n8n-callout>
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
NodeConnectionTypes,
|
||||
traverseNodeParameters,
|
||||
} from 'n8n-workflow';
|
||||
import type { IFormInput } from '@n8n/design-system';
|
||||
import type { FormFieldValueUpdate, IFormInput } from '@n8n/design-system';
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useTelemetry } from '@/composables/useTelemetry';
|
||||
@@ -249,9 +249,11 @@ const onExecute = async () => {
|
||||
};
|
||||
|
||||
// Add handler for tool selection change
|
||||
const onUpdate = (change: { name: string; value: string }) => {
|
||||
const onUpdate = (change: FormFieldValueUpdate) => {
|
||||
if (change.name !== 'toolName') return;
|
||||
selectedTool.value = change.value;
|
||||
if (typeof change.value === 'string') {
|
||||
selectedTool.value = change.value;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
|
||||
@@ -2,17 +2,18 @@
|
||||
import { nextTick, ref } from 'vue';
|
||||
import { useToast } from '@/composables/useToast';
|
||||
import { onClickOutside } from '@vueuse/core';
|
||||
import type { InputType } from '@n8n/design-system';
|
||||
|
||||
interface Props {
|
||||
modelValue: string;
|
||||
subtitle?: string;
|
||||
type: string;
|
||||
type: InputType;
|
||||
readonly?: boolean;
|
||||
placeholder?: string;
|
||||
maxlength?: number;
|
||||
required?: boolean;
|
||||
autosize?: boolean | { minRows: number; maxRows: number };
|
||||
inputType?: string;
|
||||
inputType?: InputType;
|
||||
maxHeight?: string;
|
||||
}
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
|
||||
@@ -2,7 +2,13 @@
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { useToast } from '@/composables/useToast';
|
||||
import Modal from './Modal.vue';
|
||||
import type { IFormInputs, IInviteResponse, IUser, InvitableRoleName } from '@/Interface';
|
||||
import type {
|
||||
FormFieldValueUpdate,
|
||||
IFormInputs,
|
||||
IInviteResponse,
|
||||
IUser,
|
||||
InvitableRoleName,
|
||||
} from '@/Interface';
|
||||
import { EnterpriseEditionFeature, VALID_EMAIL_REGEX, INVITE_USER_MODAL_KEY } from '@/constants';
|
||||
import { ROLE } from '@n8n/api-types';
|
||||
import { useUsersStore } from '@/stores/users.store';
|
||||
@@ -127,11 +133,15 @@ const validateEmails = (value: string | number | boolean | null | undefined) =>
|
||||
return false;
|
||||
};
|
||||
|
||||
function onInput(e: { name: string; value: InvitableRoleName }) {
|
||||
if (e.name === 'emails') {
|
||||
function isInvitableRoleName(val: unknown): val is InvitableRoleName {
|
||||
return typeof val === 'string' && [ROLE.Member, ROLE.Admin].includes(val as InvitableRoleName);
|
||||
}
|
||||
|
||||
function onInput(e: FormFieldValueUpdate) {
|
||||
if (e.name === 'emails' && typeof e.value === 'string') {
|
||||
emails.value = e.value;
|
||||
}
|
||||
if (e.name === 'role') {
|
||||
if (e.name === 'role' && isInvitableRoleName(e.value)) {
|
||||
role.value = e.value;
|
||||
}
|
||||
}
|
||||
@@ -312,7 +322,7 @@ function getEmail(email: string): string {
|
||||
</n8n-users-list>
|
||||
</div>
|
||||
<n8n-form-inputs
|
||||
v-else
|
||||
v-else-if="config"
|
||||
:inputs="config"
|
||||
:event-bus="formBus"
|
||||
:column-view="true"
|
||||
|
||||
@@ -16,8 +16,8 @@ const emit = defineEmits<{
|
||||
'update:modelValue': [tab: MAIN_HEADER_TABS, event: MouseEvent];
|
||||
}>();
|
||||
|
||||
function onUpdateModelValue(tab: MAIN_HEADER_TABS, event: MouseEvent): void {
|
||||
emit('update:modelValue', tab, event);
|
||||
function onUpdateModelValue(tab: string, event: MouseEvent): void {
|
||||
emit('update:modelValue', tab as MAIN_HEADER_TABS, event);
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -428,7 +428,8 @@ async function handleFileImport(): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
async function onWorkflowMenuSelect(action: WORKFLOW_MENU_ACTIONS): Promise<void> {
|
||||
async function onWorkflowMenuSelect(value: string): Promise<void> {
|
||||
const action = value as WORKFLOW_MENU_ACTIONS;
|
||||
switch (action) {
|
||||
case WORKFLOW_MENU_ACTIONS.DUPLICATE: {
|
||||
uiStore.openModalWithData({
|
||||
|
||||
@@ -25,6 +25,7 @@ import { usePageRedirectionHelper } from '@/composables/usePageRedirectionHelper
|
||||
|
||||
import { useGlobalEntityCreation } from '@/composables/useGlobalEntityCreation';
|
||||
import { N8nNavigationDropdown, N8nTooltip, N8nLink, N8nIconButton } from '@n8n/design-system';
|
||||
import type { IMenuItem } from '@n8n/design-system';
|
||||
import { onClickOutside, type VueInstance } from '@vueuse/core';
|
||||
import Logo from './Logo/Logo.vue';
|
||||
|
||||
@@ -67,7 +68,7 @@ const userMenuItems = ref([
|
||||
},
|
||||
]);
|
||||
|
||||
const mainMenuItems = computed(() => [
|
||||
const mainMenuItems = computed<IMenuItem[]>(() => [
|
||||
{
|
||||
id: 'cloud-admin',
|
||||
position: 'bottom',
|
||||
|
||||
@@ -8,7 +8,7 @@ import { LOCAL_STORAGE_MAIN_PANEL_RELATIVE_WIDTH, MAIN_NODE_PANEL_WIDTH } from '
|
||||
import { useNDVStore } from '@/stores/ndv.store';
|
||||
import { ndvEventBus } from '@/event-bus';
|
||||
import NDVFloatingNodes from '@/components/NDVFloatingNodes.vue';
|
||||
import type { MainPanelType, XYPosition } from '@/Interface';
|
||||
import type { Direction, MainPanelType, XYPosition } from '@/Interface';
|
||||
import { ref, onMounted, onBeforeUnmount, computed, watch, nextTick } from 'vue';
|
||||
import { useUIStore } from '@/stores/ui.store';
|
||||
import { useThrottleFn } from '@vueuse/core';
|
||||
@@ -151,8 +151,8 @@ const outputPanelRelativeTranslate = computed((): number => {
|
||||
return currentRelativeLeftDelta > 0 ? currentRelativeLeftDelta : 0;
|
||||
});
|
||||
|
||||
const supportedResizeDirections = computed((): string[] => {
|
||||
const supportedDirections = ['right'];
|
||||
const supportedResizeDirections = computed((): Direction[] => {
|
||||
const supportedDirections = ['right' as Direction];
|
||||
|
||||
if (props.isDraggable) supportedDirections.push('left');
|
||||
return supportedDirections;
|
||||
|
||||
@@ -99,10 +99,11 @@ describe('NDVSubConnections', () => {
|
||||
<div class="connectionType"><span class="connectionLabel">Tools</span>
|
||||
<div>
|
||||
<div class="connectedNodesWrapper" style="--nodes-length: 0;">
|
||||
<div class="plusButton">
|
||||
<n8n-tooltip placement="top" teleported="true" offset="10" show-after="300" disabled="false">
|
||||
<n8n-icon-button size="medium" icon="plus" type="tertiary" data-test-id="add-subnode-ai_tool-0"></n8n-icon-button>
|
||||
</n8n-tooltip>
|
||||
<div class="plusButton"><button class="button button tertiary medium withIcon square el-tooltip__trigger el-tooltip__trigger" aria-live="polite" data-test-id="add-subnode-ai_tool-0"><span class="icon"><span class="n8n-text compact size-medium regular n8n-icon n8n-icon"><!----></span></span>
|
||||
<!--v-if-->
|
||||
</button>
|
||||
<!--teleport start-->
|
||||
<!--teleport end-->
|
||||
</div>
|
||||
<!--v-if-->
|
||||
</div>
|
||||
|
||||
@@ -26,10 +26,11 @@ import { useRunWorkflow } from '@/composables/useRunWorkflow';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useI18n } from '@n8n/i18n';
|
||||
import { useTelemetry } from '@/composables/useTelemetry';
|
||||
import { type IUpdateInformation } from '@/Interface';
|
||||
import type { ButtonSize, IUpdateInformation } from '@/Interface';
|
||||
import { generateCodeForAiTransform } from '@/components/ButtonParameter/utils';
|
||||
import { needsAgentInput } from '@/utils/nodes/nodeTransforms';
|
||||
import { useUIStore } from '@/stores/ui.store';
|
||||
import type { ButtonType } from '@n8n/design-system';
|
||||
|
||||
const NODE_TEST_STEP_POPUP_COUNT_KEY = 'N8N_NODE_TEST_STEP_POPUP_COUNT';
|
||||
const MAX_POPUP_COUNT = 10;
|
||||
@@ -41,8 +42,8 @@ const props = withDefaults(
|
||||
telemetrySource: string;
|
||||
disabled?: boolean;
|
||||
label?: string;
|
||||
type?: string;
|
||||
size?: string;
|
||||
type?: ButtonType;
|
||||
size?: ButtonSize;
|
||||
transparent?: boolean;
|
||||
hideIcon?: boolean;
|
||||
tooltip?: string;
|
||||
|
||||
@@ -119,7 +119,7 @@ const options = computed<ITab[]>(() => {
|
||||
return options;
|
||||
});
|
||||
|
||||
function onTabSelect(tab: string) {
|
||||
function onTabSelect(tab: string | number) {
|
||||
if (tab === 'docs' && props.nodeType) {
|
||||
void externalHooks.run('dataDisplay.onDocumentationUrlClick', {
|
||||
nodeType: props.nodeType,
|
||||
@@ -147,7 +147,7 @@ function onTabSelect(tab: string) {
|
||||
}
|
||||
}
|
||||
|
||||
function onTooltipClick(tab: string, event: MouseEvent) {
|
||||
function onTooltipClick(tab: string | number, event: MouseEvent) {
|
||||
if (tab === 'communityNode' && (event.target as Element).localName === 'a') {
|
||||
telemetry.track('user clicked cnr docs link', { source: 'node details view' });
|
||||
}
|
||||
|
||||
@@ -26,9 +26,9 @@ const emit = defineEmits<{
|
||||
v-if="!isReadOnly"
|
||||
type="tertiary"
|
||||
:class="['n8n-input', $style.overrideCloseButton]"
|
||||
outline="false"
|
||||
:outline="false"
|
||||
icon="xmark"
|
||||
size="xsmall"
|
||||
size="mini"
|
||||
@click="emit('close')"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -114,438 +114,441 @@ const isSaving = ref(false);
|
||||
const userPermissions = computed(() =>
|
||||
getResourcePermissions(usersStore.currentUser?.globalScopes),
|
||||
);
|
||||
const survey = computed<IFormInputs>(() => [
|
||||
{
|
||||
name: COMPANY_TYPE_KEY,
|
||||
properties: {
|
||||
label: i18n.baseText('personalizationModal.whatBestDescribesYourCompany'),
|
||||
type: 'select',
|
||||
placeholder: i18n.baseText('personalizationModal.select'),
|
||||
options: [
|
||||
{
|
||||
label: i18n.baseText('personalizationModal.saas'),
|
||||
value: SAAS_COMPANY_TYPE,
|
||||
},
|
||||
{
|
||||
label: i18n.baseText('personalizationModal.eCommerce'),
|
||||
value: ECOMMERCE_COMPANY_TYPE,
|
||||
},
|
||||
const survey = computed<IFormInputs>(
|
||||
() =>
|
||||
[
|
||||
{
|
||||
name: COMPANY_TYPE_KEY,
|
||||
properties: {
|
||||
label: i18n.baseText('personalizationModal.whatBestDescribesYourCompany'),
|
||||
type: 'select',
|
||||
placeholder: i18n.baseText('personalizationModal.select'),
|
||||
options: [
|
||||
{
|
||||
label: i18n.baseText('personalizationModal.saas'),
|
||||
value: SAAS_COMPANY_TYPE,
|
||||
},
|
||||
{
|
||||
label: i18n.baseText('personalizationModal.eCommerce'),
|
||||
value: ECOMMERCE_COMPANY_TYPE,
|
||||
},
|
||||
|
||||
{
|
||||
label: i18n.baseText('personalizationModal.digitalAgencyOrConsultant'),
|
||||
value: DIGITAL_AGENCY_COMPANY_TYPE,
|
||||
{
|
||||
label: i18n.baseText('personalizationModal.digitalAgencyOrConsultant'),
|
||||
value: DIGITAL_AGENCY_COMPANY_TYPE,
|
||||
},
|
||||
{
|
||||
label: i18n.baseText('personalizationModal.systemsIntegrator'),
|
||||
value: SYSTEMS_INTEGRATOR_COMPANY_TYPE,
|
||||
},
|
||||
{
|
||||
value: EDUCATION_TYPE,
|
||||
label: i18n.baseText('personalizationModal.education'),
|
||||
},
|
||||
{
|
||||
label: i18n.baseText('personalizationModal.other'),
|
||||
value: OTHER_COMPANY_TYPE,
|
||||
},
|
||||
{
|
||||
label: i18n.baseText('personalizationModal.imNotUsingN8nForWork'),
|
||||
value: PERSONAL_COMPANY_TYPE,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: i18n.baseText('personalizationModal.systemsIntegrator'),
|
||||
value: SYSTEMS_INTEGRATOR_COMPANY_TYPE,
|
||||
},
|
||||
{
|
||||
name: COMPANY_INDUSTRY_EXTENDED_KEY,
|
||||
properties: {
|
||||
type: 'multi-select',
|
||||
label: i18n.baseText('personalizationModal.whichIndustriesIsYourCompanyIn'),
|
||||
placeholder: i18n.baseText('personalizationModal.select'),
|
||||
options: [
|
||||
{
|
||||
value: FINANCE_INSURANCE_INDUSTRY,
|
||||
label: i18n.baseText('personalizationModal.financeOrInsurance'),
|
||||
},
|
||||
{
|
||||
value: GOVERNMENT_INDUSTRY,
|
||||
label: i18n.baseText('personalizationModal.government'),
|
||||
},
|
||||
{
|
||||
value: HEALTHCARE_INDUSTRY,
|
||||
label: i18n.baseText('personalizationModal.healthcare'),
|
||||
},
|
||||
{
|
||||
value: IT_INDUSTRY,
|
||||
label: i18n.baseText('personalizationModal.it'),
|
||||
},
|
||||
{
|
||||
value: LEGAL_INDUSTRY,
|
||||
label: i18n.baseText('personalizationModal.legal'),
|
||||
},
|
||||
{
|
||||
value: MSP_INDUSTRY,
|
||||
label: i18n.baseText('personalizationModal.managedServiceProvider'),
|
||||
},
|
||||
{
|
||||
value: MARKETING_INDUSTRY,
|
||||
label: i18n.baseText('personalizationModal.marketing'),
|
||||
},
|
||||
{
|
||||
value: MEDIA_INDUSTRY,
|
||||
label: i18n.baseText('personalizationModal.media'),
|
||||
},
|
||||
{
|
||||
value: MANUFACTURING_INDUSTRY,
|
||||
label: i18n.baseText('personalizationModal.manufacturing'),
|
||||
},
|
||||
{
|
||||
value: PHYSICAL_RETAIL_OR_SERVICES,
|
||||
label: i18n.baseText('personalizationModal.physicalRetailOrServices'),
|
||||
},
|
||||
{
|
||||
value: REAL_ESTATE_OR_CONSTRUCTION,
|
||||
label: i18n.baseText('personalizationModal.realEstateOrConstruction'),
|
||||
},
|
||||
{
|
||||
value: SECURITY_INDUSTRY,
|
||||
label: i18n.baseText('personalizationModal.security'),
|
||||
},
|
||||
{
|
||||
value: TELECOMS_INDUSTRY,
|
||||
label: i18n.baseText('personalizationModal.telecoms'),
|
||||
},
|
||||
{
|
||||
value: OTHER_INDUSTRY_OPTION,
|
||||
label: i18n.baseText('personalizationModal.otherPleaseSpecify'),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
value: EDUCATION_TYPE,
|
||||
label: i18n.baseText('personalizationModal.education'),
|
||||
shouldDisplay(values): boolean {
|
||||
const companyType = (values as IPersonalizationLatestVersion)[COMPANY_TYPE_KEY];
|
||||
return companyType === OTHER_COMPANY_TYPE;
|
||||
},
|
||||
{
|
||||
label: i18n.baseText('personalizationModal.other'),
|
||||
value: OTHER_COMPANY_TYPE,
|
||||
},
|
||||
{
|
||||
name: OTHER_COMPANY_INDUSTRY_EXTENDED_KEY,
|
||||
properties: {
|
||||
placeholder: i18n.baseText('personalizationModal.specifyYourCompanysIndustry'),
|
||||
},
|
||||
{
|
||||
label: i18n.baseText('personalizationModal.imNotUsingN8nForWork'),
|
||||
value: PERSONAL_COMPANY_TYPE,
|
||||
shouldDisplay(values): boolean {
|
||||
const companyType = (values as IPersonalizationLatestVersion)[COMPANY_TYPE_KEY];
|
||||
const companyIndustry = (values as IPersonalizationLatestVersion)[
|
||||
COMPANY_INDUSTRY_EXTENDED_KEY
|
||||
];
|
||||
return (
|
||||
companyType === OTHER_COMPANY_TYPE &&
|
||||
!!companyIndustry &&
|
||||
companyIndustry.includes(OTHER_INDUSTRY_OPTION)
|
||||
);
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: COMPANY_INDUSTRY_EXTENDED_KEY,
|
||||
properties: {
|
||||
type: 'multi-select',
|
||||
label: i18n.baseText('personalizationModal.whichIndustriesIsYourCompanyIn'),
|
||||
placeholder: i18n.baseText('personalizationModal.select'),
|
||||
options: [
|
||||
{
|
||||
value: FINANCE_INSURANCE_INDUSTRY,
|
||||
label: i18n.baseText('personalizationModal.financeOrInsurance'),
|
||||
},
|
||||
{
|
||||
name: ROLE_KEY,
|
||||
properties: {
|
||||
type: 'select',
|
||||
label: i18n.baseText('personalizationModal.whichRoleBestDescribesYou'),
|
||||
placeholder: i18n.baseText('personalizationModal.select'),
|
||||
options: [
|
||||
{
|
||||
value: ROLE_BUSINESS_OWNER,
|
||||
label: i18n.baseText('personalizationModal.businessOwner'),
|
||||
},
|
||||
{
|
||||
value: ROLE_CUSTOMER_SUPPORT,
|
||||
label: i18n.baseText('personalizationModal.customerSupport'),
|
||||
},
|
||||
{
|
||||
value: ROLE_DATA_SCIENCE,
|
||||
label: i18n.baseText('personalizationModal.dataScience'),
|
||||
},
|
||||
{
|
||||
value: ROLE_DEVOPS,
|
||||
label: i18n.baseText('personalizationModal.devops'),
|
||||
},
|
||||
{
|
||||
value: ROLE_IT,
|
||||
label: i18n.baseText('personalizationModal.it'),
|
||||
},
|
||||
{
|
||||
value: ROLE_ENGINEERING,
|
||||
label: i18n.baseText('personalizationModal.engineering'),
|
||||
},
|
||||
{
|
||||
value: ROLE_SALES_AND_MARKETING,
|
||||
label: i18n.baseText('personalizationModal.salesAndMarketing'),
|
||||
},
|
||||
{
|
||||
value: ROLE_SECURITY,
|
||||
label: i18n.baseText('personalizationModal.security'),
|
||||
},
|
||||
{
|
||||
value: ROLE_OTHER,
|
||||
label: i18n.baseText('personalizationModal.otherPleaseSpecify'),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
value: GOVERNMENT_INDUSTRY,
|
||||
label: i18n.baseText('personalizationModal.government'),
|
||||
shouldDisplay(values): boolean {
|
||||
const companyType = (values as IPersonalizationLatestVersion)[COMPANY_TYPE_KEY];
|
||||
return companyType !== PERSONAL_COMPANY_TYPE;
|
||||
},
|
||||
{
|
||||
value: HEALTHCARE_INDUSTRY,
|
||||
label: i18n.baseText('personalizationModal.healthcare'),
|
||||
},
|
||||
{
|
||||
name: ROLE_OTHER_KEY,
|
||||
properties: {
|
||||
placeholder: i18n.baseText('personalizationModal.specifyYourRole'),
|
||||
},
|
||||
{
|
||||
value: IT_INDUSTRY,
|
||||
label: i18n.baseText('personalizationModal.it'),
|
||||
shouldDisplay(values): boolean {
|
||||
const companyType = (values as IPersonalizationLatestVersion)[COMPANY_TYPE_KEY];
|
||||
const role = (values as IPersonalizationLatestVersion)[ROLE_KEY];
|
||||
return companyType !== PERSONAL_COMPANY_TYPE && role === ROLE_OTHER;
|
||||
},
|
||||
{
|
||||
value: LEGAL_INDUSTRY,
|
||||
label: i18n.baseText('personalizationModal.legal'),
|
||||
},
|
||||
{
|
||||
name: DEVOPS_AUTOMATION_GOAL_KEY,
|
||||
properties: {
|
||||
type: 'multi-select',
|
||||
label: i18n.baseText('personalizationModal.whatAreYouLookingToAutomate'),
|
||||
placeholder: i18n.baseText('personalizationModal.select'),
|
||||
options: [
|
||||
{
|
||||
value: DEVOPS_AUTOMATION_CI_CD_GOAL,
|
||||
label: i18n.baseText('personalizationModal.cicd'),
|
||||
},
|
||||
{
|
||||
value: DEVOPS_AUTOMATION_CLOUD_INFRASTRUCTURE_ORCHESTRATION_GOAL,
|
||||
label: i18n.baseText('personalizationModal.cloudInfrastructureOrchestration'),
|
||||
},
|
||||
{
|
||||
value: DEVOPS_AUTOMATION_DATA_SYNCING_GOAL,
|
||||
label: i18n.baseText('personalizationModal.dataSynching'),
|
||||
},
|
||||
{
|
||||
value: DEVOPS_INCIDENT_RESPONSE_GOAL,
|
||||
label: i18n.baseText('personalizationModal.incidentResponse'),
|
||||
},
|
||||
{
|
||||
value: DEVOPS_MONITORING_AND_ALERTING_GOAL,
|
||||
label: i18n.baseText('personalizationModal.monitoringAndAlerting'),
|
||||
},
|
||||
{
|
||||
value: DEVOPS_REPORTING_GOAL,
|
||||
label: i18n.baseText('personalizationModal.reporting'),
|
||||
},
|
||||
{
|
||||
value: DEVOPS_TICKETING_SYSTEMS_INTEGRATIONS_GOAL,
|
||||
label: i18n.baseText('personalizationModal.ticketingSystemsIntegrations'),
|
||||
},
|
||||
{
|
||||
value: OTHER_AUTOMATION_GOAL,
|
||||
label: i18n.baseText('personalizationModal.other'),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
value: MSP_INDUSTRY,
|
||||
label: i18n.baseText('personalizationModal.managedServiceProvider'),
|
||||
shouldDisplay(values): boolean {
|
||||
const companyType = (values as IPersonalizationLatestVersion)[COMPANY_TYPE_KEY];
|
||||
const role = (values as IPersonalizationLatestVersion)[ROLE_KEY] as string;
|
||||
return (
|
||||
companyType !== PERSONAL_COMPANY_TYPE &&
|
||||
[ROLE_DEVOPS, ROLE_ENGINEERING, ROLE_IT].includes(role)
|
||||
);
|
||||
},
|
||||
{
|
||||
value: MARKETING_INDUSTRY,
|
||||
label: i18n.baseText('personalizationModal.marketing'),
|
||||
},
|
||||
{
|
||||
name: DEVOPS_AUTOMATION_GOAL_OTHER_KEY,
|
||||
properties: {
|
||||
placeholder: i18n.baseText('personalizationModal.specifyYourAutomationGoal'),
|
||||
},
|
||||
{
|
||||
value: MEDIA_INDUSTRY,
|
||||
label: i18n.baseText('personalizationModal.media'),
|
||||
shouldDisplay(values): boolean {
|
||||
const companyType = (values as IPersonalizationLatestVersion)[COMPANY_TYPE_KEY];
|
||||
const goals = (values as IPersonalizationLatestVersion)[DEVOPS_AUTOMATION_GOAL_KEY];
|
||||
const role = (values as IPersonalizationLatestVersion)[ROLE_KEY] as string;
|
||||
return (
|
||||
companyType !== PERSONAL_COMPANY_TYPE &&
|
||||
[ROLE_DEVOPS, ROLE_ENGINEERING, ROLE_IT].includes(role) &&
|
||||
!!goals &&
|
||||
goals.includes(DEVOPS_AUTOMATION_OTHER)
|
||||
);
|
||||
},
|
||||
{
|
||||
value: MANUFACTURING_INDUSTRY,
|
||||
label: i18n.baseText('personalizationModal.manufacturing'),
|
||||
},
|
||||
{
|
||||
name: MARKETING_AUTOMATION_GOAL_KEY,
|
||||
properties: {
|
||||
type: 'multi-select',
|
||||
label: i18n.baseText('personalizationModal.specifySalesMarketingGoal'),
|
||||
placeholder: i18n.baseText('personalizationModal.select'),
|
||||
options: [
|
||||
{
|
||||
label: i18n.baseText('personalizationModal.leadGeneration'),
|
||||
value: MARKETING_AUTOMATION_LEAD_GENERATION_GOAL,
|
||||
},
|
||||
{
|
||||
label: i18n.baseText('personalizationModal.customerCommunication'),
|
||||
value: MARKETING_AUTOMATION_CUSTOMER_COMMUNICATION,
|
||||
},
|
||||
{
|
||||
label: i18n.baseText('personalizationModal.customerActions'),
|
||||
value: MARKETING_AUTOMATION_ACTIONS,
|
||||
},
|
||||
{
|
||||
label: i18n.baseText('personalizationModal.adCampaign'),
|
||||
value: MARKETING_AUTOMATION_AD_CAMPAIGN,
|
||||
},
|
||||
{
|
||||
label: i18n.baseText('personalizationModal.reporting'),
|
||||
value: MARKETING_AUTOMATION_REPORTING,
|
||||
},
|
||||
{
|
||||
label: i18n.baseText('personalizationModal.dataSynching'),
|
||||
value: MARKETING_AUTOMATION_DATA_SYNCHING,
|
||||
},
|
||||
{
|
||||
label: i18n.baseText('personalizationModal.other'),
|
||||
value: MARKETING_AUTOMATION_OTHER,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
value: PHYSICAL_RETAIL_OR_SERVICES,
|
||||
label: i18n.baseText('personalizationModal.physicalRetailOrServices'),
|
||||
shouldDisplay(values): boolean {
|
||||
const companyType = (values as IPersonalizationLatestVersion)[COMPANY_TYPE_KEY];
|
||||
const role = (values as IPersonalizationLatestVersion)[ROLE_KEY];
|
||||
return companyType !== PERSONAL_COMPANY_TYPE && role === ROLE_SALES_AND_MARKETING;
|
||||
},
|
||||
{
|
||||
value: REAL_ESTATE_OR_CONSTRUCTION,
|
||||
label: i18n.baseText('personalizationModal.realEstateOrConstruction'),
|
||||
},
|
||||
{
|
||||
name: OTHER_MARKETING_AUTOMATION_GOAL_KEY,
|
||||
properties: {
|
||||
placeholder: i18n.baseText('personalizationModal.specifyOtherSalesAndMarketingGoal'),
|
||||
},
|
||||
{
|
||||
value: SECURITY_INDUSTRY,
|
||||
label: i18n.baseText('personalizationModal.security'),
|
||||
shouldDisplay(values): boolean {
|
||||
const companyType = (values as IPersonalizationLatestVersion)[COMPANY_TYPE_KEY];
|
||||
const goals = (values as IPersonalizationLatestVersion)[MARKETING_AUTOMATION_GOAL_KEY];
|
||||
const role = (values as IPersonalizationLatestVersion)[ROLE_KEY];
|
||||
return (
|
||||
companyType !== PERSONAL_COMPANY_TYPE &&
|
||||
role === ROLE_SALES_AND_MARKETING &&
|
||||
!!goals &&
|
||||
goals.includes(MARKETING_AUTOMATION_OTHER)
|
||||
);
|
||||
},
|
||||
{
|
||||
value: TELECOMS_INDUSTRY,
|
||||
label: i18n.baseText('personalizationModal.telecoms'),
|
||||
},
|
||||
{
|
||||
name: AUTOMATION_BENEFICIARY_KEY,
|
||||
properties: {
|
||||
type: 'select',
|
||||
label: i18n.baseText('personalizationModal.specifyAutomationBeneficiary'),
|
||||
placeholder: i18n.baseText('personalizationModal.select'),
|
||||
options: [
|
||||
{
|
||||
label: i18n.baseText('personalizationModal.myself'),
|
||||
value: AUTOMATION_BENEFICIARY_SELF,
|
||||
},
|
||||
{
|
||||
label: i18n.baseText('personalizationModal.myTeam'),
|
||||
value: AUTOMATION_BENEFICIARY_MY_TEAM,
|
||||
},
|
||||
{
|
||||
label: i18n.baseText('personalizationModal.otherTeams'),
|
||||
value: AUTOMATION_BENEFICIARY_OTHER_TEAMS,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
value: OTHER_INDUSTRY_OPTION,
|
||||
label: i18n.baseText('personalizationModal.otherPleaseSpecify'),
|
||||
shouldDisplay(values): boolean {
|
||||
const companyType = (values as IPersonalizationLatestVersion)[COMPANY_TYPE_KEY];
|
||||
return companyType !== PERSONAL_COMPANY_TYPE;
|
||||
},
|
||||
],
|
||||
},
|
||||
shouldDisplay(values): boolean {
|
||||
const companyType = (values as IPersonalizationLatestVersion)[COMPANY_TYPE_KEY];
|
||||
return companyType === OTHER_COMPANY_TYPE;
|
||||
},
|
||||
},
|
||||
{
|
||||
name: OTHER_COMPANY_INDUSTRY_EXTENDED_KEY,
|
||||
properties: {
|
||||
placeholder: i18n.baseText('personalizationModal.specifyYourCompanysIndustry'),
|
||||
},
|
||||
shouldDisplay(values): boolean {
|
||||
const companyType = (values as IPersonalizationLatestVersion)[COMPANY_TYPE_KEY];
|
||||
const companyIndustry = (values as IPersonalizationLatestVersion)[
|
||||
COMPANY_INDUSTRY_EXTENDED_KEY
|
||||
];
|
||||
return (
|
||||
companyType === OTHER_COMPANY_TYPE &&
|
||||
!!companyIndustry &&
|
||||
companyIndustry.includes(OTHER_INDUSTRY_OPTION)
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: ROLE_KEY,
|
||||
properties: {
|
||||
type: 'select',
|
||||
label: i18n.baseText('personalizationModal.whichRoleBestDescribesYou'),
|
||||
placeholder: i18n.baseText('personalizationModal.select'),
|
||||
options: [
|
||||
{
|
||||
value: ROLE_BUSINESS_OWNER,
|
||||
label: i18n.baseText('personalizationModal.businessOwner'),
|
||||
},
|
||||
{
|
||||
name: COMPANY_SIZE_KEY,
|
||||
properties: {
|
||||
type: 'select',
|
||||
label: i18n.baseText('personalizationModal.howBigIsYourCompany'),
|
||||
placeholder: i18n.baseText('personalizationModal.select'),
|
||||
options: [
|
||||
{
|
||||
label: i18n.baseText('personalizationModal.lessThan20People'),
|
||||
value: COMPANY_SIZE_20_OR_LESS,
|
||||
},
|
||||
{
|
||||
label: `20-99 ${i18n.baseText('personalizationModal.people')}`,
|
||||
value: COMPANY_SIZE_20_99,
|
||||
},
|
||||
{
|
||||
label: `100-499 ${i18n.baseText('personalizationModal.people')}`,
|
||||
value: COMPANY_SIZE_100_499,
|
||||
},
|
||||
{
|
||||
label: `500-999 ${i18n.baseText('personalizationModal.people')}`,
|
||||
value: COMPANY_SIZE_500_999,
|
||||
},
|
||||
{
|
||||
label: `1000+ ${i18n.baseText('personalizationModal.people')}`,
|
||||
value: COMPANY_SIZE_1000_OR_MORE,
|
||||
},
|
||||
{
|
||||
label: i18n.baseText('personalizationModal.imNotUsingN8nForWork'),
|
||||
value: COMPANY_SIZE_PERSONAL_USE,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
value: ROLE_CUSTOMER_SUPPORT,
|
||||
label: i18n.baseText('personalizationModal.customerSupport'),
|
||||
shouldDisplay(values): boolean {
|
||||
const companyType = (values as IPersonalizationLatestVersion)[COMPANY_TYPE_KEY];
|
||||
return companyType !== PERSONAL_COMPANY_TYPE;
|
||||
},
|
||||
{
|
||||
value: ROLE_DATA_SCIENCE,
|
||||
label: i18n.baseText('personalizationModal.dataScience'),
|
||||
},
|
||||
{
|
||||
name: REPORTED_SOURCE_KEY,
|
||||
properties: {
|
||||
type: 'select',
|
||||
label: i18n.baseText('personalizationModal.howDidYouHearAboutN8n'),
|
||||
placeholder: i18n.baseText('personalizationModal.select'),
|
||||
options: [
|
||||
{
|
||||
label: 'Google',
|
||||
value: REPORTED_SOURCE_GOOGLE,
|
||||
},
|
||||
{
|
||||
label: 'Twitter',
|
||||
value: REPORTED_SOURCE_TWITTER,
|
||||
},
|
||||
{
|
||||
label: 'LinkedIn',
|
||||
value: REPORTED_SOURCE_LINKEDIN,
|
||||
},
|
||||
{
|
||||
label: 'YouTube',
|
||||
value: REPORTED_SOURCE_YOUTUBE,
|
||||
},
|
||||
{
|
||||
label: i18n.baseText('personalizationModal.friendWordOfMouth'),
|
||||
value: REPORTED_SOURCE_FRIEND,
|
||||
},
|
||||
{
|
||||
label: i18n.baseText('personalizationModal.podcast'),
|
||||
value: REPORTED_SOURCE_PODCAST,
|
||||
},
|
||||
{
|
||||
label: i18n.baseText('personalizationModal.event'),
|
||||
value: REPORTED_SOURCE_EVENT,
|
||||
},
|
||||
{
|
||||
label: i18n.baseText('personalizationModal.otherPleaseSpecify'),
|
||||
value: REPORTED_SOURCE_OTHER,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
value: ROLE_DEVOPS,
|
||||
label: i18n.baseText('personalizationModal.devops'),
|
||||
},
|
||||
{
|
||||
name: REPORTED_SOURCE_OTHER_KEY,
|
||||
properties: {
|
||||
placeholder: i18n.baseText('personalizationModal.specifyReportedSource'),
|
||||
},
|
||||
{
|
||||
value: ROLE_IT,
|
||||
label: i18n.baseText('personalizationModal.it'),
|
||||
shouldDisplay(values): boolean {
|
||||
const reportedSource = (values as IPersonalizationLatestVersion)[REPORTED_SOURCE_KEY];
|
||||
return reportedSource === REPORTED_SOURCE_OTHER;
|
||||
},
|
||||
{
|
||||
value: ROLE_ENGINEERING,
|
||||
label: i18n.baseText('personalizationModal.engineering'),
|
||||
},
|
||||
{
|
||||
value: ROLE_SALES_AND_MARKETING,
|
||||
label: i18n.baseText('personalizationModal.salesAndMarketing'),
|
||||
},
|
||||
{
|
||||
value: ROLE_SECURITY,
|
||||
label: i18n.baseText('personalizationModal.security'),
|
||||
},
|
||||
{
|
||||
value: ROLE_OTHER,
|
||||
label: i18n.baseText('personalizationModal.otherPleaseSpecify'),
|
||||
},
|
||||
],
|
||||
},
|
||||
shouldDisplay(values): boolean {
|
||||
const companyType = (values as IPersonalizationLatestVersion)[COMPANY_TYPE_KEY];
|
||||
return companyType !== PERSONAL_COMPANY_TYPE;
|
||||
},
|
||||
},
|
||||
{
|
||||
name: ROLE_OTHER_KEY,
|
||||
properties: {
|
||||
placeholder: i18n.baseText('personalizationModal.specifyYourRole'),
|
||||
},
|
||||
shouldDisplay(values): boolean {
|
||||
const companyType = (values as IPersonalizationLatestVersion)[COMPANY_TYPE_KEY];
|
||||
const role = (values as IPersonalizationLatestVersion)[ROLE_KEY];
|
||||
return companyType !== PERSONAL_COMPANY_TYPE && role === ROLE_OTHER;
|
||||
},
|
||||
},
|
||||
{
|
||||
name: DEVOPS_AUTOMATION_GOAL_KEY,
|
||||
properties: {
|
||||
type: 'multi-select',
|
||||
label: i18n.baseText('personalizationModal.whatAreYouLookingToAutomate'),
|
||||
placeholder: i18n.baseText('personalizationModal.select'),
|
||||
options: [
|
||||
{
|
||||
value: DEVOPS_AUTOMATION_CI_CD_GOAL,
|
||||
label: i18n.baseText('personalizationModal.cicd'),
|
||||
},
|
||||
{
|
||||
value: DEVOPS_AUTOMATION_CLOUD_INFRASTRUCTURE_ORCHESTRATION_GOAL,
|
||||
label: i18n.baseText('personalizationModal.cloudInfrastructureOrchestration'),
|
||||
},
|
||||
{
|
||||
value: DEVOPS_AUTOMATION_DATA_SYNCING_GOAL,
|
||||
label: i18n.baseText('personalizationModal.dataSynching'),
|
||||
},
|
||||
{
|
||||
value: DEVOPS_INCIDENT_RESPONSE_GOAL,
|
||||
label: i18n.baseText('personalizationModal.incidentResponse'),
|
||||
},
|
||||
{
|
||||
value: DEVOPS_MONITORING_AND_ALERTING_GOAL,
|
||||
label: i18n.baseText('personalizationModal.monitoringAndAlerting'),
|
||||
},
|
||||
{
|
||||
value: DEVOPS_REPORTING_GOAL,
|
||||
label: i18n.baseText('personalizationModal.reporting'),
|
||||
},
|
||||
{
|
||||
value: DEVOPS_TICKETING_SYSTEMS_INTEGRATIONS_GOAL,
|
||||
label: i18n.baseText('personalizationModal.ticketingSystemsIntegrations'),
|
||||
},
|
||||
{
|
||||
value: OTHER_AUTOMATION_GOAL,
|
||||
label: i18n.baseText('personalizationModal.other'),
|
||||
},
|
||||
],
|
||||
},
|
||||
shouldDisplay(values): boolean {
|
||||
const companyType = (values as IPersonalizationLatestVersion)[COMPANY_TYPE_KEY];
|
||||
const role = (values as IPersonalizationLatestVersion)[ROLE_KEY] as string;
|
||||
return (
|
||||
companyType !== PERSONAL_COMPANY_TYPE &&
|
||||
[ROLE_DEVOPS, ROLE_ENGINEERING, ROLE_IT].includes(role)
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: DEVOPS_AUTOMATION_GOAL_OTHER_KEY,
|
||||
properties: {
|
||||
placeholder: i18n.baseText('personalizationModal.specifyYourAutomationGoal'),
|
||||
},
|
||||
shouldDisplay(values): boolean {
|
||||
const companyType = (values as IPersonalizationLatestVersion)[COMPANY_TYPE_KEY];
|
||||
const goals = (values as IPersonalizationLatestVersion)[DEVOPS_AUTOMATION_GOAL_KEY];
|
||||
const role = (values as IPersonalizationLatestVersion)[ROLE_KEY] as string;
|
||||
return (
|
||||
companyType !== PERSONAL_COMPANY_TYPE &&
|
||||
[ROLE_DEVOPS, ROLE_ENGINEERING, ROLE_IT].includes(role) &&
|
||||
!!goals &&
|
||||
goals.includes(DEVOPS_AUTOMATION_OTHER)
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: MARKETING_AUTOMATION_GOAL_KEY,
|
||||
properties: {
|
||||
type: 'multi-select',
|
||||
label: i18n.baseText('personalizationModal.specifySalesMarketingGoal'),
|
||||
placeholder: i18n.baseText('personalizationModal.select'),
|
||||
options: [
|
||||
{
|
||||
label: i18n.baseText('personalizationModal.leadGeneration'),
|
||||
value: MARKETING_AUTOMATION_LEAD_GENERATION_GOAL,
|
||||
},
|
||||
{
|
||||
label: i18n.baseText('personalizationModal.customerCommunication'),
|
||||
value: MARKETING_AUTOMATION_CUSTOMER_COMMUNICATION,
|
||||
},
|
||||
{
|
||||
label: i18n.baseText('personalizationModal.customerActions'),
|
||||
value: MARKETING_AUTOMATION_ACTIONS,
|
||||
},
|
||||
{
|
||||
label: i18n.baseText('personalizationModal.adCampaign'),
|
||||
value: MARKETING_AUTOMATION_AD_CAMPAIGN,
|
||||
},
|
||||
{
|
||||
label: i18n.baseText('personalizationModal.reporting'),
|
||||
value: MARKETING_AUTOMATION_REPORTING,
|
||||
},
|
||||
{
|
||||
label: i18n.baseText('personalizationModal.dataSynching'),
|
||||
value: MARKETING_AUTOMATION_DATA_SYNCHING,
|
||||
},
|
||||
{
|
||||
label: i18n.baseText('personalizationModal.other'),
|
||||
value: MARKETING_AUTOMATION_OTHER,
|
||||
},
|
||||
],
|
||||
},
|
||||
shouldDisplay(values): boolean {
|
||||
const companyType = (values as IPersonalizationLatestVersion)[COMPANY_TYPE_KEY];
|
||||
const role = (values as IPersonalizationLatestVersion)[ROLE_KEY];
|
||||
return companyType !== PERSONAL_COMPANY_TYPE && role === ROLE_SALES_AND_MARKETING;
|
||||
},
|
||||
},
|
||||
{
|
||||
name: OTHER_MARKETING_AUTOMATION_GOAL_KEY,
|
||||
properties: {
|
||||
placeholder: i18n.baseText('personalizationModal.specifyOtherSalesAndMarketingGoal'),
|
||||
},
|
||||
shouldDisplay(values): boolean {
|
||||
const companyType = (values as IPersonalizationLatestVersion)[COMPANY_TYPE_KEY];
|
||||
const goals = (values as IPersonalizationLatestVersion)[MARKETING_AUTOMATION_GOAL_KEY];
|
||||
const role = (values as IPersonalizationLatestVersion)[ROLE_KEY];
|
||||
return (
|
||||
companyType !== PERSONAL_COMPANY_TYPE &&
|
||||
role === ROLE_SALES_AND_MARKETING &&
|
||||
!!goals &&
|
||||
goals.includes(MARKETING_AUTOMATION_OTHER)
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: AUTOMATION_BENEFICIARY_KEY,
|
||||
properties: {
|
||||
type: 'select',
|
||||
label: i18n.baseText('personalizationModal.specifyAutomationBeneficiary'),
|
||||
placeholder: i18n.baseText('personalizationModal.select'),
|
||||
options: [
|
||||
{
|
||||
label: i18n.baseText('personalizationModal.myself'),
|
||||
value: AUTOMATION_BENEFICIARY_SELF,
|
||||
},
|
||||
{
|
||||
label: i18n.baseText('personalizationModal.myTeam'),
|
||||
value: AUTOMATION_BENEFICIARY_MY_TEAM,
|
||||
},
|
||||
{
|
||||
label: i18n.baseText('personalizationModal.otherTeams'),
|
||||
value: AUTOMATION_BENEFICIARY_OTHER_TEAMS,
|
||||
},
|
||||
],
|
||||
},
|
||||
shouldDisplay(values): boolean {
|
||||
const companyType = (values as IPersonalizationLatestVersion)[COMPANY_TYPE_KEY];
|
||||
return companyType !== PERSONAL_COMPANY_TYPE;
|
||||
},
|
||||
},
|
||||
{
|
||||
name: COMPANY_SIZE_KEY,
|
||||
properties: {
|
||||
type: 'select',
|
||||
label: i18n.baseText('personalizationModal.howBigIsYourCompany'),
|
||||
placeholder: i18n.baseText('personalizationModal.select'),
|
||||
options: [
|
||||
{
|
||||
label: i18n.baseText('personalizationModal.lessThan20People'),
|
||||
value: COMPANY_SIZE_20_OR_LESS,
|
||||
},
|
||||
{
|
||||
label: `20-99 ${i18n.baseText('personalizationModal.people')}`,
|
||||
value: COMPANY_SIZE_20_99,
|
||||
},
|
||||
{
|
||||
label: `100-499 ${i18n.baseText('personalizationModal.people')}`,
|
||||
value: COMPANY_SIZE_100_499,
|
||||
},
|
||||
{
|
||||
label: `500-999 ${i18n.baseText('personalizationModal.people')}`,
|
||||
value: COMPANY_SIZE_500_999,
|
||||
},
|
||||
{
|
||||
label: `1000+ ${i18n.baseText('personalizationModal.people')}`,
|
||||
value: COMPANY_SIZE_1000_OR_MORE,
|
||||
},
|
||||
{
|
||||
label: i18n.baseText('personalizationModal.imNotUsingN8nForWork'),
|
||||
value: COMPANY_SIZE_PERSONAL_USE,
|
||||
},
|
||||
],
|
||||
},
|
||||
shouldDisplay(values): boolean {
|
||||
const companyType = (values as IPersonalizationLatestVersion)[COMPANY_TYPE_KEY];
|
||||
return companyType !== PERSONAL_COMPANY_TYPE;
|
||||
},
|
||||
},
|
||||
{
|
||||
name: REPORTED_SOURCE_KEY,
|
||||
properties: {
|
||||
type: 'select',
|
||||
label: i18n.baseText('personalizationModal.howDidYouHearAboutN8n'),
|
||||
placeholder: i18n.baseText('personalizationModal.select'),
|
||||
options: [
|
||||
{
|
||||
label: 'Google',
|
||||
value: REPORTED_SOURCE_GOOGLE,
|
||||
},
|
||||
{
|
||||
label: 'Twitter',
|
||||
value: REPORTED_SOURCE_TWITTER,
|
||||
},
|
||||
{
|
||||
label: 'LinkedIn',
|
||||
value: REPORTED_SOURCE_LINKEDIN,
|
||||
},
|
||||
{
|
||||
label: 'YouTube',
|
||||
value: REPORTED_SOURCE_YOUTUBE,
|
||||
},
|
||||
{
|
||||
label: i18n.baseText('personalizationModal.friendWordOfMouth'),
|
||||
value: REPORTED_SOURCE_FRIEND,
|
||||
},
|
||||
{
|
||||
label: i18n.baseText('personalizationModal.podcast'),
|
||||
value: REPORTED_SOURCE_PODCAST,
|
||||
},
|
||||
{
|
||||
label: i18n.baseText('personalizationModal.event'),
|
||||
value: REPORTED_SOURCE_EVENT,
|
||||
},
|
||||
{
|
||||
label: i18n.baseText('personalizationModal.otherPleaseSpecify'),
|
||||
value: REPORTED_SOURCE_OTHER,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: REPORTED_SOURCE_OTHER_KEY,
|
||||
properties: {
|
||||
placeholder: i18n.baseText('personalizationModal.specifyReportedSource'),
|
||||
},
|
||||
shouldDisplay(values): boolean {
|
||||
const reportedSource = (values as IPersonalizationLatestVersion)[REPORTED_SOURCE_KEY];
|
||||
return reportedSource === REPORTED_SOURCE_OTHER;
|
||||
},
|
||||
},
|
||||
]);
|
||||
},
|
||||
] as const,
|
||||
);
|
||||
|
||||
const onSave = () => {
|
||||
formBus.emit('submit');
|
||||
@@ -575,7 +578,7 @@ const closeDialog = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const onSubmit = async (values: IPersonalizationLatestVersion) => {
|
||||
const onSubmit = async (values: object) => {
|
||||
isSaving.value = true;
|
||||
|
||||
try {
|
||||
|
||||
@@ -4,11 +4,7 @@ import { useI18n } from '@n8n/i18n';
|
||||
import { ResourceType, splitName } from '@/utils/projects.utils';
|
||||
import type { Project, ProjectIcon as BadgeIcon } from '@/types/projects.types';
|
||||
import { ProjectTypes } from '@/types/projects.types';
|
||||
import type {
|
||||
CredentialsResource,
|
||||
FolderResource,
|
||||
WorkflowResource,
|
||||
} from '../layouts/ResourcesListLayout.vue';
|
||||
import type { CredentialsResource, FolderResource, WorkflowResource } from '@/Interface';
|
||||
import { VIEWS } from '@/constants';
|
||||
|
||||
type Props = {
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
<script lang="ts" setup>
|
||||
import type { ButtonType } from '@n8n/design-system';
|
||||
import type { ButtonType, UserAction } from '@n8n/design-system';
|
||||
import { N8nIconButton, N8nActionToggle } from '@n8n/design-system';
|
||||
import { ref } from 'vue';
|
||||
import type { IUser } from 'n8n-workflow';
|
||||
import { useTemplateRef } from 'vue';
|
||||
|
||||
type Action = {
|
||||
label: string;
|
||||
value: string;
|
||||
disabled: boolean;
|
||||
};
|
||||
defineProps<{
|
||||
actions: Action[];
|
||||
actions: Array<UserAction<IUser>>;
|
||||
disabled?: boolean;
|
||||
type?: ButtonType;
|
||||
}>();
|
||||
@@ -18,7 +14,7 @@ const emit = defineEmits<{
|
||||
action: [id: string];
|
||||
}>();
|
||||
|
||||
const actionToggleRef = ref<InstanceType<typeof N8nActionToggle> | null>(null);
|
||||
const actionToggleRef = useTemplateRef('actionToggleRef');
|
||||
|
||||
defineExpose({
|
||||
openActionToggle: (isOpen: boolean) => actionToggleRef.value?.openActionToggle(isOpen),
|
||||
|
||||
@@ -16,6 +16,7 @@ import ProjectCreateResource from '@/components/Projects/ProjectCreateResource.v
|
||||
import { useSettingsStore } from '@/stores/settings.store';
|
||||
import { useProjectPages } from '@/composables/useProjectPages';
|
||||
import { truncateTextToFitWidth } from '@/utils/formatters/textFormatter';
|
||||
import type { IUser } from 'n8n-workflow';
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
@@ -96,7 +97,7 @@ const createWorkflowButton = computed(() => ({
|
||||
}));
|
||||
|
||||
const menu = computed(() => {
|
||||
const items: UserAction[] = [
|
||||
const items: Array<UserAction<IUser>> = [
|
||||
{
|
||||
value: ACTION_TYPES.CREDENTIAL,
|
||||
label: i18n.baseText('projects.header.create.credential'),
|
||||
|
||||
@@ -240,7 +240,7 @@ onMounted(async () => {
|
||||
v-for="p in filteredProjects"
|
||||
:key="p.id"
|
||||
:value="p.id"
|
||||
:label="p.name"
|
||||
:label="p.name ?? ''"
|
||||
></N8nOption>
|
||||
</N8nSelect>
|
||||
<N8nText>
|
||||
|
||||
@@ -43,10 +43,10 @@ const shared = computed<IMenuItem>(() => ({
|
||||
},
|
||||
}));
|
||||
|
||||
const getProjectMenuItem = (project: ProjectListItem) => ({
|
||||
const getProjectMenuItem = (project: ProjectListItem): IMenuItem => ({
|
||||
id: project.id,
|
||||
label: project.name,
|
||||
icon: project.icon,
|
||||
label: project.name ?? '',
|
||||
icon: project.icon as IMenuItem['icon'],
|
||||
route: {
|
||||
to: {
|
||||
name: VIEWS.PROJECTS_WORKFLOWS,
|
||||
@@ -70,6 +70,14 @@ const personalProject = computed<IMenuItem>(() => ({
|
||||
const showAddFirstProject = computed(
|
||||
() => projectsStore.isTeamProjectFeatureEnabled && !displayProjects.value.length,
|
||||
);
|
||||
|
||||
const activeTabId = computed(() => {
|
||||
return (
|
||||
(Array.isArray(projectsStore.projectNavActiveId)
|
||||
? projectsStore.projectNavActiveId[0]
|
||||
: projectsStore.projectNavActiveId) ?? undefined
|
||||
);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -78,7 +86,7 @@ const showAddFirstProject = computed(
|
||||
<N8nMenuItem
|
||||
:item="home"
|
||||
:compact="props.collapsed"
|
||||
:active-tab="projectsStore.projectNavActiveId"
|
||||
:active-tab="activeTabId"
|
||||
mode="tabs"
|
||||
data-test-id="project-home-menu-item"
|
||||
/>
|
||||
@@ -86,7 +94,7 @@ const showAddFirstProject = computed(
|
||||
v-if="projectsStore.isTeamProjectFeatureEnabled || isFoldersFeatureEnabled"
|
||||
:item="personalProject"
|
||||
:compact="props.collapsed"
|
||||
:active-tab="projectsStore.projectNavActiveId"
|
||||
:active-tab="activeTabId"
|
||||
mode="tabs"
|
||||
data-test-id="project-personal-menu-item"
|
||||
/>
|
||||
@@ -94,7 +102,7 @@ const showAddFirstProject = computed(
|
||||
v-if="projectsStore.isTeamProjectFeatureEnabled || isFoldersFeatureEnabled"
|
||||
:item="shared"
|
||||
:compact="props.collapsed"
|
||||
:active-tab="projectsStore.projectNavActiveId"
|
||||
:active-tab="activeTabId"
|
||||
mode="tabs"
|
||||
data-test-id="project-shared-menu-item"
|
||||
/>
|
||||
@@ -136,7 +144,7 @@ const showAddFirstProject = computed(
|
||||
}"
|
||||
:item="getProjectMenuItem(project)"
|
||||
:compact="props.collapsed"
|
||||
:active-tab="projectsStore.projectNavActiveId"
|
||||
:active-tab="activeTabId"
|
||||
mode="tabs"
|
||||
data-test-id="project-menu-item"
|
||||
/>
|
||||
|
||||
@@ -136,7 +136,7 @@ watch(
|
||||
v-for="project in filteredProjects"
|
||||
:key="project.id"
|
||||
:value="project.id"
|
||||
:label="project.name"
|
||||
:label="project.name ?? ''"
|
||||
>
|
||||
<ProjectSharingInfo :project="project" />
|
||||
</N8nOption>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useRoute } from 'vue-router';
|
||||
import { VIEWS } from '@/constants';
|
||||
import { useI18n } from '@n8n/i18n';
|
||||
import type { BaseTextKey } from '@n8n/i18n';
|
||||
import type { TabOptions } from '@n8n/design-system';
|
||||
|
||||
type Props = {
|
||||
showSettings?: boolean;
|
||||
@@ -23,6 +24,8 @@ const route = useRoute();
|
||||
|
||||
const selectedTab = ref<RouteRecordName | null | undefined>('');
|
||||
|
||||
const selectedTabLabel = computed(() => (selectedTab.value ? String(selectedTab.value) : ''));
|
||||
|
||||
const projectId = computed(() => {
|
||||
return Array.isArray(route?.params?.projectId)
|
||||
? route.params.projectId[0]
|
||||
@@ -70,16 +73,16 @@ const createTab = (
|
||||
label: BaseTextKey,
|
||||
routeKey: string,
|
||||
routes: Record<string, { name: RouteRecordName; params?: Record<string, string | number> }>,
|
||||
) => {
|
||||
): TabOptions<string> => {
|
||||
return {
|
||||
label: locale.baseText(label),
|
||||
value: routes[routeKey].name,
|
||||
value: routes[routeKey].name as string,
|
||||
to: routes[routeKey],
|
||||
};
|
||||
};
|
||||
|
||||
// Generate the tabs configuration
|
||||
const options = computed(() => {
|
||||
const options = computed<Array<TabOptions<string>>>(() => {
|
||||
const routes = getRouteConfigs();
|
||||
const tabs = [
|
||||
createTab('mainSidebar.workflows', 'workflows', routes),
|
||||
@@ -93,7 +96,7 @@ const options = computed(() => {
|
||||
if (props.showSettings) {
|
||||
tabs.push({
|
||||
label: locale.baseText('projects.settings'),
|
||||
value: VIEWS.PROJECT_SETTINGS,
|
||||
value: VIEWS.PROJECT_SETTINGS as string,
|
||||
to: { name: VIEWS.PROJECT_SETTINGS, params: { projectId: projectId.value } },
|
||||
});
|
||||
}
|
||||
@@ -110,8 +113,17 @@ watch(
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
function onSelectTab(value: string | number) {
|
||||
selectedTab.value = value as RouteRecordName;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<N8nTabs v-model="selectedTab" :options="options" data-test-id="project-tabs" />
|
||||
<N8nTabs
|
||||
:model-value="selectedTabLabel"
|
||||
:options="options"
|
||||
data-test-id="project-tabs"
|
||||
@update:model-value="onSelectTab"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@@ -4,7 +4,7 @@ import Modal from '../Modal.vue';
|
||||
import { PROMPT_MFA_CODE_MODAL_KEY } from '@/constants';
|
||||
import { useI18n } from '@n8n/i18n';
|
||||
import { promptMfaCodeBus } from '@/event-bus';
|
||||
import type { IFormInputs } from '@/Interface';
|
||||
import { type IFormInput } from '@/Interface';
|
||||
import { createFormEventBus } from '@n8n/design-system/utils';
|
||||
import { validate as validateUuid } from 'uuid';
|
||||
|
||||
@@ -13,7 +13,7 @@ const i18n = useI18n();
|
||||
const formBus = createFormEventBus();
|
||||
const readyToSubmit = ref(false);
|
||||
|
||||
const formFields: IFormInputs = [
|
||||
const formFields: IFormInput[] = [
|
||||
{
|
||||
name: 'mfaCodeOrMfaRecoveryCode',
|
||||
initialValue: '',
|
||||
@@ -25,9 +25,14 @@ const formFields: IFormInputs = [
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
];
|
||||
] as const;
|
||||
|
||||
function onSubmit(values: { mfaCodeOrMfaRecoveryCode: string }) {
|
||||
function onSubmit(values: object) {
|
||||
if (
|
||||
!('mfaCodeOrMfaRecoveryCode' in values && typeof values.mfaCodeOrMfaRecoveryCode === 'string')
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (validateUuid(values.mfaCodeOrMfaRecoveryCode)) {
|
||||
promptMfaCodeBus.emit('close', {
|
||||
mfaRecoveryCode: values.mfaCodeOrMfaRecoveryCode,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user