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