ci: Ensure that eslint runs on all frontend code (no-changelog) (#4602)

* ensure that eslint runs on all frontend code

* remove tslint from `design-system`

* enable prettier and eslint-prettier for `design-system`

* Delete tslint.json

* use a single editorconfig for the repo

* enable prettier for all code in `design-system`

* more linting fixes on design-system

* ignore coverage for git and prettier

* lintfix on editor-ui
This commit is contained in:
कारतोफ्फेलस्क्रिप्ट™ 2022-11-15 18:20:54 +01:00 committed by GitHub
parent d96d6f11db
commit 13659d036f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
227 changed files with 2222 additions and 2566 deletions

View file

@ -15,3 +15,6 @@ indent_size = 2
[*.yml] [*.yml]
indent_style = space indent_style = space
indent_size = 2 indent_size = 2
[*.ts]
quote_type = single

View file

@ -1,7 +1,6 @@
dist dist
packages/editor-ui packages/editor-ui
packages/design-system package.json
package*.json
!packages/nodes-base/src !packages/nodes-base/src
!packages/nodes-base/test !packages/nodes-base/test

View file

@ -9,11 +9,9 @@ const config = (module.exports = {
}, },
ignorePatterns: [ ignorePatterns: [
'.eslintrc.js', // TODO: remove this
'node_modules/**', 'node_modules/**',
'dist/**', 'dist/**',
'test/**', // TODO: remove this 'test/**', // TODO: remove this
'jest.config.js', // TODO: remove this
], ],
plugins: [ plugins: [

View file

@ -14,10 +14,15 @@ module.exports = {
parser: 'vue-eslint-parser', parser: 'vue-eslint-parser',
parserOptions: { parserOptions: {
parser: '@typescript-eslint/parser', parser: {
ts: '@typescript-eslint/parser',
js: '@typescript-eslint/parser',
vue: 'vue-eslint-parser',
template: 'vue-eslint-parser',
},
}, },
ignorePatterns: ['**/*.js', '**/*.d.ts', 'vite.config.ts'], ignorePatterns: ['**/*.js', '**/*.d.ts', 'vite.config.ts', '**/*.ts.snap'],
rules: { rules: {
'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off', 'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off',

View file

@ -1,15 +0,0 @@
root = true
[*]
charset = utf-8
indent_style = tab
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
[package.json]
indent_style = space
indent_size = 2
[*.ts]
quote_type = single

View file

@ -6,23 +6,50 @@ module.exports = {
parserOptions: { parserOptions: {
project: ['./tsconfig.json'], project: ['./tsconfig.json'],
tsconfigRootDir: __dirname,
extraFileExtensions: ['.vue'],
}, },
rules: { rules: {
// TODO: Remove these // TODO: Remove these
'import/no-default-export': 'off', 'import/no-default-export': 'off',
'import/no-extraneous-dependencies': 'off',
'import/order': 'off', 'import/order': 'off',
'prettier/prettier': 'off', '@typescript-eslint/no-explicit-any': 'warn',
'@typescript-eslint/member-delimiter-style': 'off', '@typescript-eslint/no-unsafe-argument': 'warn',
'@typescript-eslint/naming-convention': 'off', '@typescript-eslint/no-unsafe-return': 'warn',
'@typescript-eslint/no-explicit-any': 'off', '@typescript-eslint/no-unsafe-member-access': 'warn',
'@typescript-eslint/no-unsafe-argument': 'off', },
'@typescript-eslint/no-unsafe-return': 'off',
'@typescript-eslint/no-unused-vars': 'off', overrides: [
'@typescript-eslint/prefer-nullish-coalescing': 'off', {
'@typescript-eslint/prefer-optional-chain': 'off', files: ['src/**/*.stories.{js,ts}'],
'@typescript-eslint/restrict-template-expressions': 'off', rules: {
'@typescript-eslint/ban-ts-comment': ['warn', { 'ts-ignore': true }], 'import/no-extraneous-dependencies': ['error', { devDependencies: true }],
} },
},
{
files: ['src/**/*.stories.{js,ts}', 'src/**/*.vue', 'src/**/*.spec.ts'],
rules: {
'@typescript-eslint/naming-convention': [
'warn',
{
selector: ['variable', 'property'],
format: ['PascalCase', 'camelCase', 'UPPER_CASE'],
},
],
},
},
{
files: ['src/components/N8nFormInput/validators.ts'],
rules: {
'@typescript-eslint/naming-convention': [
'error',
{
selector: ['property'],
format: ['camelCase', 'UPPER_CASE'],
},
],
},
},
],
}; };

View file

@ -1 +1,2 @@
coverage
storybook-static storybook-static

View file

@ -0,0 +1,3 @@
coverage
dist
package.json

View file

@ -14,7 +14,7 @@ module.exports = {
postcssLoaderOptions: { postcssLoaderOptions: {
implementation: require('postcss'), implementation: require('postcss'),
}, },
} },
}, },
'storybook-addon-designs', 'storybook-addon-designs',
'storybook-addon-themes', 'storybook-addon-themes',

View file

@ -1,11 +1,11 @@
@use "./fonts.scss"; @use './fonts.scss';
@use "~/src/css/base.scss" with ( @use '~/src/css/base.scss' with (
$font-path: '~element-ui/lib/theme-chalk/fonts', $font-path: '~element-ui/lib/theme-chalk/fonts'
); );
@use "~/src/css/reset.scss"; @use '~/src/css/reset.scss';
@use "~/src/css/index.scss"; @use '~/src/css/index.scss';
.multi-container > * { .multi-container > * {
margin-bottom: 10px; margin-bottom: 10px;

View file

@ -21,15 +21,22 @@
"test:dev": "vitest", "test:dev": "vitest",
"build:storybook": "build-storybook", "build:storybook": "build-storybook",
"storybook": "start-storybook -p 6006", "storybook": "start-storybook -p 6006",
"format": "prettier **/**.{ts,vue} --write", "format": "prettier **/**.{js,ts,vue,css,scss,mdx,html} --write .",
"lint": "tslint -p tsconfig.json -c tslint.json && eslint .", "lint": "eslint --ext .js,.ts,.vue src",
"lintfix": "tslint --fix -p tsconfig.json -c tslint.json && eslint . --fix" "lintfix": "eslint --ext .js,.ts,.vue src --fix"
}, },
"peerDependencies": { "peerDependencies": {
"@fortawesome/fontawesome-svg-core": "1.x", "@fortawesome/fontawesome-svg-core": "1.x",
"@fortawesome/free-solid-svg-icons": "5.x", "@fortawesome/free-solid-svg-icons": "5.x",
"@fortawesome/vue-fontawesome": "2.x", "@fortawesome/vue-fontawesome": "2.x",
"core-js": "3.x" "core-js": "3.x",
"markdown-it": "^12.3.2",
"markdown-it-emoji": "^2.0.0",
"markdown-it-link-attributes": "^4.0.0",
"markdown-it-task-lists": "^2.1.1",
"vue": "^2.7",
"vue-typed-mixins": "^0.2.0",
"xss": "^1.0.10"
}, },
"devDependencies": { "devDependencies": {
"@fortawesome/fontawesome-svg-core": "^1.2.35", "@fortawesome/fontawesome-svg-core": "^1.2.35",
@ -43,14 +50,12 @@
"@testing-library/jest-dom": "^5.16.4", "@testing-library/jest-dom": "^5.16.4",
"@testing-library/vue": "^5.8.3", "@testing-library/vue": "^5.8.3",
"@types/markdown-it": "^12.2.3", "@types/markdown-it": "^12.2.3",
"@types/markdown-it-emoji": "^2.0.2",
"@types/markdown-it-link-attributes": "^3.0.1",
"@types/sanitize-html": "^2.6.2", "@types/sanitize-html": "^2.6.2",
"c8": "7.11.0", "c8": "7.11.0",
"core-js": "^3.6.5", "core-js": "^3.6.5",
"jsdom": "19.0.0", "jsdom": "19.0.0",
"markdown-it": "^12.3.2",
"markdown-it-emoji": "^2.0.0",
"markdown-it-link-attributes": "^4.0.0",
"markdown-it-task-lists": "^2.1.1",
"node-notifier": ">=8.0.1", "node-notifier": ">=8.0.1",
"sass": "^1.55.0", "sass": "^1.55.0",
"sass-loader": "^10.1.1", "sass-loader": "^10.1.1",
@ -60,16 +65,13 @@
"vite": "^2.9.5", "vite": "^2.9.5",
"@vitejs/plugin-vue2": "^1.1.2", "@vitejs/plugin-vue2": "^1.1.2",
"vitest": "^0.9.3", "vitest": "^0.9.3",
"vue": "^2.7",
"vue-class-component": "^7.2.3", "vue-class-component": "^7.2.3",
"vue-loader": "^15.9.7", "vue-loader": "^15.9.7",
"vue-property-decorator": "^9.1.2", "vue-property-decorator": "^9.1.2",
"vue-template-compiler": "^2.7", "vue-template-compiler": "^2.7",
"vue-tsc": "^0.34.8", "vue-tsc": "^0.34.8",
"vue-typed-mixins": "^0.2.0",
"vue2-boring-avatars": "0.3.4", "vue2-boring-avatars": "0.3.4",
"webpack": "^4.46.0", "webpack": "^4.46.0"
"xss": "^1.0.10"
}, },
"dependencies": { "dependencies": {
"element-ui": "~2.15.7", "element-ui": "~2.15.7",

View file

@ -1,8 +1,6 @@
/* tslint:disable:variable-name */
import N8nActionBox from './ActionBox.vue'; import N8nActionBox from './ActionBox.vue';
import { action } from '@storybook/addon-actions'; import { action } from '@storybook/addon-actions';
import {StoryFn} from "@storybook/vue"; import type { StoryFn } from '@storybook/vue';
export default { export default {
title: 'Atoms/ActionBox', title: 'Atoms/ActionBox',
@ -35,8 +33,9 @@ const Template: StoryFn = (args, { argTypes }) => ({
export const ActionBox = Template.bind({}); export const ActionBox = Template.bind({});
ActionBox.args = { ActionBox.args = {
emoji: "😿", emoji: '😿',
heading: "Headline you need to know", heading: 'Headline you need to know',
description: "Long description that you should know something is the way it is because of how it is. ", description:
buttonText: "Do something", 'Long description that you should know something is the way it is because of how it is. ',
buttonText: 'Do something',
}; };

View file

@ -118,5 +118,4 @@ export default Vue.extend({
width: 100%; width: 100%;
text-align: left; text-align: left;
} }
</style> </style>

View file

@ -1,21 +1,17 @@
import {render} from '@testing-library/vue'; import { render } from '@testing-library/vue';
import N8NActionBox from '../ActionBox.vue'; import N8NActionBox from '../ActionBox.vue';
describe('N8NActionBox', () => { describe('N8NActionBox', () => {
it('should render correctly', () => { it('should render correctly', () => {
const wrapper = render(N8NActionBox, { const wrapper = render(N8NActionBox, {
props: { props: {
emoji: "😿", emoji: '😿',
heading: "Headline you need to know", heading: 'Headline you need to know',
description: "Long description that you should know something is the way it is because of how it is. ", description:
buttonText: "Do something", 'Long description that you should know something is the way it is because of how it is. ',
buttonText: 'Do something',
}, },
stubs: [ stubs: ['n8n-heading', 'n8n-text', 'n8n-button', 'n8n-callout'],
'n8n-heading',
'n8n-text',
'n8n-button',
'n8n-callout',
],
}); });
expect(wrapper.html()).toMatchSnapshot(); expect(wrapper.html()).toMatchSnapshot();
}); });

View file

@ -1,5 +1,5 @@
import N8nActionDropdown from "./ActionDropdown.vue"; import N8nActionDropdown from './ActionDropdown.vue';
import { StoryFn } from '@storybook/vue'; import type { StoryFn } from '@storybook/vue';
export default { export default {
title: 'Atoms/ActionDropdown', title: 'Atoms/ActionDropdown',

View file

@ -1,8 +1,13 @@
<template> <template>
<div :class="['action-dropdown-container', $style.actionDropdownContainer]"> <div :class="['action-dropdown-container', $style.actionDropdownContainer]">
<el-dropdown :placement="placement" :trigger="trigger" @command="onSelect" ref="elementDropdown"> <el-dropdown
:placement="placement"
:trigger="trigger"
@command="onSelect"
ref="elementDropdown"
>
<div :class="$style.activator" @click.prevent @blur="onButtonBlur"> <div :class="$style.activator" @click.prevent @blur="onButtonBlur">
<n8n-icon :icon="activatorIcon"/> <n8n-icon :icon="activatorIcon" />
</div> </div>
<el-dropdown-menu slot="dropdown" :class="$style.userActionsMenu"> <el-dropdown-menu slot="dropdown" :class="$style.userActionsMenu">
<el-dropdown-item <el-dropdown-item
@ -12,13 +17,15 @@
:disabled="item.disabled" :disabled="item.disabled"
:divided="item.divided" :divided="item.divided"
> >
<div :class="{ <div
:class="{
[$style.itemContainer]: true, [$style.itemContainer]: true,
[$style.hasCustomStyling]: item.customClass !== undefined, [$style.hasCustomStyling]: item.customClass !== undefined,
[item.customClass]: item.customClass !== undefined, [item.customClass]: item.customClass !== undefined,
}"> }"
>
<span v-if="item.icon" :class="$style.icon"> <span v-if="item.icon" :class="$style.icon">
<n8n-icon :icon="item.icon" :size="item.iconSize"/> <n8n-icon :icon="item.icon" :size="item.iconSize" />
</span> </span>
<span :class="$style.label"> <span :class="$style.label">
{{ item.label }} {{ item.label }}
@ -31,7 +38,7 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import Vue, { PropType } from "vue"; import Vue, { PropType } from 'vue';
import ElDropdown from 'element-ui/lib/dropdown'; import ElDropdown from 'element-ui/lib/dropdown';
import ElDropdownMenu from 'element-ui/lib/dropdown-menu'; import ElDropdownMenu from 'element-ui/lib/dropdown-menu';
import ElDropdownItem from 'element-ui/lib/dropdown-item'; import ElDropdownItem from 'element-ui/lib/dropdown-item';
@ -78,22 +85,22 @@ export default Vue.extend({
iconSize: { iconSize: {
type: String, type: String,
default: 'medium', default: 'medium',
validator: (value: string): boolean => validator: (value: string): boolean => ['small', 'medium', 'large'].includes(value),
['small', 'medium', 'large'].includes(value),
}, },
trigger: { trigger: {
type: String, type: String,
default: 'click', default: 'click',
validator: (value: string): boolean => validator: (value: string): boolean => ['click', 'hover'].includes(value),
['click', 'hover'].includes(value),
}, },
}, },
methods: { methods: {
onSelect(action: string) : void { onSelect(action: string): void {
this.$emit('select', action); this.$emit('select', action);
}, },
onButtonBlur(event: FocusEvent): void { onButtonBlur(event: FocusEvent): void {
const elementDropdown = this.$refs.elementDropdown as Vue & { hide: () => void } | undefined; const elementDropdown = this.$refs.elementDropdown as
| (Vue & { hide: () => void })
| undefined;
// Hide dropdown when clicking outside of current document // Hide dropdown when clicking outside of current document
if (elementDropdown && event.relatedTarget === null) { if (elementDropdown && event.relatedTarget === null) {
elementDropdown.hide(); elementDropdown.hide();
@ -101,11 +108,9 @@ export default Vue.extend({
}, },
}, },
}); });
</script> </script>
<style lang="scss" module> <style lang="scss" module>
.activator { .activator {
cursor: pointer; cursor: pointer;
padding: var(--spacing-2xs); padding: var(--spacing-2xs);
@ -131,7 +136,9 @@ export default Vue.extend({
text-align: center; text-align: center;
margin-right: var(--spacing-2xs); margin-right: var(--spacing-2xs);
svg { width: 1.2em !important; } svg {
width: 1.2em !important;
}
} }
:global(li.is-disabled) { :global(li.is-disabled) {
@ -139,5 +146,4 @@ export default Vue.extend({
color: inherit !important; color: inherit !important;
} }
} }
</style> </style>

View file

@ -28,7 +28,8 @@ const Template = (args, { argTypes }) => ({
components: { components: {
N8nActionToggle, N8nActionToggle,
}, },
template: '<div style="height:300px;width:300px;display:flex;align-items:center;justify-content:center"><n8n-action-toggle v-bind="$props" @action="onAction" /></div>', template:
'<div style="height:300px;width:300px;display:flex;align-items:center;justify-content:center"><n8n-action-toggle v-bind="$props" @action="onAction" /></div>',
methods, methods,
}); });

View file

@ -8,11 +8,8 @@
@command="onCommand" @command="onCommand"
@visible-change="onVisibleChange" @visible-change="onVisibleChange"
> >
<span :class="{[$style.button]: true, [$style[theme]]: !!theme}"> <span :class="{ [$style.button]: true, [$style[theme]]: !!theme }">
<component :is="$options.components.N8nIcon" <component :is="$options.components.N8nIcon" icon="ellipsis-v" :size="iconSize" />
icon="ellipsis-v"
:size="iconSize"
/>
</span> </span>
<el-dropdown-menu slot="dropdown" data-test-id="action-toggle-dropdown"> <el-dropdown-menu slot="dropdown" data-test-id="action-toggle-dropdown">
<el-dropdown-item <el-dropdown-item
@ -21,7 +18,7 @@
:command="action.value" :command="action.value"
:disabled="action.disabled" :disabled="action.disabled"
> >
{{action.label}} {{ action.label }}
<div :class="$style.iconContainer"> <div :class="$style.iconContainer">
<component <component
v-if="action.type === 'external-link'" v-if="action.type === 'external-link'"
@ -66,8 +63,7 @@ export default Vue.extend({
size: { size: {
type: String, type: String,
default: 'medium', default: 'medium',
validator: (value: string): boolean => validator: (value: string): boolean => ['mini', 'small', 'medium'].includes(value),
['mini', 'small', 'medium'].includes(value),
}, },
iconSize: { iconSize: {
type: String, type: String,
@ -75,8 +71,7 @@ export default Vue.extend({
theme: { theme: {
type: String, type: String,
default: 'default', default: 'default',
validator: (value: string): boolean => validator: (value: string): boolean => ['default', 'dark'].includes(value),
['default', 'dark'].includes(value),
}, },
}, },
methods: { methods: {

View file

@ -7,19 +7,15 @@
variant="marble" variant="marble"
:colors="getColors(colors)" :colors="getColors(colors)"
/> />
<div <div v-else :class="[$style.empty, $style[size]]"></div>
v-else <span v-if="firstName" :class="$style.initials">{{ initials }}</span>
:class="[$style.empty, $style[size]]"
>
</div>
<span v-if="firstName" :class="$style.initials">{{initials}}</span>
</span> </span>
</template> </template>
<script lang="ts"> <script lang="ts">
import Avatar from 'vue2-boring-avatars'; import Avatar from 'vue2-boring-avatars';
const sizes: {[size: string]: number} = { const sizes: { [size: string]: number } = {
small: 28, small: 28,
large: 48, large: 48,
medium: 40, medium: 40,
@ -41,7 +37,13 @@ export default Vue.extend({
default: 'medium', default: 'medium',
}, },
colors: { colors: {
default: () => (['--color-primary', '--color-secondary', '--color-avatar-accent-1', '--color-avatar-accent-2', '--color-primary-tint-1']), default: () => [
'--color-primary',
'--color-secondary',
'--color-avatar-accent-1',
'--color-avatar-accent-2',
'--color-primary-tint-1',
],
}, },
}, },
components: { components: {
@ -49,7 +51,10 @@ export default Vue.extend({
}, },
computed: { computed: {
initials() { initials() {
return (this.firstName ? this.firstName.charAt(0): '') + (this.lastName? this.lastName.charAt(0): ''); return (
(this.firstName ? this.firstName.charAt(0) : '') +
(this.lastName ? this.lastName.charAt(0) : '')
);
}, },
}, },
methods: { methods: {
@ -75,7 +80,7 @@ export default Vue.extend({
.empty { .empty {
border-radius: 50%; border-radius: 50%;
background-color: var(--color-foreground-dark); background-color: var(--color-foreground-dark);
opacity: .3; opacity: 0.3;
} }
.initials { .initials {

View file

@ -20,8 +20,7 @@ const Template = (args, { argTypes }) => ({
components: { components: {
N8nBadge, N8nBadge,
}, },
template: template: '<n8n-badge v-bind="$props">Badge</n8n-badge>',
'<n8n-badge v-bind="$props">Badge</n8n-badge>',
}); });
export const Badge = Template.bind({}); export const Badge = Template.bind({});

View file

@ -1,7 +1,5 @@
<template> <template>
<span <span :class="['n8n-badge', $style[theme]]">
:class="['n8n-badge', $style[theme]]"
>
<n8n-text :size="size" :bold="bold" :compact="true"> <n8n-text :size="size" :bold="bold" :compact="true">
<slot></slot> <slot></slot>
</n8n-text> </n8n-text>

View file

@ -16,5 +16,5 @@ const Template = (args, { argTypes }) => ({
export const BlockUi = Template.bind({}); export const BlockUi = Template.bind({});
BlockUi.args = { BlockUi.args = {
show: false show: false,
}; };

View file

@ -1,15 +1,20 @@
<template> <template>
<transition name="fade" mode="out-in"> <transition name="fade" mode="out-in">
<div v-show="show" :class="['n8n-block-ui', $style.uiBlocker]" role="dialog" :aria-hidden="true" /> <div
v-show="show"
:class="['n8n-block-ui', $style.uiBlocker]"
role="dialog"
:aria-hidden="true"
/>
</transition> </transition>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
type BlockUiProps = { type BlockUiProps = {
show: boolean; show: boolean;
} };
const props = withDefaults(defineProps<BlockUiProps>(), { withDefaults(defineProps<BlockUiProps>(), {
show: false, show: false,
}); });
</script> </script>

View file

@ -1,7 +1,6 @@
/* tslint:disable:variable-name */
import N8nButton from './Button.vue'; import N8nButton from './Button.vue';
import { action } from '@storybook/addon-actions'; import { action } from '@storybook/addon-actions';
import { StoryFn } from "@storybook/vue"; import type { StoryFn } from '@storybook/vue';
export default { export default {
title: 'Atoms/Button', title: 'Atoms/Button',
@ -63,22 +62,6 @@ const AllSizesTemplate: StoryFn = (args, { argTypes }) => ({
methods, methods,
}); });
const AllColorsTemplate: StoryFn = (args, { argTypes }) => ({
props: Object.keys(argTypes),
components: {
N8nButton,
},
template: `<div>
<n8n-button v-bind="$props" type="primary" @click="onClick" />
<n8n-button v-bind="$props" type="secondary" @click="onClick" />
<n8n-button v-bind="$props" type="tertiary" @click="onClick" />
<n8n-button v-bind="$props" type="success" @click="onClick" />
<n8n-button v-bind="$props" type="warning" @click="onClick" />
<n8n-button v-bind="$props" type="danger" @click="onClick" />
</div>`,
methods,
});
const AllColorsAndSizesTemplate: StoryFn = (args, { argTypes }) => ({ const AllColorsAndSizesTemplate: StoryFn = (args, { argTypes }) => ({
props: Object.keys(argTypes), props: Object.keys(argTypes),
components: { components: {
@ -170,4 +153,3 @@ Square.args = {
label: '48', label: '48',
square: true, square: true,
}; };

View file

@ -8,15 +8,8 @@
v-on="$listeners" v-on="$listeners"
> >
<span :class="$style.icon" v-if="loading || icon"> <span :class="$style.icon" v-if="loading || icon">
<n8n-spinner <n8n-spinner v-if="loading" :size="size" />
v-if="loading" <n8n-icon v-else-if="icon" :icon="icon" :size="size" />
:size="size"
/>
<n8n-icon
v-else-if="icon"
:icon="icon"
:size="size"
/>
</span> </span>
<span v-if="label || $slots.default"> <span v-if="label || $slots.default">
<slot>{{ label }}</slot> <slot>{{ label }}</slot>
@ -76,8 +69,7 @@ export default Vue.extend({
}, },
float: { float: {
type: String, type: String,
validator: (value: string): boolean => validator: (value: string): boolean => ['left', 'right'].includes(value),
['left', 'right'].includes(value),
}, },
square: { square: {
type: Boolean, type: Boolean,
@ -96,7 +88,8 @@ export default Vue.extend({
return this.disabled ? 'true' : 'false'; return this.disabled ? 'true' : 'false';
}, },
classes(): string { classes(): string {
return `button ${this.$style.button} ${this.$style[this.type]}` + return (
`button ${this.$style.button} ${this.$style[this.type]}` +
`${this.size ? ` ${this.$style[this.size]}` : ''}` + `${this.size ? ` ${this.$style[this.size]}` : ''}` +
`${this.outline ? ` ${this.$style.outline}` : ''}` + `${this.outline ? ` ${this.$style.outline}` : ''}` +
`${this.loading ? ` ${this.$style.loading}` : ''}` + `${this.loading ? ` ${this.$style.loading}` : ''}` +
@ -106,7 +99,8 @@ export default Vue.extend({
`${this.block ? ` ${this.$style.block}` : ''}` + `${this.block ? ` ${this.$style.block}` : ''}` +
`${this.active ? ` ${this.$style.active}` : ''}` + `${this.active ? ` ${this.$style.active}` : ''}` +
`${this.icon || this.loading ? ` ${this.$style.icon}` : ''}` + `${this.icon || this.loading ? ` ${this.$style.icon}` : ''}` +
`${this.square ? ` ${this.$style.square}` : ''}`; `${this.square ? ` ${this.$style.square}` : ''}`
);
}, },
}, },
}); });
@ -150,7 +144,8 @@ export default Vue.extend({
outline: $focus-outline-width solid $button-focus-outline-color; outline: $focus-outline-width solid $button-focus-outline-color;
} }
&:active, &.active { &:active,
&.active {
color: $button-active-color; color: $button-active-color;
border-color: $button-active-border-color; border-color: $button-active-border-color;
background-color: $button-active-background-color; background-color: $button-active-background-color;
@ -213,7 +208,12 @@ $loading-overlay-background-color: rgba(255, 255, 255, 0);
--button-hover-color: var(--color-text-dark); --button-hover-color: var(--color-text-dark);
--button-hover-border-color: var(--color-neutral-800); --button-hover-border-color: var(--color-neutral-800);
--button-focus-outline-color: hsla(var(--color-neutral-h), var(--color-neutral-s), var(--color-neutral-l), 0.2); --button-focus-outline-color: hsla(
var(--color-neutral-h),
var(--color-neutral-s),
var(--color-neutral-l),
0.2
);
} }
.success { .success {
@ -227,7 +227,12 @@ $loading-overlay-background-color: rgba(255, 255, 255, 0);
--button-hover-background-color: var(--color-success-450); --button-hover-background-color: var(--color-success-450);
--button-hover-border-color: var(--color-success-450); --button-hover-border-color: var(--color-success-450);
--button-focus-outline-color: hsla(var(--color-success-h), var(--color-success-s), var(--color-success-l), 0.33); --button-focus-outline-color: hsla(
var(--color-success-h),
var(--color-success-s),
var(--color-success-l),
0.33
);
} }
.warning { .warning {
@ -241,7 +246,12 @@ $loading-overlay-background-color: rgba(255, 255, 255, 0);
--button-hover-background-color: var(--color-warning-650); --button-hover-background-color: var(--color-warning-650);
--button-hover-border-color: var(--color-warning-650); --button-hover-border-color: var(--color-warning-650);
--button-focus-outline-color: hsla(var(--color-warning-h), var(--color-warning-s), var(--color-warning-l), 0.33); --button-focus-outline-color: hsla(
var(--color-warning-h),
var(--color-warning-s),
var(--color-warning-l),
0.33
);
} }
.danger { .danger {
@ -256,7 +266,12 @@ $loading-overlay-background-color: rgba(255, 255, 255, 0);
--button-hover-background-color: var(--color-danger-700); --button-hover-background-color: var(--color-danger-700);
--button-hover-border-color: var(--color-danger-700); --button-hover-border-color: var(--color-danger-700);
--button-focus-outline-color: hsla(var(--color-danger-h), var(--color-danger-s), var(--color-danger-l), 0.33); --button-focus-outline-color: hsla(
var(--color-danger-h),
var(--color-danger-s),
var(--color-danger-l),
0.33
);
} }
/** /**

View file

@ -1,6 +1,6 @@
import {render} from '@testing-library/vue'; import { render } from '@testing-library/vue';
import N8nButton from "../Button.vue"; import N8nButton from '../Button.vue';
import ElButton from "../overrides/ElButton.vue"; import ElButton from '../overrides/ElButton.vue';
const slots = { const slots = {
default: 'Button', default: 'Button',

View file

@ -1,17 +1,17 @@
// Vitest Snapshot v1 // Vitest Snapshot v1
exports[`components > N8nButton > overrides > should render correctly 1`] = `"<button aria-disabled=\\"false\\" aria-busy=\\"false\\" aria-live=\\"polite\\" class=\\"button _button_1qq65_115 _secondary_1qq65_170 _medium_1qq65_254 _icon_1qq65_384\\" icon=\\"plus-circle\\" type=\\"secondary\\"><span class=\\"_icon_1qq65_384\\"><n8n-icon-stub icon=\\"plus-circle\\" size=\\"medium\\"></n8n-icon-stub></span><span>Button</span></button>"`; exports[`components > N8nButton > overrides > should render correctly 1`] = `"<button aria-disabled=\\"false\\" aria-busy=\\"false\\" aria-live=\\"polite\\" class=\\"button _button_10jsj_115 _secondary_10jsj_170 _medium_10jsj_274 _icon_10jsj_404\\" icon=\\"plus-circle\\" type=\\"secondary\\"><span class=\\"_icon_10jsj_404\\"><n8n-icon-stub icon=\\"plus-circle\\" size=\\"medium\\"></n8n-icon-stub></span><span>Button</span></button>"`;
exports[`components > N8nButton > props > icon > should render icon button 1`] = `"<button aria-disabled=\\"false\\" aria-busy=\\"false\\" aria-live=\\"polite\\" class=\\"button _button_1qq65_115 _primary_1qq65_288 _medium_1qq65_254 _icon_1qq65_384\\"><span class=\\"_icon_1qq65_384\\"><n8n-icon-stub icon=\\"plus-circle\\" size=\\"medium\\"></n8n-icon-stub></span><span>Button</span></button>"`; exports[`components > N8nButton > props > icon > should render icon button 1`] = `"<button aria-disabled=\\"false\\" aria-busy=\\"false\\" aria-live=\\"polite\\" class=\\"button _button_10jsj_115 _primary_10jsj_308 _medium_10jsj_274 _icon_10jsj_404\\"><span class=\\"_icon_10jsj_404\\"><n8n-icon-stub icon=\\"plus-circle\\" size=\\"medium\\"></n8n-icon-stub></span><span>Button</span></button>"`;
exports[`components > N8nButton > props > loading > should render loading spinner 1`] = `"<button disabled=\\"disabled\\" aria-disabled=\\"false\\" aria-busy=\\"true\\" aria-live=\\"polite\\" class=\\"button _button_1qq65_115 _primary_1qq65_288 _medium_1qq65_254 _loading_1qq65_355 _icon_1qq65_384\\"><span class=\\"_icon_1qq65_384\\"><n8n-spinner-stub size=\\"medium\\" type=\\"dots\\"></n8n-spinner-stub></span><span>Button</span></button>"`; exports[`components > N8nButton > props > loading > should render loading spinner 1`] = `"<button disabled=\\"disabled\\" aria-disabled=\\"false\\" aria-busy=\\"true\\" aria-live=\\"polite\\" class=\\"button _button_10jsj_115 _primary_10jsj_308 _medium_10jsj_274 _loading_10jsj_375 _icon_10jsj_404\\"><span class=\\"_icon_10jsj_404\\"><n8n-spinner-stub size=\\"medium\\" type=\\"dots\\"></n8n-spinner-stub></span><span>Button</span></button>"`;
exports[`components > N8nButton > props > square > should render square button 1`] = ` exports[`components > N8nButton > props > square > should render square button 1`] = `
"<button aria-disabled=\\"false\\" aria-busy=\\"false\\" aria-live=\\"polite\\" class=\\"button _button_1qq65_115 _primary_1qq65_288 _medium_1qq65_254 _square_1qq65_239\\"> "<button aria-disabled=\\"false\\" aria-busy=\\"false\\" aria-live=\\"polite\\" class=\\"button _button_10jsj_115 _primary_10jsj_308 _medium_10jsj_274 _square_10jsj_259\\">
<!----><span>48</span></button>" <!----><span>48</span></button>"
`; `;
exports[`components > N8nButton > should render correctly 1`] = ` exports[`components > N8nButton > should render correctly 1`] = `
"<button aria-disabled=\\"false\\" aria-busy=\\"false\\" aria-live=\\"polite\\" class=\\"button _button_1qq65_115 _primary_1qq65_288 _medium_1qq65_254\\"> "<button aria-disabled=\\"false\\" aria-busy=\\"false\\" aria-live=\\"polite\\" class=\\"button _button_10jsj_115 _primary_10jsj_308 _medium_10jsj_274\\">
<!----><span>Button</span></button>" <!----><span>Button</span></button>"
`; `;

View file

@ -1,3 +1,3 @@
import ElButton from "./ElButton.vue"; import ElButton from './ElButton.vue';
export default ElButton; export default ElButton;

View file

@ -1,10 +1,6 @@
<template> <template>
<n8n-button <n8n-button ref="button" v-bind="attrs" v-on="$listeners">
ref="button" <slot />
v-bind="attrs"
v-on="$listeners"
>
<slot/>
</n8n-button> </n8n-button>
</template> </template>

View file

@ -1,8 +1,7 @@
import N8nCallout from './Callout.vue'; import N8nCallout from './Callout.vue';
import N8nLink from '../N8nLink'; import N8nLink from '../N8nLink';
import N8nText from '../N8nText'; import N8nText from '../N8nText';
import { StoryFn } from '@storybook/vue'; import type { StoryFn } from '@storybook/vue';
export default { export default {
title: 'Atoms/Callout', title: 'Atoms/Callout',
@ -33,7 +32,15 @@ export default {
}, },
}; };
const template : StoryFn = (args, { argTypes }) => ({ interface Args {
theme: string;
icon: string;
default: string;
actions: string;
trailingContent: string;
}
const template: StoryFn<Args> = (args, { argTypes }) => ({
props: Object.keys(argTypes), props: Object.keys(argTypes),
components: { components: {
N8nLink, N8nLink,
@ -79,7 +86,6 @@ customCallout.args = {
`, `,
}; };
export const secondaryCallout = template.bind({}); export const secondaryCallout = template.bind({});
secondaryCallout.args = { secondaryCallout.args = {
theme: 'secondary', theme: 'secondary',

View file

@ -1,12 +1,8 @@
<template> <template>
<div :class="classes" role="alert"> <div :class="classes" role="alert">
<div :class="$style['message-section']"> <div :class="$style['message-section']">
<div :class="$style.icon"> <div :class="$style.icon">
<n8n-icon <n8n-icon :icon="getIcon" :size="theme === 'secondary' ? 'medium' : 'large'" />
:icon="getIcon"
:size="theme === 'secondary' ? 'medium' : 'large'"
/>
</div> </div>
<slot />&nbsp; <slot />&nbsp;
<slot name="actions" /> <slot name="actions" />
@ -46,11 +42,7 @@ export default Vue.extend({
}, },
computed: { computed: {
classes(): string[] { classes(): string[] {
return [ return ['n8n-callout', this.$style.callout, this.$style[this.theme]];
'n8n-callout',
this.$style.callout,
this.$style[this.theme],
];
}, },
getIcon(): string { getIcon(): string {
if (Object.keys(CALLOUT_DEFAULT_ICONS).includes(this.theme)) { if (Object.keys(CALLOUT_DEFAULT_ICONS).includes(this.theme)) {
@ -79,7 +71,8 @@ export default Vue.extend({
display: flex; display: flex;
} }
.info, .custom { .info,
.custom {
border-color: var(--color-foreground-base); border-color: var(--color-foreground-base);
background-color: var(--color-background-light); background-color: var(--color-background-light);
color: var(--color-info); color: var(--color-info);

View file

@ -8,7 +8,7 @@ describe('components', () => {
props: { props: {
theme: 'info', theme: 'info',
}, },
stubs: [ 'n8n-icon', 'n8n-text' ], stubs: ['n8n-icon', 'n8n-text'],
slots: { slots: {
default: '<n8n-text size="small">This is an info callout.</n8n-text>', default: '<n8n-text size="small">This is an info callout.</n8n-text>',
}, },
@ -20,7 +20,7 @@ describe('components', () => {
props: { props: {
theme: 'success', theme: 'success',
}, },
stubs: [ 'n8n-icon', 'n8n-text' ], stubs: ['n8n-icon', 'n8n-text'],
slots: { slots: {
default: '<n8n-text size="small">This is a success callout.</n8n-text>', default: '<n8n-text size="small">This is a success callout.</n8n-text>',
}, },
@ -32,7 +32,7 @@ describe('components', () => {
props: { props: {
theme: 'warning', theme: 'warning',
}, },
stubs: [ 'n8n-icon', 'n8n-text' ], stubs: ['n8n-icon', 'n8n-text'],
slots: { slots: {
default: '<n8n-text size="small">This is a warning callout.</n8n-text>', default: '<n8n-text size="small">This is a warning callout.</n8n-text>',
}, },
@ -44,7 +44,7 @@ describe('components', () => {
props: { props: {
theme: 'danger', theme: 'danger',
}, },
stubs: [ 'n8n-icon', 'n8n-text' ], stubs: ['n8n-icon', 'n8n-text'],
slots: { slots: {
default: '<n8n-text size="small">This is a danger callout.</n8n-text>', default: '<n8n-text size="small">This is a danger callout.</n8n-text>',
}, },
@ -56,7 +56,7 @@ describe('components', () => {
props: { props: {
theme: 'secondary', theme: 'secondary',
}, },
stubs: [ 'n8n-icon', 'n8n-text' ], stubs: ['n8n-icon', 'n8n-text'],
slots: { slots: {
default: '<n8n-text size="small">This is a secondary callout.</n8n-text>', default: '<n8n-text size="small">This is a secondary callout.</n8n-text>',
}, },
@ -69,7 +69,7 @@ describe('components', () => {
theme: 'custom', theme: 'custom',
icon: 'code-branch', icon: 'code-branch',
}, },
stubs: [ 'n8n-icon', 'n8n-text' ], stubs: ['n8n-icon', 'n8n-text'],
slots: { slots: {
default: '<n8n-text size="small">This is a secondary callout.</n8n-text>', default: '<n8n-text size="small">This is a secondary callout.</n8n-text>',
}, },
@ -82,11 +82,12 @@ describe('components', () => {
theme: 'custom', theme: 'custom',
icon: 'code-branch', icon: 'code-branch',
}, },
stubs: [ 'n8n-icon', 'n8n-text', 'n8n-link' ], stubs: ['n8n-icon', 'n8n-text', 'n8n-link'],
slots: { slots: {
default: '<n8n-text size="small">This is a secondary callout.</n8n-text>', default: '<n8n-text size="small">This is a secondary callout.</n8n-text>',
actions: '<n8n-link size="small">Do something!</n8n-link>', actions: '<n8n-link size="small">Do something!</n8n-link>',
trailingContent: '<n8n-link theme="secondary" size="small" :bold="true" :underline="true" to="https://n8n.io">Learn more</n8n-link>', trailingContent:
'<n8n-link theme="secondary" size="small" :bold="true" :underline="true" to="https://n8n.io">Learn more</n8n-link>',
}, },
}); });
expect(wrapper.html()).toMatchSnapshot(); expect(wrapper.html()).toMatchSnapshot();

View file

@ -1,9 +1,9 @@
// Vitest Snapshot v1 // Vitest Snapshot v1
exports[`components > N8nCallout > should render additional slots correctly 1`] = ` exports[`components > N8nCallout > should render additional slots correctly 1`] = `
"<div role=\\"alert\\" class=\\"n8n-callout _callout_p74de_1 _custom_p74de_16\\"> "<div role=\\"alert\\" class=\\"n8n-callout _callout_dfd91_1 _custom_dfd91_17\\">
<div class=\\"_message-section_p74de_12\\"> <div class=\\"_message-section_dfd91_12\\">
<div class=\\"_icon_p74de_40\\"> <div class=\\"_icon_dfd91_41\\">
<n8n-icon-stub icon=\\"code-branch\\" size=\\"large\\"></n8n-icon-stub> <n8n-icon-stub icon=\\"code-branch\\" size=\\"large\\"></n8n-icon-stub>
</div> </div>
<n8n-text-stub size=\\"small\\">This is a secondary callout.</n8n-text-stub>&nbsp; <n8n-link-stub size=\\"small\\">Do something!</n8n-link-stub> <n8n-text-stub size=\\"small\\">This is a secondary callout.</n8n-text-stub>&nbsp; <n8n-link-stub size=\\"small\\">Do something!</n8n-link-stub>
@ -13,9 +13,9 @@ exports[`components > N8nCallout > should render additional slots correctly 1`]
`; `;
exports[`components > N8nCallout > should render custom theme correctly 1`] = ` exports[`components > N8nCallout > should render custom theme correctly 1`] = `
"<div role=\\"alert\\" class=\\"n8n-callout _callout_p74de_1 _custom_p74de_16\\"> "<div role=\\"alert\\" class=\\"n8n-callout _callout_dfd91_1 _custom_dfd91_17\\">
<div class=\\"_message-section_p74de_12\\"> <div class=\\"_message-section_dfd91_12\\">
<div class=\\"_icon_p74de_40\\"> <div class=\\"_icon_dfd91_41\\">
<n8n-icon-stub icon=\\"code-branch\\" size=\\"large\\"></n8n-icon-stub> <n8n-icon-stub icon=\\"code-branch\\" size=\\"large\\"></n8n-icon-stub>
</div> </div>
<n8n-text-stub size=\\"small\\">This is a secondary callout.</n8n-text-stub>&nbsp; <n8n-text-stub size=\\"small\\">This is a secondary callout.</n8n-text-stub>&nbsp;
@ -24,9 +24,9 @@ exports[`components > N8nCallout > should render custom theme correctly 1`] = `
`; `;
exports[`components > N8nCallout > should render danger theme correctly 1`] = ` exports[`components > N8nCallout > should render danger theme correctly 1`] = `
"<div role=\\"alert\\" class=\\"n8n-callout _callout_p74de_1 _danger_p74de_34\\"> "<div role=\\"alert\\" class=\\"n8n-callout _callout_dfd91_1 _danger_dfd91_35\\">
<div class=\\"_message-section_p74de_12\\"> <div class=\\"_message-section_dfd91_12\\">
<div class=\\"_icon_p74de_40\\"> <div class=\\"_icon_dfd91_41\\">
<n8n-icon-stub icon=\\"times-circle\\" size=\\"large\\"></n8n-icon-stub> <n8n-icon-stub icon=\\"times-circle\\" size=\\"large\\"></n8n-icon-stub>
</div> </div>
<n8n-text-stub size=\\"small\\">This is a danger callout.</n8n-text-stub>&nbsp; <n8n-text-stub size=\\"small\\">This is a danger callout.</n8n-text-stub>&nbsp;
@ -35,9 +35,9 @@ exports[`components > N8nCallout > should render danger theme correctly 1`] = `
`; `;
exports[`components > N8nCallout > should render info theme correctly 1`] = ` exports[`components > N8nCallout > should render info theme correctly 1`] = `
"<div role=\\"alert\\" class=\\"n8n-callout _callout_p74de_1 _info_p74de_16\\"> "<div role=\\"alert\\" class=\\"n8n-callout _callout_dfd91_1 _info_dfd91_16\\">
<div class=\\"_message-section_p74de_12\\"> <div class=\\"_message-section_dfd91_12\\">
<div class=\\"_icon_p74de_40\\"> <div class=\\"_icon_dfd91_41\\">
<n8n-icon-stub icon=\\"info-circle\\" size=\\"large\\"></n8n-icon-stub> <n8n-icon-stub icon=\\"info-circle\\" size=\\"large\\"></n8n-icon-stub>
</div> </div>
<n8n-text-stub size=\\"small\\">This is an info callout.</n8n-text-stub>&nbsp; <n8n-text-stub size=\\"small\\">This is an info callout.</n8n-text-stub>&nbsp;
@ -46,9 +46,9 @@ exports[`components > N8nCallout > should render info theme correctly 1`] = `
`; `;
exports[`components > N8nCallout > should render secondary theme correctly 1`] = ` exports[`components > N8nCallout > should render secondary theme correctly 1`] = `
"<div role=\\"alert\\" class=\\"n8n-callout _callout_p74de_1 _secondary_p74de_44\\"> "<div role=\\"alert\\" class=\\"n8n-callout _callout_dfd91_1 _secondary_dfd91_45\\">
<div class=\\"_message-section_p74de_12\\"> <div class=\\"_message-section_dfd91_12\\">
<div class=\\"_icon_p74de_40\\"> <div class=\\"_icon_dfd91_41\\">
<n8n-icon-stub icon=\\"info-circle\\" size=\\"medium\\"></n8n-icon-stub> <n8n-icon-stub icon=\\"info-circle\\" size=\\"medium\\"></n8n-icon-stub>
</div> </div>
<n8n-text-stub size=\\"small\\">This is a secondary callout.</n8n-text-stub>&nbsp; <n8n-text-stub size=\\"small\\">This is a secondary callout.</n8n-text-stub>&nbsp;
@ -57,9 +57,9 @@ exports[`components > N8nCallout > should render secondary theme correctly 1`] =
`; `;
exports[`components > N8nCallout > should render success theme correctly 1`] = ` exports[`components > N8nCallout > should render success theme correctly 1`] = `
"<div role=\\"alert\\" class=\\"n8n-callout _callout_p74de_1 _success_p74de_28\\"> "<div role=\\"alert\\" class=\\"n8n-callout _callout_dfd91_1 _success_dfd91_29\\">
<div class=\\"_message-section_p74de_12\\"> <div class=\\"_message-section_dfd91_12\\">
<div class=\\"_icon_p74de_40\\"> <div class=\\"_icon_dfd91_41\\">
<n8n-icon-stub icon=\\"check-circle\\" size=\\"large\\"></n8n-icon-stub> <n8n-icon-stub icon=\\"check-circle\\" size=\\"large\\"></n8n-icon-stub>
</div> </div>
<n8n-text-stub size=\\"small\\">This is a success callout.</n8n-text-stub>&nbsp; <n8n-text-stub size=\\"small\\">This is a success callout.</n8n-text-stub>&nbsp;
@ -68,9 +68,9 @@ exports[`components > N8nCallout > should render success theme correctly 1`] = `
`; `;
exports[`components > N8nCallout > should render warning theme correctly 1`] = ` exports[`components > N8nCallout > should render warning theme correctly 1`] = `
"<div role=\\"alert\\" class=\\"n8n-callout _callout_p74de_1 _warning_p74de_22\\"> "<div role=\\"alert\\" class=\\"n8n-callout _callout_dfd91_1 _warning_dfd91_23\\">
<div class=\\"_message-section_p74de_12\\"> <div class=\\"_message-section_dfd91_12\\">
<div class=\\"_icon_p74de_40\\"> <div class=\\"_icon_dfd91_41\\">
<n8n-icon-stub icon=\\"exclamation-triangle\\" size=\\"large\\"></n8n-icon-stub> <n8n-icon-stub icon=\\"exclamation-triangle\\" size=\\"large\\"></n8n-icon-stub>
</div> </div>
<n8n-text-stub size=\\"small\\">This is a warning callout.</n8n-text-stub>&nbsp; <n8n-text-stub size=\\"small\\">This is a warning callout.</n8n-text-stub>&nbsp;

View file

@ -1,17 +1,15 @@
/* tslint:disable:variable-name */
import N8nCard from './Card.vue'; import N8nCard from './Card.vue';
import {StoryFn} from "@storybook/vue"; import type { StoryFn } from '@storybook/vue';
import N8nButton from "../N8nButton/Button.vue"; import N8nButton from '../N8nButton/Button.vue';
import N8nIcon from "../N8nIcon/Icon.vue"; import N8nIcon from '../N8nIcon/Icon.vue';
import N8nText from "../N8nText/Text.vue"; import N8nText from '../N8nText/Text.vue';
export default { export default {
title: 'Atoms/Card', title: 'Atoms/Card',
component: N8nCard, component: N8nCard,
}; };
export const Default: StoryFn = (args, {argTypes}) => ({ export const Default: StoryFn = (args, { argTypes }) => ({
props: Object.keys(argTypes), props: Object.keys(argTypes),
components: { components: {
N8nCard, N8nCard,
@ -19,7 +17,7 @@ export const Default: StoryFn = (args, {argTypes}) => ({
template: `<n8n-card v-bind="$props">This is a card.</n8n-card>`, template: `<n8n-card v-bind="$props">This is a card.</n8n-card>`,
}); });
export const Hoverable: StoryFn = (args, {argTypes}) => ({ export const Hoverable: StoryFn = (args, { argTypes }) => ({
props: Object.keys(argTypes), props: Object.keys(argTypes),
components: { components: {
N8nCard, N8nCard,
@ -38,8 +36,7 @@ Hoverable.args = {
hoverable: true, hoverable: true,
}; };
export const WithSlots: StoryFn = (args, { argTypes }) => ({
export const WithSlots: StoryFn = (args, {argTypes}) => ({
props: Object.keys(argTypes), props: Object.keys(argTypes),
components: { components: {
N8nCard, N8nCard,

View file

@ -1,21 +1,21 @@
<template> <template>
<div :class="classes" v-on="$listeners"> <div :class="classes" v-on="$listeners">
<div :class="$style.icon" v-if="$slots.prepend"> <div :class="$style.icon" v-if="$slots.prepend">
<slot name="prepend"/> <slot name="prepend" />
</div> </div>
<div :class="$style.content"> <div :class="$style.content">
<div :class="$style.header" v-if="$slots.header"> <div :class="$style.header" v-if="$slots.header">
<slot name="header"/> <slot name="header" />
</div> </div>
<div :class="$style.body" v-if="$slots.default"> <div :class="$style.body" v-if="$slots.default">
<slot/> <slot />
</div> </div>
<div :class="$style.footer" v-if="$slots.footer"> <div :class="$style.footer" v-if="$slots.footer">
<slot name="footer"/> <slot name="footer" />
</div> </div>
</div> </div>
<div :class="$style.actions" v-if="$slots.append"> <div :class="$style.actions" v-if="$slots.append">
<slot name="append"/> <slot name="append" />
</div> </div>
</div> </div>
</template> </template>

View file

@ -1,5 +1,5 @@
import {render} from '@testing-library/vue'; import { render } from '@testing-library/vue';
import N8nCard from "../Card.vue"; import N8nCard from '../Card.vue';
describe('components', () => { describe('components', () => {
describe('N8nCard', () => { describe('N8nCard', () => {

View file

@ -1,6 +1,5 @@
/* tslint:disable:variable-name */ import N8nCheckbox from './Checkbox.vue';
import N8nCheckbox from "./Checkbox.vue"; import type { StoryFn } from '@storybook/vue';
import { StoryFn } from '@storybook/vue';
import { action } from '@storybook/addon-actions'; import { action } from '@storybook/addon-actions';
export default { export default {

View file

@ -51,20 +51,18 @@ export default Vue.extend({
labelSize: { labelSize: {
type: String, type: String,
default: 'medium', default: 'medium',
validator: (value: string): boolean => validator: (value: string): boolean => ['small', 'medium'].includes(value),
['small', 'medium'].includes(value),
}, },
}, },
methods: { methods: {
onChange(event: Event) { onChange(event: Event) {
this.$emit("input", event); this.$emit('input', event);
}, },
}, },
}); });
</script> </script>
<style lang="scss" module> <style lang="scss" module>
.n8nCheckbox { .n8nCheckbox {
display: flex !important; display: flex !important;
white-space: normal !important; white-space: normal !important;
@ -73,5 +71,4 @@ export default Vue.extend({
white-space: normal; white-space: normal;
} }
} }
</style> </style>

View file

@ -4,8 +4,7 @@ import { action } from '@storybook/addon-actions';
export default { export default {
title: 'Modules/FormBox', title: 'Modules/FormBox',
component: N8nFormBox, component: N8nFormBox,
argTypes: { argTypes: {},
},
parameters: { parameters: {
backgrounds: { default: '--color-background-light' }, backgrounds: { default: '--color-background-light' },
}, },
@ -35,7 +34,7 @@ FormBox.args = {
label: 'Your Email', label: 'Your Email',
type: 'email', type: 'email',
required: true, required: true,
validationRules: [{name: 'VALID_EMAIL'}], validationRules: [{ name: 'VALID_EMAIL' }],
}, },
}, },
{ {
@ -51,7 +50,7 @@ FormBox.args = {
label: 'Your Password', label: 'Your Password',
type: 'password', type: 'password',
required: true, required: true,
validationRules: [{name: 'DEFAULT_PASSWORD_RULES'}], validationRules: [{ name: 'DEFAULT_PASSWORD_RULES' }],
}, },
}, },
{ {
@ -66,4 +65,3 @@ FormBox.args = {
redirectText: 'Go somewhere', redirectText: 'Go somewhere',
redirectLink: 'https://n8n.io', redirectLink: 'https://n8n.io',
}; };

View file

@ -1,20 +1,11 @@
<template> <template>
<div <div :class="['n8n-form-box', $style.container]">
:class="['n8n-form-box', $style.container]" <div v-if="title" :class="$style.heading">
> <n8n-heading size="xlarge">
<div {{ title }}
v-if="title"
:class="$style.heading"
>
<n8n-heading
size="xlarge"
>
{{title}}
</n8n-heading> </n8n-heading>
</div> </div>
<div <div :class="$style.inputsContainer">
:class="$style.inputsContainer"
>
<n8n-form-inputs <n8n-form-inputs
:inputs="inputs" :inputs="inputs"
:eventBus="formBus" :eventBus="formBus"
@ -24,16 +15,9 @@
/> />
</div> </div>
<div :class="$style.buttonsContainer" v-if="secondaryButtonText || buttonText"> <div :class="$style.buttonsContainer" v-if="secondaryButtonText || buttonText">
<span <span v-if="secondaryButtonText" :class="$style.secondaryButtonContainer">
v-if="secondaryButtonText" <n8n-link size="medium" theme="text" @click="onSecondaryButtonClick">
:class="$style.secondaryButtonContainer" {{ secondaryButtonText }}
>
<n8n-link
size="medium"
theme="text"
@click="onSecondaryButtonClick"
>
{{secondaryButtonText}}
</n8n-link> </n8n-link>
</span> </span>
<n8n-button <n8n-button
@ -45,11 +29,8 @@
/> />
</div> </div>
<div :class="$style.actionContainer"> <div :class="$style.actionContainer">
<n8n-link <n8n-link v-if="redirectText && redirectLink" :to="redirectLink">
v-if="redirectText && redirectLink" {{ redirectText }}
:to="redirectLink"
>
{{redirectText}}
</n8n-link> </n8n-link>
</div> </div>
</div> </div>
@ -103,10 +84,10 @@ export default Vue.extend({
}; };
}, },
methods: { methods: {
onInput(e: {name: string, value: string}) { onInput(e: { name: string; value: string }) {
this.$emit('input', e); this.$emit('input', e);
}, },
onSubmit(e: {[key: string]: string}) { onSubmit(e: { [key: string]: string }) {
this.$emit('submit', e); this.$emit('submit', e);
}, },
onButtonClick() { onButtonClick() {

View file

@ -4,8 +4,7 @@ import { action } from '@storybook/addon-actions';
export default { export default {
title: 'Modules/FormInput', title: 'Modules/FormInput',
component: N8nFormInput, component: N8nFormInput,
argTypes: { argTypes: {},
},
}; };
const methods = { const methods = {

View file

@ -6,7 +6,13 @@
@focus="onFocus" @focus="onFocus"
ref="inputRef" ref="inputRef"
/> />
<n8n-input-label v-else :inputName="name" :label="label" :tooltipText="tooltipText" :required="required && showRequiredAsterisk"> <n8n-input-label
v-else
:inputName="name"
:label="label"
:tooltipText="tooltipText"
:required="required && showRequiredAsterisk"
>
<div :class="showErrors ? $style.errorInput : ''" @keydown.stop @keydown.enter="onEnter"> <div :class="showErrors ? $style.errorInput : ''" @keydown.stop @keydown.enter="onEnter">
<slot v-if="hasDefaultSlot" /> <slot v-if="hasDefaultSlot" />
<n8n-select <n8n-select
@ -21,7 +27,7 @@
ref="inputRef" ref="inputRef"
> >
<n8n-option <n8n-option
v-for="option in (options || [])" v-for="option in options || []"
:key="option.value" :key="option.value"
:value="option.value" :value="option.value"
:label="option.label" :label="option.label"
@ -69,7 +75,7 @@ import N8nInputLabel from '../N8nInputLabel';
import N8nCheckbox from '../N8nCheckbox'; import N8nCheckbox from '../N8nCheckbox';
import { getValidationError, VALIDATORS } from './validators'; import { getValidationError, VALIDATORS } from './validators';
import { Rule, RuleGroup, IValidator } from "../../types"; import { Rule, RuleGroup, IValidator } from '../../types';
import { t } from '../../locale'; import { t } from '../../locale';
@ -87,9 +93,9 @@ export interface Props {
documentationUrl?: string; documentationUrl?: string;
documentationText?: string; documentationText?: string;
validationRules?: Array<Rule | RuleGroup>; validationRules?: Array<Rule | RuleGroup>;
validators?: {[key: string]: IValidator | RuleGroup}; validators?: { [key: string]: IValidator | RuleGroup };
maxlength?: number; maxlength?: number;
options?: Array<{value: string | number, label: string}>; options?: Array<{ value: string | number; label: string }>;
autocomplete?: string; autocomplete?: string;
name?: string; name?: string;
focusInitially?: boolean; focusInitially?: boolean;
@ -105,11 +111,11 @@ const props = withDefaults(defineProps<Props>(), {
}); });
const emit = defineEmits<{ const emit = defineEmits<{
(event: 'validate', shouldValidate: boolean): void, (event: 'validate', shouldValidate: boolean): void;
(event: 'input', value: any): void, (event: 'input', value: any): void;
(event: 'focus'): void, (event: 'focus'): void;
(event: 'blur'): void, (event: 'blur'): void;
(event: 'enter'): void, (event: 'enter'): void;
}>(); }>();
const state = reactive({ const state = reactive({
@ -122,10 +128,10 @@ 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']> {
const rules = props.validationRules || []; const rules = props.validationRules ?? [];
const validators = { const validators = {
...VALIDATORS, ...VALIDATORS,
...(props.validators || {}), ...(props.validators ?? {}),
} as { [key: string]: IValidator | RuleGroup }; } as { [key: string]: IValidator | RuleGroup };
if (props.required) { if (props.required) {
@ -149,11 +155,7 @@ function getInputValidationError(): ReturnType<IValidator['validate']> {
if (rules[i].hasOwnProperty('rules')) { if (rules[i].hasOwnProperty('rules')) {
const rule = rules[i] as RuleGroup; const rule = rules[i] as RuleGroup;
const error = getValidationError( const error = getValidationError(props.value, validators, rule);
props.value,
validators,
rule,
);
if (error) return error; if (error) return error;
} }
} }
@ -190,10 +192,11 @@ const validationError = computed<string | null>(() => {
const hasDefaultSlot = computed(() => !!slots.default); const hasDefaultSlot = computed(() => !!slots.default);
const showErrors = computed(() => ( const showErrors = computed(
() =>
!!validationError.value && !!validationError.value &&
((props.validateOnBlur && state.hasBlurred && !state.isTyping) || props.showValidationWarnings) ((props.validateOnBlur && state.hasBlurred && !state.isTyping) || props.showValidationWarnings),
)); );
onMounted(() => { onMounted(() => {
emit('validate', !validationError.value); emit('validate', !validationError.value);
@ -201,7 +204,10 @@ onMounted(() => {
if (props.focusInitially && inputRef.value) inputRef.value.focus(); if (props.focusInitially && inputRef.value) inputRef.value.focus();
}); });
watch(() => validationError.value, (error) => emit('validate', !error)); watch(
() => validationError.value,
(error) => emit('validate', !error),
);
defineExpose({ inputRef }); defineExpose({ inputRef });
</script> </script>

View file

@ -1,12 +1,11 @@
import { IValidator, RuleGroup, Validatable } from '../../types';
import { IValidator, RuleGroup } from "../../types";
export const emailRegex = export const emailRegex =
/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
export const VALIDATORS: { [key: string]: IValidator | RuleGroup } = { export const VALIDATORS: { [key: string]: IValidator | RuleGroup } = {
REQUIRED: { REQUIRED: {
validate: (value: string | number | boolean | null | undefined) => { validate: (value: Validatable) => {
if (typeof value === 'string' && !!value.trim()) { if (typeof value === 'string' && !!value.trim()) {
return false; return false;
} }
@ -21,7 +20,7 @@ export const VALIDATORS: { [key: string]: IValidator | RuleGroup } = {
}, },
}, },
MIN_LENGTH: { MIN_LENGTH: {
validate: (value: string | number | boolean | null | undefined, config: { minimum: number }) => { validate: (value: Validatable, config: { minimum: number }) => {
if (typeof value === 'string' && value.length < config.minimum) { if (typeof value === 'string' && value.length < config.minimum) {
return { return {
messageKey: 'formInput.validator.minCharactersRequired', messageKey: 'formInput.validator.minCharactersRequired',
@ -33,7 +32,7 @@ export const VALIDATORS: { [key: string]: IValidator | RuleGroup } = {
}, },
}, },
MAX_LENGTH: { MAX_LENGTH: {
validate: (value: string | number | boolean | null | undefined, config: { maximum: number }) => { validate: (value: Validatable, config: { maximum: number }) => {
if (typeof value === 'string' && value.length > config.maximum) { if (typeof value === 'string' && value.length > config.maximum) {
return { return {
messageKey: 'formInput.validator.maxCharactersRequired', messageKey: 'formInput.validator.maxCharactersRequired',
@ -45,12 +44,12 @@ export const VALIDATORS: { [key: string]: IValidator | RuleGroup } = {
}, },
}, },
CONTAINS_NUMBER: { CONTAINS_NUMBER: {
validate: (value: string | number | boolean | null | undefined, config: { minimum: number }) => { validate: (value: Validatable, config: { minimum: number }) => {
if (typeof value !== 'string') { if (typeof value !== 'string') {
return false; return false;
} }
const numberCount = (value.match(/\d/g) || []).length; const numberCount = (value.match(/\d/g) ?? []).length;
if (numberCount < config.minimum) { if (numberCount < config.minimum) {
return { return {
messageKey: 'formInput.validator.numbersRequired', messageKey: 'formInput.validator.numbersRequired',
@ -62,7 +61,7 @@ export const VALIDATORS: { [key: string]: IValidator | RuleGroup } = {
}, },
}, },
VALID_EMAIL: { VALID_EMAIL: {
validate: (value: string | number | boolean | null | undefined) => { validate: (value: Validatable) => {
if (!emailRegex.test(String(value).trim().toLowerCase())) { if (!emailRegex.test(String(value).trim().toLowerCase())) {
return { return {
messageKey: 'formInput.validator.validEmailRequired', messageKey: 'formInput.validator.validEmailRequired',
@ -73,12 +72,12 @@ export const VALIDATORS: { [key: string]: IValidator | RuleGroup } = {
}, },
}, },
CONTAINS_UPPERCASE: { CONTAINS_UPPERCASE: {
validate: (value: string | number | boolean | null | undefined, config: { minimum: number }) => { validate: (value: Validatable, config: { minimum: number }) => {
if (typeof value !== 'string') { if (typeof value !== 'string') {
return false; return false;
} }
const uppercaseCount = (value.match(/[A-Z]/g) || []).length; const uppercaseCount = (value.match(/[A-Z]/g) ?? []).length;
if (uppercaseCount < config.minimum) { if (uppercaseCount < config.minimum) {
return { return {
messageKey: 'formInput.validator.uppercaseCharsRequired', messageKey: 'formInput.validator.uppercaseCharsRequired',
@ -101,35 +100,30 @@ export const VALIDATORS: { [key: string]: IValidator | RuleGroup } = {
messageKey: 'formInput.validator.defaultPasswordRequirements', messageKey: 'formInput.validator.defaultPasswordRequirements',
}, },
}, },
{ name: 'MAX_LENGTH', config: {maximum: 64} }, { name: 'MAX_LENGTH', config: { maximum: 64 } },
], ],
}, },
}; };
export const getValidationError = ( export const getValidationError = <T extends Validatable, C>(
value: any, // tslint:disable-line:no-any value: T,
validators: { [key: string]: IValidator | RuleGroup }, validators: { [key: string]: IValidator | RuleGroup },
validator: IValidator | RuleGroup, validator: IValidator | RuleGroup,
config?: any, // tslint:disable-line:no-any config?: C,
): ReturnType<IValidator['validate']> => { ): ReturnType<IValidator['validate']> => {
if (validator.hasOwnProperty('rules')) { if (validator.hasOwnProperty('rules')) {
const rules = (validator as RuleGroup).rules; const rules = (validator as RuleGroup).rules;
for (let i = 0; i < rules.length; i++) { for (let i = 0; i < rules.length; i++) {
if (rules[i].hasOwnProperty('rules')) { if (rules[i].hasOwnProperty('rules')) {
const error = getValidationError( const error = getValidationError(value, validators, rules[i] as RuleGroup, config);
value,
validators,
rules[i] as RuleGroup,
config,
);
if (error) { if (error) {
return error; return error;
} }
} }
if (rules[i].hasOwnProperty('name') ) { if (rules[i].hasOwnProperty('name')) {
const rule = rules[i] as {name: string, config?: any}; // tslint:disable-line:no-any const rule = rules[i] as { name: string; config?: C };
if (!validators[rule.name]) { if (!validators[rule.name]) {
continue; continue;
} }
@ -140,17 +134,14 @@ export const getValidationError = (
validators[rule.name] as IValidator, validators[rule.name] as IValidator,
rule.config, rule.config,
); );
if (error && (validator as RuleGroup).defaultError !== undefined) { if (error && 'defaultError' in validator && validator.defaultError) {
// @ts-ignore
return validator.defaultError; return validator.defaultError;
} else if (error) { } else if (error) {
return error; return error;
} }
} }
} }
} else if ( } else if (validator.hasOwnProperty('validate')) {
validator.hasOwnProperty('validate')
) {
return (validator as IValidator).validate(value, config); return (validator as IValidator).validate(value, config);
} }

View file

@ -72,11 +72,11 @@ FormInputs.args = {
name: 'agree', name: 'agree',
properties: { properties: {
type: 'checkbox', type: 'checkbox',
label: 'Signup for newsletter and somebody from our marketing team will get in touch with you as soon as possible. You will not spam you, just want to send you some love every now and then ❤️', label:
'Signup for newsletter and somebody from our marketing team will get in touch with you as soon as possible. You will not spam you, just want to send you some love every now and then ❤️',
labelSize: 'small', labelSize: 'small',
tooltipText: 'Check this if you agree to be contacted by our marketing team' tooltipText: 'Check this if you agree to be contacted by our marketing team',
} },
} },
], ],
}; };

View file

@ -1,15 +1,15 @@
<template> <template>
<ResizeObserver <ResizeObserver :breakpoints="[{ bp: 'md', width: 500 }]">
:breakpoints="[{bp: 'md', width: 500}]"
>
<template v-slot="{ bp }"> <template v-slot="{ bp }">
<div :class="bp === 'md' || columnView? $style.grid : $style.gridMulti"> <div :class="bp === 'md' || columnView ? $style.grid : $style.gridMulti">
<div <div v-for="input in filteredInputs" :key="input.name">
v-for="(input) in filteredInputs" <n8n-text
:key="input.name" color="text-base"
v-if="input.properties.type === 'info'"
tag="div"
align="center"
> >
<n8n-text color="text-base" v-if="input.properties.type === 'info'" tag="div" align="center"> {{ input.properties.label }}
{{input.properties.label}}
</n8n-text> </n8n-text>
<n8n-form-input <n8n-form-input
v-else v-else
@ -57,8 +57,8 @@ export default Vue.extend({
data() { data() {
return { return {
showValidationWarnings: false, showValidationWarnings: false,
values: {} as {[key: string]: any}, values: {} as { [key: string]: any },
validity: {} as {[key: string]: boolean}, validity: {} as { [key: string]: boolean },
}; };
}, },
mounted() { mounted() {
@ -74,10 +74,8 @@ export default Vue.extend({
}, },
computed: { computed: {
filteredInputs(): IFormInput[] { filteredInputs(): IFormInput[] {
return (this.inputs as IFormInput[]).filter( return (this.inputs as IFormInput[]).filter((input) =>
(input) => typeof input.shouldDisplay === 'function' typeof input.shouldDisplay === 'function' ? input.shouldDisplay(this.values) : true,
? input.shouldDisplay(this.values)
: true,
); );
}, },
isReadyToSubmit(): boolean { isReadyToSubmit(): boolean {
@ -96,7 +94,7 @@ export default Vue.extend({
...this.values, ...this.values,
[name]: value, // eslint-disable-line @typescript-eslint/no-unsafe-assignment [name]: value, // eslint-disable-line @typescript-eslint/no-unsafe-assignment
}; };
this.$emit('input', {name, value}); // eslint-disable-line @typescript-eslint/no-unsafe-assignment this.$emit('input', { name, value }); // eslint-disable-line @typescript-eslint/no-unsafe-assignment
}, },
onValidate(name: string, valid: boolean) { onValidate(name: string, valid: boolean) {
Vue.set(this.validity, name, valid); Vue.set(this.validity, name, valid);
@ -104,7 +102,7 @@ export default Vue.extend({
onSubmit() { onSubmit() {
this.showValidationWarnings = true; this.showValidationWarnings = true;
if (this.isReadyToSubmit) { if (this.isReadyToSubmit) {
const toSubmit = (this.filteredInputs ).reduce<{ [key: string]: unknown }>((accu, input) => { const toSubmit = this.filteredInputs.reduce<{ [key: string]: unknown }>((accu, input) => {
if (this.values[input.name]) { if (this.values[input.name]) {
accu[input.name] = this.values[input.name]; accu[input.name] = this.values[input.name];
} }
@ -133,5 +131,4 @@ export default Vue.extend({
composes: grid; composes: grid;
grid-template-columns: repeat(2, 1fr); grid-template-columns: repeat(2, 1fr);
} }
</style> </style>

View file

@ -21,11 +21,15 @@ export default Vue.extend({
size: { size: {
type: String, type: String,
default: 'medium', default: 'medium',
validator: (value: string): boolean => ['2xlarge', 'xlarge', 'large', 'medium', 'small'].includes(value), validator: (value: string): boolean =>
['2xlarge', 'xlarge', 'large', 'medium', 'small'].includes(value),
}, },
color: { color: {
type: String, type: String,
validator: (value: string): boolean => ['primary', 'text-dark', 'text-base', 'text-light', 'text-xlight', 'danger'].includes(value), validator: (value: string): boolean =>
['primary', 'text-dark', 'text-base', 'text-light', 'text-xlight', 'danger'].includes(
value,
),
}, },
align: { align: {
type: String, type: String,
@ -36,15 +40,17 @@ export default Vue.extend({
classes() { classes() {
const applied = []; const applied = [];
if (this.align) { if (this.align) {
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
applied.push(`align-${this.align}`); applied.push(`align-${this.align}`);
} }
if (this.color) { if (this.color) {
applied.push(this.color); applied.push(this.color);
} }
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
applied.push(`size-${this.size}`); applied.push(`size-${this.size}`);
applied.push(this.bold? 'bold': 'regular'); applied.push(this.bold ? 'bold' : 'regular');
return applied.map((c) => (this.$style as { [key: string]: string })[c]); return applied.map((c) => (this.$style as { [key: string]: string })[c]);
}, },
@ -121,5 +127,4 @@ export default Vue.extend({
.align-center { .align-center {
text-align: center; text-align: center;
} }
</style> </style>

View file

@ -1,15 +1,6 @@
<template> <template>
<n8n-text <n8n-text :size="size" :color="color" :compact="true" class="n8n-icon">
:size="size" <font-awesome-icon :icon="icon" :spin="spin" :class="$style[size]" />
:color="color"
:compact="true"
class="n8n-icon"
>
<font-awesome-icon
:icon="icon"
:spin="spin"
:class="$style[size]"
/>
</n8n-text> </n8n-text>
</template> </template>
@ -44,7 +35,6 @@ export default Vue.extend({
}); });
</script> </script>
<style lang="scss" module> <style lang="scss" module>
.xlarge { .xlarge {
width: var(--font-size-xl) !important; width: var(--font-size-xl) !important;

View file

@ -1,7 +1,6 @@
/* tslint:disable:variable-name */
import N8nIconButton from './IconButton.vue'; import N8nIconButton from './IconButton.vue';
import { action } from '@storybook/addon-actions'; import { action } from '@storybook/addon-actions';
import { StoryFn } from "@storybook/vue"; import type { StoryFn } from '@storybook/vue';
export default { export default {
title: 'Atoms/Icon Button', title: 'Atoms/Icon Button',

View file

@ -1,9 +1,5 @@
<template> <template>
<n8n-button <n8n-button square v-bind="$props" v-on="$listeners" />
square
v-bind="$props"
v-on="$listeners"
/>
</template> </template>
<script lang="ts"> <script lang="ts">
@ -51,8 +47,7 @@ export default Vue.extend({
}, },
float: { float: {
type: String, type: String,
validator: (value: string): boolean => validator: (value: string): boolean => ['left', 'right'].includes(value),
['left', 'right'].includes(value),
}, },
}, },
}); });

View file

@ -1,14 +1,11 @@
/* tslint:disable:variable-name */
import N8nInfoAccordion from './InfoAccordion.vue'; import N8nInfoAccordion from './InfoAccordion.vue';
import { StoryFn } from "@storybook/vue"; import type { StoryFn } from '@storybook/vue';
import { action } from "@storybook/addon-actions"; import { action } from '@storybook/addon-actions';
export default { export default {
title: 'Atoms/Info Accordion', title: 'Atoms/Info Accordion',
component: N8nInfoAccordion, component: N8nInfoAccordion,
argTypes: { argTypes: {},
},
parameters: { parameters: {
backgrounds: { default: '--color-background-light' }, backgrounds: { default: '--color-background-light' },
}, },
@ -18,7 +15,7 @@ const methods = {
onClick: action('click'), onClick: action('click'),
}; };
export const Default: StoryFn = (args, {argTypes}) => ({ export const Default: StoryFn = (args, { argTypes }) => ({
props: Object.keys(argTypes), props: Object.keys(argTypes),
components: { components: {
N8nInfoAccordion, N8nInfoAccordion,

View file

@ -1,17 +1,33 @@
<template> <template>
<div :class="['accordion', $style.container]" > <div :class="['accordion', $style.container]">
<div :class="{[$style.header]: true, [$style.expanded]: expanded }" @click="toggle"> <div :class="{ [$style.header]: true, [$style.expanded]: expanded }" @click="toggle">
<n8n-icon v-if="headerIcon" :icon="headerIcon.icon" :color="headerIcon.color" size="small" class="mr-2xs"/> <n8n-icon
<n8n-text :class="$style.headerText" color="text-base" size="small" align="left" bold>{{ title }}</n8n-text> v-if="headerIcon"
<n8n-icon :icon="expanded? 'chevron-up' : 'chevron-down'" bold /> :icon="headerIcon.icon"
:color="headerIcon.color"
size="small"
class="mr-2xs"
/>
<n8n-text :class="$style.headerText" color="text-base" size="small" align="left" bold>{{
title
}}</n8n-text>
<n8n-icon :icon="expanded ? 'chevron-up' : 'chevron-down'" bold />
</div> </div>
<div v-if="expanded" :class="{[$style.description]: true, [$style.collapsed]: !expanded}" @click="onClick"> <div
v-if="expanded"
:class="{ [$style.description]: true, [$style.collapsed]: !expanded }"
@click="onClick"
>
<!-- 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"> <n8n-tooltip :disabled="!item.tooltip">
<div slot="content" v-html="item.tooltip" @click="onTooltipClick(item.id, $event)"></div> <div
<n8n-icon :icon="item.icon" :color="item.iconColor" size="small" class="mr-2xs"/> slot="content"
v-html="item.tooltip"
@click="onTooltipClick(item.id, $event)"
></div>
<n8n-icon :icon="item.icon" :color="item.iconColor" size="small" class="mr-2xs" />
</n8n-tooltip> </n8n-tooltip>
<n8n-text size="small" color="text-base">{{ item.label }}</n8n-text> <n8n-text size="small" color="text-base">{{ item.label }}</n8n-text>
</div> </div>
@ -59,7 +75,7 @@ export default Vue.extend({
default: false, default: false,
}, },
headerIcon: { headerIcon: {
type: Object as () => { icon: string, color: string }, type: Object as PropType<{ icon: string; color: string }>,
required: false, required: false,
}, },
}, },
@ -128,5 +144,4 @@ export default Vue.extend({
font-weight: var(--font-weight-bold); font-weight: var(--font-weight-bold);
} }
} }
</style> </style>

View file

@ -1,6 +1,4 @@
/* tslint:disable:variable-name */ import type { StoryFn } from '@storybook/vue';
import { StoryFn } from '@storybook/vue';
import N8nInfoTip from './InfoTip.vue'; import N8nInfoTip from './InfoTip.vue';
export default { export default {

View file

@ -1,5 +1,12 @@
<template> <template>
<div :class="{'n8n-info-tip': true, [$style[theme]]: true, [$style[type]]: true, [$style.bold]: bold}"> <div
:class="{
'n8n-info-tip': true,
[$style[theme]]: true,
[$style[type]]: true,
[$style.bold]: bold,
}"
>
<n8n-tooltip <n8n-tooltip
v-if="type === 'tooltip'" v-if="type === 'tooltip'"
:placement="tooltipPlacement" :placement="tooltipPlacement"
@ -7,14 +14,14 @@
:disabled="type !== 'tooltip'" :disabled="type !== 'tooltip'"
> >
<span :class="$style.iconText"> <span :class="$style.iconText">
<n8n-icon :icon="theme.startsWith('info') ? 'info-circle': 'exclamation-triangle'" /> <n8n-icon :icon="theme.startsWith('info') ? 'info-circle' : 'exclamation-triangle'" />
</span> </span>
<span slot="content"> <span slot="content">
<slot /> <slot />
</span> </span>
</n8n-tooltip> </n8n-tooltip>
<span :class="$style.iconText" v-else> <span :class="$style.iconText" v-else>
<n8n-icon :icon="theme.startsWith('info') ? 'info-circle': 'exclamation-triangle'" /> <n8n-icon :icon="theme.startsWith('info') ? 'info-circle' : 'exclamation-triangle'" />
<span> <span>
<slot /> <slot />
</span> </span>
@ -44,8 +51,7 @@ export default Vue.extend({
type: { type: {
type: String, type: String,
default: 'note', default: 'note',
validator: (value: string): boolean => validator: (value: string): boolean => ['note', 'tooltip'].includes(value),
['note', 'tooltip'].includes(value),
}, },
bold: { bold: {
type: Boolean, type: Boolean,

View file

@ -1,11 +1,8 @@
import {render} from '@testing-library/vue'; import { render } from '@testing-library/vue';
import N8nInfoTip from "../InfoTip.vue"; import N8nInfoTip from '../InfoTip.vue';
const slots = { const slots = {
default: [ default: ['Need help doing something?', '<a href="/docs" target="_blank">Open docs</a>'],
'Need help doing something?',
'<a href="/docs" target="_blank">Open docs</a>',
],
}; };
const stubs = ['n8n-tooltip']; const stubs = ['n8n-tooltip'];

View file

@ -1,8 +1,7 @@
/* tslint:disable:variable-name */
import N8nInput from './Input.vue'; import N8nInput from './Input.vue';
import N8nIcon from '../N8nIcon'; import N8nIcon from '../N8nIcon';
import { action } from '@storybook/addon-actions'; import { action } from '@storybook/addon-actions';
import { StoryFn } from '@storybook/vue'; import type { StoryFn } from '@storybook/vue';
export default { export default {
title: 'Atoms/Input', title: 'Atoms/Input',
@ -41,7 +40,8 @@ const Template: StoryFn = (args, { argTypes }) => ({
components: { components: {
N8nInput, N8nInput,
}, },
template: '<n8n-input v-bind="$props" v-model="val" @input="onInput" @change="onChange" @focus="onFocus" />', template:
'<n8n-input v-bind="$props" v-model="val" @input="onInput" @change="onChange" @focus="onFocus" />',
data() { data() {
return { return {
val: '', val: '',
@ -82,14 +82,14 @@ TextArea.args = {
placeholder: 'placeholder...', placeholder: 'placeholder...',
}; };
const WithPrefix: StoryFn = (args, { argTypes }) => ({ const WithPrefix: StoryFn = (args, { argTypes }) => ({
props: Object.keys(argTypes), props: Object.keys(argTypes),
components: { components: {
N8nIcon, N8nIcon,
N8nInput, N8nInput,
}, },
template: '<n8n-input v-bind="$props" v-model="val" @input="onInput" @change="onChange" @focus="onFocus"><n8n-icon icon="clock" slot="prefix" /></n8n-input>', template:
'<n8n-input v-bind="$props" v-model="val" @input="onInput" @change="onChange" @focus="onFocus"><n8n-icon icon="clock" slot="prefix" /></n8n-input>',
data() { data() {
return { return {
val: '', val: '',
@ -109,7 +109,8 @@ const WithSuffix: StoryFn = (args, { argTypes }) => ({
N8nIcon, N8nIcon,
N8nInput, N8nInput,
}, },
template: '<n8n-input v-bind="$props" v-model="val" @input="onInput" @change="onChange" @focus="onFocus"><n8n-icon icon="clock" slot="suffix" /></n8n-input>', template:
'<n8n-input v-bind="$props" v-model="val" @input="onInput" @change="onChange" @focus="onFocus"><n8n-icon icon="clock" slot="suffix" /></n8n-input>',
data() { data() {
return { return {
val: '', val: '',

View file

@ -33,8 +33,7 @@ export default Vue.extend({
ElInput, // eslint-disable-line @typescript-eslint/no-unsafe-assignment ElInput, // eslint-disable-line @typescript-eslint/no-unsafe-assignment
}, },
props: { props: {
value: { value: {},
},
type: { type: {
type: String, type: String,
validator: (value: string): boolean => validator: (value: string): boolean =>

View file

@ -1,5 +1,5 @@
import {render} from '@testing-library/vue'; import { render } from '@testing-library/vue';
import N8nInput from "../Input.vue"; import N8nInput from '../Input.vue';
describe('N8nInput', () => { describe('N8nInput', () => {
it('should render correctly', () => { it('should render correctly', () => {

View file

@ -17,15 +17,21 @@
<n8n-text color="primary" :bold="bold" :size="size" v-if="required">*</n8n-text> <n8n-text color="primary" :bold="bold" :size="size" v-if="required">*</n8n-text>
</n8n-text> </n8n-text>
</div> </div>
<span :class="[$style.infoIcon, showTooltip ? $style.visible: $style.hidden]" v-if="tooltipText && label"> <span
:class="[$style.infoIcon, showTooltip ? $style.visible : $style.hidden]"
v-if="tooltipText && label"
>
<n8n-tooltip placement="top" :popper-class="$style.tooltipPopper"> <n8n-tooltip placement="top" :popper-class="$style.tooltipPopper">
<n8n-icon icon="question-circle" size="small" /> <n8n-icon icon="question-circle" size="small" />
<div slot="content" v-html="addTargetBlank(tooltipText)" /> <div slot="content" v-html="addTargetBlank(tooltipText)" />
</n8n-tooltip> </n8n-tooltip>
</span> </span>
<div v-if="$slots.options && label" :class="{[$style.overlay]: true, [$style.visible]: showOptions}" /> <div
<div v-if="$slots.options" :class="{[$style.options]: true, [$style.visible]: showOptions}"> v-if="$slots.options && label"
<slot name="options"/> :class="{ [$style.overlay]: true, [$style.visible]: showOptions }"
/>
<div v-if="$slots.options" :class="{ [$style.options]: true, [$style.visible]: showOptions }">
<slot name="options" />
</div> </div>
</label> </label>
<slot /> <slot />
@ -71,8 +77,7 @@ export default Vue.extend({
size: { size: {
type: String, type: String,
default: 'medium', default: 'medium',
validator: (value: string): boolean => validator: (value: string): boolean => ['small', 'medium'].includes(value),
['small', 'medium'].includes(value),
}, },
underline: { underline: {
type: Boolean, type: Boolean,
@ -98,7 +103,8 @@ export default Vue.extend({
.inputLabel { .inputLabel {
display: block; display: block;
} }
.container:hover,.inputLabel:hover { .container:hover,
.inputLabel:hover {
.infoIcon { .infoIcon {
opacity: 1; opacity: 1;
} }
@ -136,7 +142,7 @@ export default Vue.extend({
.options { .options {
opacity: 0; opacity: 0;
background-color: var(--color-background-xlight); background-color: var(--color-background-xlight);
transition: opacity 250ms cubic-bezier(.98,-0.06,.49,-0.2); // transition on hover out transition: opacity 250ms cubic-bezier(0.98, -0.06, 0.49, -0.2); // transition on hover out
> * { > * {
float: right; float: right;
@ -147,7 +153,7 @@ export default Vue.extend({
position: relative; position: relative;
flex-grow: 1; flex-grow: 1;
opacity: 0; opacity: 0;
transition: opacity 250ms cubic-bezier(.98,-0.06,.49,-0.2); // transition on hover out transition: opacity 250ms cubic-bezier(0.98, -0.06, 0.49, -0.2); // transition on hover out
> div { > div {
position: absolute; position: absolute;
@ -157,7 +163,11 @@ export default Vue.extend({
right: 0; right: 0;
z-index: 0; z-index: 0;
background: linear-gradient(270deg, var(--color-foreground-xlight) 72.19%, rgba(255, 255, 255, 0) 107.45%); background: linear-gradient(
270deg,
var(--color-foreground-xlight) 72.19%,
rgba(255, 255, 255, 0) 107.45%
);
} }
} }
@ -197,5 +207,4 @@ export default Vue.extend({
margin-left: var(--spacing-s); margin-left: var(--spacing-s);
} }
} }
</style> </style>

View file

@ -91,4 +91,3 @@ Sizes.args = {
placeholder: 'placeholder...', placeholder: 'placeholder...',
controls: false, controls: false,
}; };

View file

@ -1,11 +1,6 @@
<template> <template>
<n8n-route :to="to" :newWindow="newWindow" <n8n-route :to="to" :newWindow="newWindow" v-on="$listeners" class="n8n-link">
v-on="$listeners" <span :class="$style[`${underline ? `${theme}-underline` : theme}`]">
class="n8n-link"
>
<span
:class="$style[`${underline ? `${theme}-underline` : theme}`]"
>
<n8n-text :size="size" :bold="bold"> <n8n-text :size="size" :bold="bold">
<slot></slot> <slot></slot>
</n8n-text> </n8n-text>
@ -54,18 +49,13 @@ export default Vue.extend({
</script> </script>
<style lang="scss" module> <style lang="scss" module>
@import "../../utils"; @import '../../utils';
.primary { .primary {
color: var(--color-primary); color: var(--color-primary);
&:active { &:active {
color: saturation( color: saturation(--color-primary-h, --color-primary-s, --color-primary-l, -(30%));
--color-primary-h,
--color-primary-s,
--color-primary-l,
-(30%)
);
} }
} }
@ -121,5 +111,4 @@ export default Vue.extend({
composes: secondary; composes: secondary;
text-decoration: underline; text-decoration: underline;
} }
</style> </style>

View file

@ -1,5 +1,9 @@
<template> <template>
<el-skeleton :loading="loading" :animated="animated" :class="['n8n-loading', `n8n-loading-${variant}`]"> <el-skeleton
:loading="loading"
:animated="animated"
:class="['n8n-loading', `n8n-loading-${variant}`]"
>
<template slot="template"> <template slot="template">
<div v-if="variant === 'h1'"> <div v-if="variant === 'h1'">
<div <div
@ -9,9 +13,7 @@
[$style.h1Last]: item === rows && rows > 1 && shrinkLast, [$style.h1Last]: item === rows && rows > 1 && shrinkLast,
}" }"
> >
<el-skeleton-item <el-skeleton-item :variant="variant" />
:variant="variant"
/>
</div> </div>
</div> </div>
<div v-else-if="variant === 'p'"> <div v-else-if="variant === 'p'">
@ -20,21 +22,15 @@
:key="index" :key="index"
:class="{ :class="{
[$style.pLast]: item === rows && rows > 1 && shrinkLast, [$style.pLast]: item === rows && rows > 1 && shrinkLast,
}"> }"
<el-skeleton-item >
:variant="variant" <el-skeleton-item :variant="variant" />
/>
</div> </div>
</div> </div>
<div :class="$style.custom" v-else-if="variant === 'custom'"> <div :class="$style.custom" v-else-if="variant === 'custom'">
<el-skeleton-item <el-skeleton-item :variant="variant" />
:variant="variant"
/>
</div> </div>
<el-skeleton-item <el-skeleton-item v-else :variant="variant" />
v-else
:variant="variant"
/>
</template> </template>
</el-skeleton> </el-skeleton>
</template> </template>
@ -71,7 +67,20 @@ export default Vue.extend({
variant: { variant: {
type: String, type: String,
default: 'p', default: 'p',
validator: (value: string): boolean => ['custom', 'p', 'text', 'h1', 'h3', 'text', 'caption', 'button', 'image', 'circle', 'rect'].includes(value), validator: (value: string): boolean =>
[
'custom',
'p',
'text',
'h1',
'h3',
'text',
'caption',
'button',
'image',
'circle',
'rect',
].includes(value),
}, },
}, },
}); });

View file

@ -41,8 +41,10 @@ export const Markdown = Template.bind({});
Markdown.args = { Markdown.args = {
content: `I wanted a system to monitor website content changes and notify me. So I made it using n8n.\n\nEspecially my competitor blogs. I wanted to know how often they are posting new articles. (I used their sitemap.xml file) (The below workflow may vary)\n\nIn the Below example, I used HackerNews for example.\n\nExplanation:\n\n- First HTTP Request node crawls the webpage and grabs the website source code\n- Then wait for x minutes\n- Again, HTTP Node crawls the webpage\n- If Node compares both results are equal if anything is changed. Itll go to the false branch and notify me in telegram.\n\n**Workflow:**\n\n![](fileId:1)\n\n**Sample Response:**\n\n![](https://community.n8n.io/uploads/default/original/2X/d/d21ba41d7ac9ff5cd8148fedb07d0f1ff53b2529.png)\n`, content: `I wanted a system to monitor website content changes and notify me. So I made it using n8n.\n\nEspecially my competitor blogs. I wanted to know how often they are posting new articles. (I used their sitemap.xml file) (The below workflow may vary)\n\nIn the Below example, I used HackerNews for example.\n\nExplanation:\n\n- First HTTP Request node crawls the webpage and grabs the website source code\n- Then wait for x minutes\n- Again, HTTP Node crawls the webpage\n- If Node compares both results are equal if anything is changed. Itll go to the false branch and notify me in telegram.\n\n**Workflow:**\n\n![](fileId:1)\n\n**Sample Response:**\n\n![](https://community.n8n.io/uploads/default/original/2X/d/d21ba41d7ac9ff5cd8148fedb07d0f1ff53b2529.png)\n`,
loading: false, loading: false,
images: [{ images: [
{
id: 1, id: 1,
url: 'https://community.n8n.io/uploads/default/optimized/2X/b/b737a95de4dfe0825d50ca098171e9f33a459e74_2_690x288.png', url: 'https://community.n8n.io/uploads/default/optimized/2X/b/b737a95de4dfe0825d50ca098171e9f33a459e74_2_690x288.png',
}], },
],
}; };

View file

@ -4,18 +4,13 @@
v-if="!loading" v-if="!loading"
ref="editor" ref="editor"
class="ph-no-capture" class="ph-no-capture"
:class="$style[theme]" v-html="htmlContent" :class="$style[theme]"
v-html="htmlContent"
@click="onClick" @click="onClick"
/> />
<div v-else :class="$style.markdown"> <div v-else :class="$style.markdown">
<div v-for="(block, index) in loadingBlocks" <div v-for="(block, index) in loadingBlocks" :key="index">
:key="index"> <n8n-loading :loading="loading" :rows="loadingRows" animated variant="p" />
<n8n-loading
:loading="loading"
:rows="loadingRows"
animated
variant="p"
/>
<div :class="$style.spacer" /> <div :class="$style.spacer" />
</div> </div>
</div> </div>
@ -26,10 +21,9 @@
import N8nLoading from '../N8nLoading'; import N8nLoading from '../N8nLoading';
import Markdown from 'markdown-it'; import Markdown from 'markdown-it';
// @ts-ignore
import markdownLink from 'markdown-it-link-attributes'; import markdownLink from 'markdown-it-link-attributes';
// @ts-ignore
import markdownEmoji from 'markdown-it-emoji'; import markdownEmoji from 'markdown-it-emoji';
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore // @ts-ignore
import markdownTasklists from 'markdown-it-task-lists'; import markdownTasklists from 'markdown-it-task-lists';
@ -41,26 +35,32 @@ const DEFAULT_OPTIONS_MARKDOWN = {
linkify: true, linkify: true,
typographer: true, typographer: true,
breaks: true, breaks: true,
}; } as const;
const DEFAULT_OPTIONS_LINK_ATTRIBUTES = { const DEFAULT_OPTIONS_LINK_ATTRIBUTES = {
attrs: { attrs: {
target: '_blank', target: '_blank',
rel: 'noopener', rel: 'noopener',
}, },
}; } as const;
const DEFAULT_OPTIONS_TASKLISTS = { const DEFAULT_OPTIONS_TASKLISTS = {
label: true, label: true,
labelAfter: true, labelAfter: true,
}; } as const;
interface IImage { interface IImage {
id: string; id: string;
url: string; url: string;
} }
import Vue from 'vue'; interface Options {
markdown: typeof DEFAULT_OPTIONS_MARKDOWN;
linkAttributes: typeof DEFAULT_OPTIONS_LINK_ATTRIBUTES;
tasklists: typeof DEFAULT_OPTIONS_TASKLISTS;
}
import Vue, { PropType } from 'vue';
export default Vue.extend({ export default Vue.extend({
components: { components: {
@ -75,7 +75,7 @@ export default Vue.extend({
type: Boolean, type: Boolean,
}, },
images: { images: {
type: Array, type: Array<IImage>,
}, },
loading: { loading: {
type: Boolean, type: Boolean,
@ -86,16 +86,14 @@ export default Vue.extend({
}, },
loadingRows: { loadingRows: {
type: Number, type: Number,
default: () => { default: () => 3,
return 3;
},
}, },
theme: { theme: {
type: String, type: String,
default: 'markdown', default: 'markdown',
}, },
options: { options: {
type: Object, type: Object as PropType<Options>,
default() { default() {
return { return {
markdown: DEFAULT_OPTIONS_MARKDOWN, markdown: DEFAULT_OPTIONS_MARKDOWN,
@ -113,7 +111,6 @@ export default Vue.extend({
const imageUrls: { [key: string]: string } = {}; const imageUrls: { [key: string]: string } = {};
if (this.images) { if (this.images) {
// @ts-ignore
this.images.forEach((image: IImage) => { this.images.forEach((image: IImage) => {
if (!image) { if (!image) {
// Happens if an image got deleted but the workflow // Happens if an image got deleted but the workflow
@ -125,14 +122,13 @@ export default Vue.extend({
} }
const fileIdRegex = new RegExp('fileId:([0-9]+)'); const fileIdRegex = new RegExp('fileId:([0-9]+)');
const imageFilesRegex = /\.(jpeg|jpg|gif|png|webp|bmp|tif|tiff|apng|svg|avif)$/;
let contentToRender = this.content; let contentToRender = this.content;
if (this.withMultiBreaks) { if (this.withMultiBreaks) {
contentToRender = contentToRender.replaceAll('\n\n', '\n&nbsp;\n'); contentToRender = contentToRender.replaceAll('\n\n', '\n&nbsp;\n');
} }
const html = this.md.render(escapeMarkdown(contentToRender)); const html = this.md.render(escapeMarkdown(contentToRender));
const safeHtml = xss(html, { const safeHtml = xss(html, {
onTagAttr: (tag, name, value, isWhiteAttr) => { onTagAttr: (tag, name, value) => {
if (tag === 'img' && name === 'src') { if (tag === 'img' && name === 'src') {
if (value.match(fileIdRegex)) { if (value.match(fileIdRegex)) {
const id = value.split('fileId:')[1]; const id = value.split('fileId:')[1];
@ -147,7 +143,7 @@ export default Vue.extend({
} }
// Return nothing, means keep the default handling measure // Return nothing, means keep the default handling measure
}, },
onTag (tag, code, options) { onTag(tag, code) {
if (tag === 'img' && code.includes(`alt="workflow-screenshot"`)) { if (tag === 'img' && code.includes(`alt="workflow-screenshot"`)) {
return ''; return '';
} }
@ -170,13 +166,13 @@ export default Vue.extend({
onClick(event: MouseEvent) { onClick(event: MouseEvent) {
let clickedLink = null; let clickedLink = null;
if(event.target instanceof HTMLAnchorElement) { if (event.target instanceof HTMLAnchorElement) {
clickedLink = event.target; clickedLink = event.target;
} }
if(event.target instanceof HTMLElement && event.target.matches('a *')) { if (event.target instanceof HTMLElement && event.target.matches('a *')) {
const parentLink = event.target.closest('a'); const parentLink = event.target.closest('a');
if(parentLink) { if (parentLink) {
clickedLink = parentLink; clickedLink = parentLink;
} }
} }
@ -195,13 +191,17 @@ export default Vue.extend({
line-height: var(--font-line-height-xloose); line-height: var(--font-line-height-xloose);
} }
h1, h2, h3, h4 { h1,
h2,
h3,
h4 {
margin-bottom: var(--spacing-s); margin-bottom: var(--spacing-s);
font-size: var(--font-size-m); font-size: var(--font-size-m);
font-weight: var(--font-weight-bold); font-weight: var(--font-weight-bold);
} }
h3, h4 { h3,
h4 {
font-weight: var(--font-weight-bold); font-weight: var(--font-weight-bold);
} }
@ -210,7 +210,8 @@ export default Vue.extend({
margin-bottom: var(--spacing-s); margin-bottom: var(--spacing-s);
} }
ul, ol { ul,
ol {
margin-bottom: var(--spacing-s); margin-bottom: var(--spacing-s);
padding-left: var(--spacing-m); padding-left: var(--spacing-m);
@ -261,7 +262,10 @@ export default Vue.extend({
.sticky { .sticky {
color: var(--color-text-dark); color: var(--color-text-dark);
h1, h2, h3, h4 { h1,
h2,
h3,
h4 {
margin-bottom: var(--spacing-2xs); margin-bottom: var(--spacing-2xs);
font-weight: var(--font-weight-bold); font-weight: var(--font-weight-bold);
line-height: var(--font-line-height-loose); line-height: var(--font-line-height-loose);
@ -275,7 +279,10 @@ export default Vue.extend({
font-size: 24px; font-size: 24px;
} }
h3, h4, h5, h6 { h3,
h4,
h5,
h6 {
font-size: var(--font-size-m); font-size: var(--font-size-m);
} }
@ -286,7 +293,8 @@ export default Vue.extend({
line-height: var(--font-line-height-loose); line-height: var(--font-line-height-loose);
} }
ul, ol { ul,
ol {
margin-bottom: var(--spacing-2xs); margin-bottom: var(--spacing-2xs);
padding-left: var(--spacing-m); padding-left: var(--spacing-m);
@ -304,7 +312,9 @@ export default Vue.extend({
color: var(--color-secondary); color: var(--color-secondary);
} }
pre > code,li > code, p > code { pre > code,
li > code,
p > code {
color: var(--color-secondary); color: var(--color-secondary);
} }
@ -317,7 +327,7 @@ export default Vue.extend({
img { img {
object-fit: contain; object-fit: contain;
&[src*="#full-width"] { &[src*='#full-width'] {
width: 100%; width: 100%;
} }
} }

View file

@ -1,14 +1,13 @@
import N8nMenu from './Menu.vue'; import N8nMenu from './Menu.vue';
import N8nIcon from '../N8nIcon'; import N8nIcon from '../N8nIcon';
import N8nText from '../N8nText'; import N8nText from '../N8nText';
import { StoryFn } from '@storybook/vue'; import type { StoryFn } from '@storybook/vue';
import { action } from '@storybook/addon-actions'; import { action } from '@storybook/addon-actions';
export default { export default {
title: 'Atoms/Menu', title: 'Atoms/Menu',
component: N8nMenu, component: N8nMenu,
argTypes: { argTypes: {},
},
}; };
const methods = { const methods = {

View file

@ -1,22 +1,20 @@
<template> <template>
<div :class="{ <div
:class="{
['menu-container']: true, ['menu-container']: true,
[$style.container]: true, [$style.container]: true,
[$style.menuCollapsed]: collapsed [$style.menuCollapsed]: collapsed,
}"> }"
>
<div v-if="$slots.header" :class="$style.menuHeader"> <div v-if="$slots.header" :class="$style.menuHeader">
<slot name="header"></slot> <slot name="header"></slot>
</div> </div>
<div :class="$style.menuContent"> <div :class="$style.menuContent">
<div :class="{[$style.upperContent]: true, ['pt-xs']: $slots.menuPrefix }"> <div :class="{ [$style.upperContent]: true, ['pt-xs']: $slots.menuPrefix }">
<div v-if="$slots.menuPrefix" :class="$style.menuPrefix"> <div v-if="$slots.menuPrefix" :class="$style.menuPrefix">
<slot name="menuPrefix"></slot> <slot name="menuPrefix"></slot>
</div> </div>
<el-menu <el-menu :defaultActive="defaultActive" :collapse="collapsed" v-on="$listeners">
:defaultActive="defaultActive"
:collapse="collapsed"
v-on="$listeners"
>
<n8n-menu-item <n8n-menu-item
v-for="item in upperMenuItems" v-for="item in upperMenuItems"
:key="item.id" :key="item.id"
@ -30,11 +28,7 @@
</el-menu> </el-menu>
</div> </div>
<div :class="[$style.lowerContent, 'pb-2xs']"> <div :class="[$style.lowerContent, 'pb-2xs']">
<el-menu <el-menu :defaultActive="defaultActive" :collapse="collapsed" v-on="$listeners">
:defaultActive="defaultActive"
:collapse="collapsed"
v-on="$listeners"
>
<n8n-menu-item <n8n-menu-item
v-for="item in lowerMenuItems" v-for="item in lowerMenuItems"
:key="item.id" :key="item.id"
@ -62,7 +56,6 @@ import ElMenu from 'element-ui/lib/menu';
import N8nMenuItem from '../N8nMenuItem'; import N8nMenuItem from '../N8nMenuItem';
import Vue, { PropType } from 'vue'; import Vue, { PropType } from 'vue';
import { Route } from 'vue-router';
import { IMenuItem } from '../../types'; import { IMenuItem } from '../../types';
export default Vue.extend({ export default Vue.extend({
@ -108,9 +101,13 @@ export default Vue.extend({
}, },
mounted() { mounted() {
if (this.mode === 'router') { if (this.mode === 'router') {
const found = this.items.find(item => { const found = this.items.find((item) => {
return Array.isArray(item.activateOnRouteNames) && item.activateOnRouteNames.includes(this.$route.name || '') || return (
Array.isArray(item.activateOnRoutePaths) && item.activateOnRoutePaths.includes(this.$route.path); (Array.isArray(item.activateOnRouteNames) &&
item.activateOnRouteNames.includes(this.$route.name ?? '')) ||
(Array.isArray(item.activateOnRoutePaths) &&
item.activateOnRoutePaths.includes(this.$route.path))
);
}); });
this.activeTab = found ? found.id : ''; this.activeTab = found ? found.id : '';
} else { } else {
@ -121,10 +118,14 @@ export default Vue.extend({
}, },
computed: { computed: {
upperMenuItems(): IMenuItem[] { upperMenuItems(): IMenuItem[] {
return this.items.filter((item: IMenuItem) => item.position === 'top' && item.available !== false); return this.items.filter(
(item: IMenuItem) => item.position === 'top' && item.available !== false,
);
}, },
lowerMenuItems(): IMenuItem[] { lowerMenuItems(): IMenuItem[] {
return this.items.filter((item: IMenuItem) => item.position === 'bottom' && item.available !== false); return this.items.filter(
(item: IMenuItem) => item.position === 'bottom' && item.available !== false,
);
}, },
}, },
methods: { methods: {
@ -178,11 +179,13 @@ export default Vue.extend({
.menuCollapsed { .menuCollapsed {
transition: width 150ms ease-in-out; transition: width 150ms ease-in-out;
:global(.hideme) { display: none !important; } :global(.hideme) {
display: none !important;
}
} }
.menuPrefix, .menuSuffix { .menuPrefix,
.menuSuffix {
padding: var(--spacing-xs) var(--spacing-l); padding: var(--spacing-xs) var(--spacing-l);
} }
</style> </style>

View file

@ -1,6 +1,6 @@
import N8nMenuItem from "."; import N8nMenuItem from '.';
import ElMenu from 'element-ui/lib/menu'; import ElMenu from 'element-ui/lib/menu';
import { StoryFn } from '@storybook/vue'; import type { StoryFn } from '@storybook/vue';
export default { export default {
title: 'Atoms/MenuItem', title: 'Atoms/MenuItem',
@ -11,7 +11,7 @@ const template: StoryFn = (args, { argTypes }) => ({
props: Object.keys(argTypes), props: Object.keys(argTypes),
components: { components: {
ElMenu, // eslint-disable-line @typescript-eslint/no-unsafe-assignment ElMenu, // eslint-disable-line @typescript-eslint/no-unsafe-assignment
N8nMenuItem , N8nMenuItem,
}, },
template: ` template: `
<div style="width: 200px"> <div style="width: 200px">

View file

@ -6,14 +6,19 @@
:class="{ :class="{
[$style.submenu]: true, [$style.submenu]: true,
[$style.compact]: compact, [$style.compact]: compact,
[$style.active]: mode === 'router' && isItemActive(item) [$style.active]: mode === 'router' && isItemActive(item),
}" }"
:index="item.id" :index="item.id"
popper-append-to-body popper-append-to-body
:popper-class="`${$style.submenuPopper} ${popperClass}`" :popper-class="`${$style.submenuPopper} ${popperClass}`"
> >
<template slot="title"> <template slot="title">
<n8n-icon v-if="item.icon" :class="$style.icon" :icon="item.icon" :size="item.customIconSize || 'large'" /> <n8n-icon
v-if="item.icon"
:class="$style.icon"
:icon="item.icon"
:size="item.customIconSize || 'large'"
/>
<span :class="$style.label">{{ item.label }}</span> <span :class="$style.label">{{ item.label }}</span>
</template> </template>
<el-menu-item <el-menu-item
@ -32,7 +37,13 @@
<span :class="$style.label">{{ child.label }}</span> <span :class="$style.label">{{ child.label }}</span>
</el-menu-item> </el-menu-item>
</el-submenu> </el-submenu>
<n8n-tooltip v-else placement="right" :content="item.label" :disabled="!compact" :open-delay="tooltipDelay"> <n8n-tooltip
v-else
placement="right"
:content="item.label"
:disabled="!compact"
:open-delay="tooltipDelay"
>
<el-menu-item <el-menu-item
:id="item.id" :id="item.id"
:class="{ :class="{
@ -40,12 +51,17 @@
[$style.item]: true, [$style.item]: true,
[$style.disableActiveStyle]: !isItemActive(item), [$style.disableActiveStyle]: !isItemActive(item),
[$style.active]: isItemActive(item), [$style.active]: isItemActive(item),
[$style.compact]: compact [$style.compact]: compact,
}" }"
:index="item.id" :index="item.id"
@click="onItemClick(item)" @click="onItemClick(item)"
> >
<n8n-icon v-if="item.icon" :class="$style.icon" :icon="item.icon" :size="item.customIconSize || 'large'" /> <n8n-icon
v-if="item.icon"
:class="$style.icon"
:icon="item.icon"
:size="item.customIconSize || 'large'"
/>
<span :class="$style.label">{{ item.label }}</span> <span :class="$style.label">{{ item.label }}</span>
</el-menu-item> </el-menu-item>
</n8n-tooltip> </n8n-tooltip>
@ -58,8 +74,7 @@ import ElMenuItem from 'element-ui/lib/menu-item';
import N8nTooltip from '../N8nTooltip'; import N8nTooltip from '../N8nTooltip';
import N8nIcon from '../N8nIcon'; import N8nIcon from '../N8nIcon';
import { IMenuItem } from '../../types'; import { IMenuItem } from '../../types';
import Vue from 'vue'; import Vue, { PropType } from 'vue';
import { Route } from 'vue-router';
export default Vue.extend({ export default Vue.extend({
name: 'n8n-menu-item', name: 'n8n-menu-item',
@ -71,7 +86,7 @@ export default Vue.extend({
}, },
props: { props: {
item: { item: {
type: Object as () => IMenuItem, type: Object as PropType<IMenuItem>,
required: true, required: true,
}, },
compact: { compact: {
@ -97,21 +112,30 @@ export default Vue.extend({
}, },
computed: { computed: {
availableChildren(): IMenuItem[] { availableChildren(): IMenuItem[] {
return Array.isArray(this.item.children) ? this.item.children.filter(child => child.available !== false) : []; return Array.isArray(this.item.children)
? this.item.children.filter((child) => child.available !== false)
: [];
}, },
}, },
methods: { methods: {
isItemActive(item: IMenuItem): boolean { isItemActive(item: IMenuItem): boolean {
const isItemActive = this.isActive(item); const isItemActive = this.isActive(item);
const hasActiveChild = Array.isArray(item.children) && item.children.some(child => this.isActive(child)); const hasActiveChild =
Array.isArray(item.children) && item.children.some((child) => this.isActive(child));
return isItemActive || hasActiveChild; return isItemActive || hasActiveChild;
}, },
isActive(item: IMenuItem): boolean { isActive(item: IMenuItem): boolean {
if (this.mode === 'router') { if (this.mode === 'router') {
if (item.activateOnRoutePaths) { if (item.activateOnRoutePaths) {
return Array.isArray(item.activateOnRoutePaths) && item.activateOnRoutePaths.includes(this.$route.path); return (
Array.isArray(item.activateOnRoutePaths) &&
item.activateOnRoutePaths.includes(this.$route.path)
);
} else if (item.activateOnRouteNames) { } else if (item.activateOnRouteNames) {
return Array.isArray(item.activateOnRouteNames) && item.activateOnRouteNames.includes(this.$route.name || ''); return (
Array.isArray(item.activateOnRouteNames) &&
item.activateOnRouteNames.includes(this.$route.name ?? '')
);
} }
return false; return false;
} else { } else {
@ -127,22 +151,20 @@ export default Vue.extend({
if (item.properties.newWindow) { if (item.properties.newWindow) {
window.open(href); window.open(href);
} } else {
else {
window.location.assign(item.properties.href); window.location.assign(item.properties.href);
} }
} }
this.$emit('click', event, item.id); this.$emit('click', event, item.id);
}, },
}, },
}); });
</script> </script>
<style module lang="scss"> <style module lang="scss">
// Element menu-item overrides // Element menu-item overrides
:global(.el-menu-item), :global(.el-submenu__title) { :global(.el-menu-item),
:global(.el-submenu__title) {
--menu-font-color: var(--color-text-base); --menu-font-color: var(--color-text-base);
--menu-item-active-background-color: var(--color-foreground-base); --menu-item-active-background-color: var(--color-foreground-base);
--menu-item-active-font-color: var(--color-text-dark); --menu-item-active-font-color: var(--color-text-dark);
@ -152,7 +174,6 @@ export default Vue.extend({
--submenu-item-height: 27px; --submenu-item-height: 27px;
} }
.submenu { .submenu {
background: none !important; background: none !important;
@ -177,7 +198,9 @@ export default Vue.extend({
} }
&:hover { &:hover {
.icon { color: var(--color-text-dark) } .icon {
color: var(--color-text-dark);
}
} }
} }
@ -189,9 +212,11 @@ export default Vue.extend({
user-select: none; user-select: none;
&:hover { &:hover {
.icon { color: var(--color-text-dark) } .icon {
color: var(--color-text-dark);
}
}
} }
};
} }
.disableActiveStyle { .disableActiveStyle {
@ -214,10 +239,13 @@ export default Vue.extend({
} }
.active { .active {
&, & :global(.el-submenu__title) { &,
& :global(.el-submenu__title) {
background-color: var(--color-foreground-base); background-color: var(--color-foreground-base);
border-radius: var(--border-radius-base); border-radius: var(--border-radius-base);
.icon { color: var(--color-text-dark) } .icon {
color: var(--color-text-dark);
}
} }
} }

View file

@ -1,6 +1,5 @@
/* tslint:disable:variable-name */ import N8nNodeIcon from './NodeIcon.vue';
import N8nNodeIcon from "./NodeIcon.vue"; import type { StoryFn } from '@storybook/vue';
import { StoryFn } from '@storybook/vue';
export default { export default {
title: 'Atoms/NodeIcon', title: 'Atoms/NodeIcon',

View file

@ -16,7 +16,7 @@
<font-awesome-icon v-else :icon="name" :style="fontStyleData" /> <font-awesome-icon v-else :icon="name" :style="fontStyleData" />
</div> </div>
<div v-else :class="$style['node-icon-placeholder']"> <div v-else :class="$style['node-icon-placeholder']">
{{ nodeTypeName? nodeTypeName.charAt(0) : '?' }} {{ nodeTypeName ? nodeTypeName.charAt(0) : '?' }}
? ?
</div> </div>
</n8n-tooltip> </n8n-tooltip>
@ -39,8 +39,7 @@ export default Vue.extend({
type: { type: {
type: String, type: String,
required: true, required: true,
validator: (value: string): boolean => validator: (value: string): boolean => ['file', 'icon', 'unknown'].includes(value),
['file', 'icon', 'unknown'].includes(value),
}, },
src: { src: {
type: String, type: String,
@ -68,8 +67,8 @@ export default Vue.extend({
}, },
}, },
computed: { computed: {
iconStyleData (): object { iconStyleData(): object {
if(!this.size) { if (!this.size) {
return { return {
color: this.color || '', color: this.color || '',
}; };
@ -82,7 +81,7 @@ export default Vue.extend({
'line-height': `${this.size}px`, 'line-height': `${this.size}px`,
}; };
}, },
fontStyleData (): object { fontStyleData(): object {
return { return {
'max-width': `${this.size}px`, 'max-width': `${this.size}px`,
}; };

View file

@ -1,7 +1,5 @@
/* tslint:disable:variable-name */
import N8nNotice from './Notice.vue'; import N8nNotice from './Notice.vue';
import {StoryFn} from "@storybook/vue"; import type { StoryFn } from '@storybook/vue';
export default { export default {
title: 'Atoms/Notice', title: 'Atoms/Notice',
@ -14,7 +12,7 @@ export default {
}, },
}; };
const SlotTemplate: StoryFn = (args, {argTypes}) => ({ const SlotTemplate: StoryFn = (args, { argTypes }) => ({
props: Object.keys(argTypes), props: Object.keys(argTypes),
components: { components: {
N8nNotice, N8nNotice,
@ -22,7 +20,7 @@ const SlotTemplate: StoryFn = (args, {argTypes}) => ({
template: `<n8n-notice v-bind="$props">This is a notice! Thread carefully from this point forward.</n8n-notice>`, template: `<n8n-notice v-bind="$props">This is a notice! Thread carefully from this point forward.</n8n-notice>`,
}); });
const PropTemplate: StoryFn = (args, {argTypes}) => ({ const PropTemplate: StoryFn = (args, { argTypes }) => ({
props: Object.keys(argTypes), props: Object.keys(argTypes),
components: { components: {
N8nNotice, N8nNotice,
@ -53,19 +51,22 @@ Info.args = {
export const Sanitized = PropTemplate.bind({}); export const Sanitized = PropTemplate.bind({});
Sanitized.args = { Sanitized.args = {
theme: 'warning', theme: 'warning',
content: '<script>alert(1)</script> This content contains a script tag and is <strong>sanitized</strong>.', content:
'<script>alert(1)</script> This content contains a script tag and is <strong>sanitized</strong>.',
}; };
export const Truncated = PropTemplate.bind({}); export const Truncated = PropTemplate.bind({});
Truncated.args = { Truncated.args = {
theme: 'warning', theme: 'warning',
truncate: true, truncate: true,
content: 'This content is long and will be truncated at 150 characters. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.', content:
'This content is long and will be truncated at 150 characters. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.',
}; };
export const HtmlEdgeCase = PropTemplate.bind({}); export const HtmlEdgeCase = PropTemplate.bind({});
HtmlEdgeCase.args = { HtmlEdgeCase.args = {
theme: 'warning', theme: 'warning',
truncate: true, truncate: true,
content: 'This content is long and will be truncated at 150 characters. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod <a href="">read the documentation</a> ut labore et dolore magna aliqua.', content:
'This content is long and will be truncated at 150 characters. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod <a href="">read the documentation</a> ut labore et dolore magna aliqua.',
}; };

View file

@ -1,5 +1,5 @@
<template> <template>
<div :id="id" :class="classes" role="alert" @click=onClick> <div :id="id" :class="classes" role="alert" @click="onClick">
<div class="notice-content"> <div class="notice-content">
<n8n-text size="small" :compact="true"> <n8n-text size="small" :compact="true">
<slot> <slot>
@ -18,16 +18,14 @@
<script lang="ts"> <script lang="ts">
import Vue from 'vue'; import Vue from 'vue';
import sanitizeHtml from 'sanitize-html'; import sanitizeHtml from 'sanitize-html';
import N8nText from "../../components/N8nText"; import N8nText from '../../components/N8nText';
import Locale from "../../mixins/locale"; import Locale from '../../mixins/locale';
import { uid } from "../../utils"; import { uid } from '../../utils';
export default Vue.extend({ export default Vue.extend({
name: 'n8n-notice', name: 'n8n-notice',
directives: {}, directives: {},
mixins: [ mixins: [Locale],
Locale,
],
props: { props: {
id: { id: {
type: String, type: String,
@ -56,11 +54,7 @@ export default Vue.extend({
}, },
computed: { computed: {
classes(): string[] { classes(): string[] {
return [ return ['notice', this.$style.notice, this.$style[this.theme]];
'notice',
this.$style.notice,
this.$style[this.theme],
];
}, },
canTruncate(): boolean { canTruncate(): boolean {
return this.fullContent !== undefined; return this.fullContent !== undefined;
@ -71,18 +65,16 @@ export default Vue.extend({
this.showFullContent = !this.showFullContent; this.showFullContent = !this.showFullContent;
}, },
sanitizeHtml(text: string): string { sanitizeHtml(text: string): string {
return sanitizeHtml( return sanitizeHtml(text, {
text, {
allowedAttributes: { a: ['data-key', 'href', 'target'] }, allowedAttributes: { a: ['data-key', 'href', 'target'] },
}, });
);
}, },
onClick(event: MouseEvent) { onClick(event: MouseEvent) {
if (!(event.target instanceof HTMLElement)) return; if (!(event.target instanceof HTMLElement)) return;
if (event.target.localName !== 'a') return; if (event.target.localName !== 'a') return;
if (event.target.dataset && event.target.dataset.key) { if (event.target.dataset?.key) {
event.stopPropagation(); event.stopPropagation();
event.preventDefault(); event.preventDefault();
@ -97,7 +89,6 @@ export default Vue.extend({
}, },
}, },
}); });
</script> </script>
<style lang="scss" module> <style lang="scss" module>

View file

@ -1,5 +1,5 @@
import { render } from '@testing-library/vue'; import { render } from '@testing-library/vue';
import N8nNotice from "../Notice.vue"; import N8nNotice from '../Notice.vue';
describe('components', () => { describe('components', () => {
describe('N8nNotice', () => { describe('N8nNotice', () => {

View file

@ -1,19 +1,16 @@
/* tslint:disable:variable-name */
import N8nPulse from './Pulse.vue'; import N8nPulse from './Pulse.vue';
import { StoryFn } from "@storybook/vue"; import type { StoryFn } from '@storybook/vue';
export default { export default {
title: 'Atoms/Pulse', title: 'Atoms/Pulse',
component: N8nPulse, component: N8nPulse,
argTypes: { argTypes: {},
},
parameters: { parameters: {
backgrounds: { default: '--color-background-light' }, backgrounds: { default: '--color-background-light' },
}, },
}; };
export const Default: StoryFn = (args, {argTypes}) => ({ export const Default: StoryFn = () => ({
components: { components: {
N8nPulse, N8nPulse,
}, },

View file

@ -17,7 +17,6 @@ export default Vue.extend({
</script> </script>
<style lang="scss" module> <style lang="scss" module>
$--light-pulse-color: hsla( $--light-pulse-color: hsla(
var(--color-primary-h), var(--color-primary-h),
var(--color-primary-s), var(--color-primary-s),
@ -112,5 +111,4 @@ $--dark-pulse-color: hsla(
box-shadow: 0 0 0 0 transparent; box-shadow: 0 0 0 0 transparent;
} }
} }
</style> </style>

View file

@ -1,7 +1,26 @@
<template> <template>
<label role="radio" tabindex="-1" :class="{'n8n-radio-button': true, [$style.container]: true, [$style.hoverable]: !this.disabled}" aria-checked="true"> <label
<input type="radio" tabindex="-1" autocomplete="off" :class="$style.input" :value="value"> role="radio"
<div :class="{[$style.button]: true, [$style.active]: active, [$style[size]]: true, [$style.disabled]: disabled}" @click="$emit('click')">{{ label }}</div> tabindex="-1"
:class="{
'n8n-radio-button': true,
[$style.container]: true,
[$style.hoverable]: !this.disabled,
}"
aria-checked="true"
>
<input type="radio" tabindex="-1" autocomplete="off" :class="$style.input" :value="value" />
<div
:class="{
[$style.button]: true,
[$style.active]: active,
[$style[size]]: true,
[$style.disabled]: disabled,
}"
@click="$emit('click')"
>
{{ label }}
</div>
</label> </label>
</template> </template>
@ -26,8 +45,7 @@ export default Vue.extend({
size: { size: {
type: String, type: String,
default: 'medium', default: 'medium',
validator: (value: string): boolean => validator: (value: string): boolean => ['small', 'medium'].includes(value),
['small', 'medium'].includes(value),
}, },
disabled: { disabled: {
type: Boolean, type: Boolean,

View file

@ -25,8 +25,7 @@ const Template = (args, { argTypes }) => ({
components: { components: {
N8nRadioButtons, N8nRadioButtons,
}, },
template: template: `<n8n-radio-buttons v-model="val" v-bind="$props" @input="onInput">
`<n8n-radio-buttons v-model="val" v-bind="$props" @input="onInput">
</n8n-radio-buttons>`, </n8n-radio-buttons>`,
methods, methods,
data() { data() {

View file

@ -1,5 +1,8 @@
<template> <template>
<div role="radiogroup" :class="{'n8n-radio-buttons': true, [$style.radioGroup]: true, [$style.disabled]: disabled}"> <div
role="radiogroup"
:class="{ 'n8n-radio-buttons': true, [$style.radioGroup]: true, [$style.disabled]: disabled }"
>
<RadioButton <RadioButton
v-for="option in options" v-for="option in options"
:key="option.value" :key="option.value"
@ -23,8 +26,7 @@ export default Vue.extend({
value: { value: {
type: String, type: String,
}, },
options: { options: {},
},
size: { size: {
type: String, type: String,
}, },
@ -36,7 +38,7 @@ export default Vue.extend({
RadioButton, RadioButton,
}, },
methods: { methods: {
onClick(option: {label: string, value: string, disabled?: boolean}) { onClick(option: { label: string; value: string; disabled?: boolean }) {
if (this.disabled || option.disabled) { if (this.disabled || option.disabled) {
return; return;
} }
@ -47,7 +49,6 @@ export default Vue.extend({
</script> </script>
<style lang="scss" module> <style lang="scss" module>
.radioGroup { .radioGroup {
display: inline-flex; display: inline-flex;
line-height: 1; line-height: 1;
@ -61,6 +62,4 @@ export default Vue.extend({
.disabled { .disabled {
cursor: not-allowed; cursor: not-allowed;
} }
</style> </style>

View file

@ -26,7 +26,8 @@ const Template = (args, { argTypes }) => ({
return { return {
newWidth: this.width, newWidth: this.width,
newHeight: this.height, newHeight: this.height,
background: "linear-gradient(90deg, rgba(255,0,0,1) 0%, rgba(255,154,0,1) 10%, rgba(208,222,33,1) 20%, rgba(79,220,74,1) 30%, rgba(63,218,216,1) 40%, rgba(47,201,226,1) 50%, rgba(28,127,238,1) 60%, rgba(95,21,242,1) 70%, rgba(186,12,248,1) 80%, rgba(251,7,217,1) 90%, rgba(255,0,0,1) 100%)", background:
'linear-gradient(90deg, rgba(255,0,0,1) 0%, rgba(255,154,0,1) 10%, rgba(208,222,33,1) 20%, rgba(79,220,74,1) 30%, rgba(63,218,216,1) 40%, rgba(47,201,226,1) 50%, rgba(28,127,238,1) 60%, rgba(95,21,242,1) 70%, rgba(186,12,248,1) 80%, rgba(251,7,217,1) 90%, rgba(255,0,0,1) 100%)',
}; };
}, },
computed: { computed: {
@ -38,8 +39,7 @@ const Template = (args, { argTypes }) => ({
}; };
}, },
}, },
template: template: `<div style="width: fit-content; height: fit-content">
`<div style="width: fit-content; height: fit-content">
<n8n-resize-wrapper <n8n-resize-wrapper
v-bind="$props" v-bind="$props"
@resize="onResize" @resize="onResize"
@ -65,13 +65,13 @@ Resize.args = {
gridSize: 20, gridSize: 20,
isResizingEnabled: true, isResizingEnabled: true,
supportedDirections: [ supportedDirections: [
"right", 'right',
"top", 'top',
"bottom", 'bottom',
"left", 'left',
"topLeft", 'topLeft',
"topRight", 'topRight',
"bottomLeft", 'bottomLeft',
"bottomRight", 'bottomRight',
], ],
}; };

View file

@ -19,11 +19,9 @@ function closestNumber(value: number, divisor: number): number {
const q = value / divisor; const q = value / divisor;
const n1 = divisor * q; const n1 = divisor * q;
const n2 = (value * divisor) > 0 ? const n2 = value * divisor > 0 ? divisor * (q + 1) : divisor * (q - 1);
(divisor * (q + 1)) : (divisor * (q - 1));
if (Math.abs(value - n1) < Math.abs(value - n2)) if (Math.abs(value - n1) < Math.abs(value - n2)) return n1;
return n1;
return n2; return n2;
} }
@ -35,7 +33,7 @@ function getSize(min: number, virtual: number, gridSize: number): number {
} }
return min; return min;
}; }
const directionsCursorMaps: { [key: string]: string } = { const directionsCursorMaps: { [key: string]: string } = {
right: 'ew-resize', right: 'ew-resize',
@ -43,7 +41,7 @@ const directionsCursorMaps: { [key: string]: string } = {
bottom: 'ns-resize', bottom: 'ns-resize',
left: 'ew-resize', left: 'ew-resize',
topLeft: 'nw-resize', topLeft: 'nw-resize',
topRight : 'ne-resize', topRight: 'ne-resize',
bottomLeft: 'sw-resize', bottomLeft: 'sw-resize',
bottomRight: 'se-resize', bottomRight: 'se-resize',
}; };
@ -95,8 +93,8 @@ export default Vue.extend({
enabledDirections() { enabledDirections() {
const availableDirections = Object.keys(directionsCursorMaps); const availableDirections = Object.keys(directionsCursorMaps);
if(!this.isResizingEnabled) return []; if (!this.isResizingEnabled) return [];
if(this.supportedDirections.length === 0) return availableDirections; if (this.supportedDirections.length === 0) return availableDirections;
return this.supportedDirections; return this.supportedDirections;
}, },
@ -156,7 +154,7 @@ export default Vue.extend({
const width = getSize(this.minWidth, this.vWidth, this.gridSize); const width = getSize(this.minWidth, this.vWidth, this.gridSize);
const dX = left && width !== this.width ? -1 * (width - this.width) : 0; const dX = left && width !== this.width ? -1 * (width - this.width) : 0;
const dY = top && height !== this.height ? -1 * (height - this.height): 0; const dY = top && height !== this.height ? -1 * (height - this.height) : 0;
const x = event.x; const x = event.x;
const y = event.y; const y = event.y;
const direction = this.dir; const direction = this.dir;

View file

@ -1,18 +1,9 @@
<template> <template>
<span> <span>
<router-link <router-link v-if="useRouterLink" :to="to" v-on="$listeners">
v-if="useRouterLink"
:to="to"
v-on="$listeners"
>
<slot></slot> <slot></slot>
</router-link> </router-link>
<a <a v-else :href="to" :target="openNewWindow ? '_blank' : '_self'" v-on="$listeners">
v-else
:href="to"
:target="openNewWindow ? '_blank': '_self'"
v-on="$listeners"
>
<slot></slot> <slot></slot>
</a> </a>
</span> </span>
@ -56,4 +47,3 @@ export default Vue.extend({
}, },
}); });
</script> </script>

View file

@ -1,6 +1,4 @@
/* tslint:disable:variable-name */ import type { StoryFn } from '@storybook/vue';
import { StoryFn } from '@storybook/vue';
import N8nSelect from './Select.vue'; import N8nSelect from './Select.vue';
import N8nOption from '../N8nOption'; import N8nOption from '../N8nOption';
import N8nIcon from '../N8nIcon'; import N8nIcon from '../N8nIcon';
@ -54,7 +52,8 @@ const Template: StoryFn = (args, { argTypes }) => ({
N8nOption, N8nOption,
N8nIcon, N8nIcon,
}, },
template: '<n8n-select v-bind="$props" v-model="val" @input="onInput" @change="onChange"><n8n-option value="1">op1</n8n-option><n8n-option value="2">op2</n8n-option></n8n-select>', template:
'<n8n-select v-bind="$props" v-model="val" @input="onInput" @change="onChange"><n8n-option value="1">op1</n8n-option><n8n-option value="2">op2</n8n-option></n8n-select>',
data() { data() {
return { return {
val: '', val: '',
@ -71,7 +70,12 @@ Filterable.args = {
defaultFirstOption: true, defaultFirstOption: true,
}; };
const selects = ['large', 'medium', 'small', 'mini'].map((size) => `<n8n-select v-bind="$props" v-model="val" @input="onInput" @change="onChange" size="${size}"><n8n-option value="1">op1</n8n-option><n8n-option value="2">op2</n8n-option></n8n-select>`).join(''); const selects = ['large', 'medium', 'small', 'mini']
.map(
(size) =>
`<n8n-select v-bind="$props" v-model="val" @input="onInput" @change="onChange" size="${size}"><n8n-option value="1">op1</n8n-option><n8n-option value="2">op2</n8n-option></n8n-select>`,
)
.join('');
const ManyTemplate: StoryFn = (args, { argTypes }) => ({ const ManyTemplate: StoryFn = (args, { argTypes }) => ({
props: Object.keys(argTypes), props: Object.keys(argTypes),
@ -96,7 +100,12 @@ Sizes.args = {
placeholder: 'placeholder...', placeholder: 'placeholder...',
}; };
const selectsWithIcon = ['xlarge', 'large', 'medium', 'small', 'mini'].map((size) => `<n8n-select v-bind="$props" v-model="val" @input="onInput" size="${size}"><n8n-icon icon="search" slot="prefix" /><n8n-option value="1">op1</n8n-option><n8n-option value="2">op2</n8n-option></n8n-select>`).join(''); const selectsWithIcon = ['xlarge', 'large', 'medium', 'small', 'mini']
.map(
(size) =>
`<n8n-select v-bind="$props" v-model="val" @input="onInput" size="${size}"><n8n-icon icon="search" slot="prefix" /><n8n-option value="1">op1</n8n-option><n8n-option value="2">op2</n8n-option></n8n-select>`,
)
.join('');
const ManyTemplateWithIcon: StoryFn = (args, { argTypes }) => ({ const ManyTemplateWithIcon: StoryFn = (args, { argTypes }) => ({
props: Object.keys(argTypes), props: Object.keys(argTypes),
@ -121,7 +130,6 @@ WithIcon.args = {
placeholder: 'placeholder...', placeholder: 'placeholder...',
}; };
const LimitedWidthTemplate: StoryFn = (args, { argTypes }) => ({ const LimitedWidthTemplate: StoryFn = (args, { argTypes }) => ({
props: Object.keys(argTypes), props: Object.keys(argTypes),
components: { components: {
@ -129,7 +137,8 @@ const LimitedWidthTemplate: StoryFn = (args, { argTypes }) => ({
N8nOption, N8nOption,
N8nIcon, N8nIcon,
}, },
template: '<div style="width:100px;"><n8n-select v-bind="$props" v-model="val" @input="onInput" @change="onChange"><n8n-option value="1" label="opt1 11 1111" /><n8n-option value="2" label="opt2 test very long ipsum"/></n8n-select></div>', template:
'<div style="width:100px;"><n8n-select v-bind="$props" v-model="val" @input="onInput" @change="onChange"><n8n-option value="1" label="opt1 11 1111" /><n8n-option value="2" label="opt2 test very long ipsum"/></n8n-select></div>',
data() { data() {
return { return {
val: '', val: '',

View file

@ -1,5 +1,11 @@
<template> <template>
<div :class="{'n8n-select': true, [$style.container]: true, [$style.withPrepend]: !!$slots.prepend}"> <div
:class="{
'n8n-select': true,
[$style.container]: true,
[$style.withPrepend]: !!$slots.prepend,
}"
>
<div v-if="$slots.prepend" :class="$style.prepend"> <div v-if="$slots.prepend" :class="$style.prepend">
<slot name="prepend" /> <slot name="prepend" />
</div> </div>
@ -41,8 +47,7 @@ export default Vue.extend({
ElSelect, // eslint-disable-line @typescript-eslint/no-unsafe-assignment ElSelect, // eslint-disable-line @typescript-eslint/no-unsafe-assignment
}, },
props: { props: {
value: { value: {},
},
size: { size: {
type: String, type: String,
default: 'large', default: 'large',
@ -112,21 +117,21 @@ export default Vue.extend({
}, },
methods: { methods: {
focus() { focus() {
const select = this.$refs.innerSelect as Vue & HTMLElement | undefined; const select = this.$refs.innerSelect as (Vue & HTMLElement) | undefined;
if (select) { if (select) {
select.focus(); select.focus();
} }
}, },
blur() { blur() {
const select = this.$refs.innerSelect as Vue & HTMLElement | undefined; const select = this.$refs.innerSelect as (Vue & HTMLElement) | undefined;
if (select) { if (select) {
select.blur(); select.blur();
} }
}, },
focusOnInput() { focusOnInput() {
const select = this.$refs.innerSelect as Vue & HTMLElement | undefined; const select = this.$refs.innerSelect as (Vue & HTMLElement) | undefined;
if (select) { if (select) {
const input = select.$refs.input as Vue & HTMLElement | undefined; const input = select.$refs.input as (Vue & HTMLElement) | undefined;
if (input) { if (input) {
input.focus(); input.focus();
} }

View file

@ -1,6 +1,6 @@
import {render} from '@testing-library/vue'; import { render } from '@testing-library/vue';
import N8nSelect from "../Select.vue"; import N8nSelect from '../Select.vue';
import N8nOption from "../../N8nOption/Option.vue"; import N8nOption from '../../N8nOption/Option.vue';
describe('components', () => { describe('components', () => {
describe('N8nSelect', () => { describe('N8nSelect', () => {

View file

@ -1,12 +1,12 @@
<template> <template>
<span class="n8n-spinner"> <span class="n8n-spinner">
<div v-if="type === 'ring'" class="lds-ring"><div></div><div></div><div></div><div></div></div> <div v-if="type === 'ring'" class="lds-ring">
<n8n-icon <div></div>
v-else <div></div>
icon="spinner" <div></div>
:size="size" <div></div>
spin </div>
/> <n8n-icon v-else icon="spinner" :size="size" spin />
</span> </span>
</template> </template>
@ -23,13 +23,13 @@ export default Vue.extend({
props: { props: {
size: { size: {
type: String, type: String,
validator (value: string): boolean { validator(value: string): boolean {
return ['small', 'medium', 'large'].includes(value); return ['small', 'medium', 'large'].includes(value);
}, },
}, },
type: { type: {
type: String, type: String,
validator (value: string): boolean { validator(value: string): boolean {
return ['dots', 'ring'].includes(value); return ['dots', 'ring'].includes(value);
}, },
default: 'dots', default: 'dots',
@ -73,5 +73,4 @@ export default Vue.extend({
transform: rotate(360deg); transform: rotate(360deg);
} }
} }
</style> </style>

View file

@ -1,6 +1,6 @@
<template> <template>
<div <div
:class="{'n8n-sticky': true, [$style.sticky]: true, [$style.clickable]: !isResizing}" :class="{ 'n8n-sticky': true, [$style.sticky]: true, [$style.clickable]: !isResizing }"
:style="styles" :style="styles"
@keydown.prevent @keydown.prevent
> >
@ -39,7 +39,7 @@
@keydown.stop @keydown.stop
@wheel.stop @wheel.stop
class="sticky-textarea ph-no-capture" class="sticky-textarea ph-no-capture"
:class="{'full-height': !shouldShowFooter}" :class="{ 'full-height': !shouldShowFooter }"
> >
<n8n-input <n8n-input
:value="content" :value="content"
@ -49,13 +49,9 @@
@input="onInput" @input="onInput"
ref="input" ref="input"
/> />
</div> </div>
<div v-if="editMode && shouldShowFooter" :class="$style.footer"> <div v-if="editMode && shouldShowFooter" :class="$style.footer">
<n8n-text <n8n-text size="xsmall" aligh="right">
size="xsmall"
aligh="right"
>
<span v-html="t('sticky.markdownHint')"></span> <span v-html="t('sticky.markdownHint')"></span>
</n8n-text> </n8n-text>
</div> </div>
@ -142,7 +138,7 @@ export default mixins(Locale).extend({
} }
return this.width; return this.width;
}, },
styles(): { height: string, width: string } { styles(): { height: string; width: string } {
return { return {
height: `${this.resHeight}px`, height: `${this.resHeight}px`,
width: `${this.resWidth}px`, width: `${this.resWidth}px`,
@ -184,10 +180,7 @@ export default mixins(Locale).extend({
watch: { watch: {
editMode(newMode, prevMode) { editMode(newMode, prevMode) {
setTimeout(() => { setTimeout(() => {
if (newMode && if (newMode && !prevMode && this.$refs.input) {
!prevMode &&
this.$refs.input
) {
const textarea = this.$refs.input as HTMLTextAreaElement; const textarea = this.$refs.input as HTMLTextAreaElement;
if (this.defaultText === this.content) { if (this.defaultText === this.content) {
textarea.select(); textarea.select();
@ -195,7 +188,6 @@ export default mixins(Locale).extend({
textarea.focus(); textarea.focus();
} }
}, 100); }, 100);
}, },
}, },
}); });
@ -227,7 +219,12 @@ export default mixins(Locale).extend({
left: 0; left: 0;
bottom: 0; bottom: 0;
position: absolute; position: absolute;
background: linear-gradient(180deg, var(--color-sticky-default-background), #fff5d600 0.01%, var(--color-sticky-default-background)); background: linear-gradient(
180deg,
var(--color-sticky-default-background),
#fff5d600 0.01%,
var(--color-sticky-default-background)
);
border-radius: var(--border-radius-base); border-radius: var(--border-radius-base);
} }
} }

View file

@ -5,8 +5,7 @@ import { action } from '@storybook/addon-actions';
export default { export default {
title: 'Atoms/Tabs', title: 'Atoms/Tabs',
component: N8nTabs, component: N8nTabs,
argTypes: { argTypes: {},
},
parameters: { parameters: {
backgrounds: { default: '--color-background-xlight' }, backgrounds: { default: '--color-background-xlight' },
}, },
@ -21,8 +20,7 @@ const Template = (args, { argTypes }) => ({
components: { components: {
N8nTabs, N8nTabs,
}, },
template: template: `<n8n-tabs v-model="val" v-bind="$props" @input="onInput">
`<n8n-tabs v-model="val" v-bind="$props" @input="onInput">
</n8n-tabs>`, </n8n-tabs>`,
methods, methods,
data() { data() {

View file

@ -7,13 +7,18 @@
<n8n-icon icon="chevron-right" size="small" /> <n8n-icon icon="chevron-right" size="small" />
</div> </div>
<div ref="tabs" :class="$style.tabs"> <div ref="tabs" :class="$style.tabs">
<div v-for="option in options" <div
v-for="option in options"
:key="option.value" :key="option.value"
:id="option.value" :id="option.value"
:class="{ [$style.alignRight]: option.align === 'right' }" :class="{ [$style.alignRight]: option.align === 'right' }"
> >
<n8n-tooltip :disabled="!option.tooltip" placement="bottom"> <n8n-tooltip :disabled="!option.tooltip" placement="bottom">
<div slot="content" v-html="option.tooltip" @click="handleTooltipClick(option.value, $event)"></div> <div
slot="content"
v-html="option.tooltip"
@click="handleTooltipClick(option.value, $event)"
></div>
<a <a
v-if="option.href" v-if="option.href"
target="_blank" target="_blank"
@ -23,7 +28,9 @@
> >
<div> <div>
{{ option.label }} {{ option.label }}
<span :class="$style.external"><n8n-icon icon="external-link-alt" size="small" /></span> <span :class="$style.external"
><n8n-icon icon="external-link-alt" size="small"
/></span>
</div> </div>
</a> </a>
@ -56,8 +63,7 @@ export default Vue.extend({
container.addEventListener('scroll', (event: Event) => { container.addEventListener('scroll', (event: Event) => {
const width = container.clientWidth; const width = container.clientWidth;
const scrollWidth = container.scrollWidth; const scrollWidth = container.scrollWidth;
// @ts-ignore this.scrollPosition = (event.target as Element).scrollLeft;
this.scrollPosition = event.srcElement.scrollLeft; // eslint-disable-line @typescript-eslint/no-unsafe-assignment
this.canScrollRight = scrollWidth - width > this.scrollPosition; this.canScrollRight = scrollWidth - width > this.scrollPosition;
}); });
@ -87,10 +93,8 @@ export default Vue.extend({
}; };
}, },
props: { props: {
value: { value: {},
}, options: {},
options: {
},
}, },
methods: { methods: {
handleTooltipClick(tab: string, event: MouseEvent) { handleTooltipClick(tab: string, event: MouseEvent) {
@ -106,7 +110,9 @@ export default Vue.extend({
this.scroll(50); this.scroll(50);
}, },
scroll(left: number) { scroll(left: number) {
const container = this.$refs.tabs as (HTMLDivElement & { scrollBy: ScrollByFunction }) | undefined; const container = this.$refs.tabs as
| (HTMLDivElement & { scrollBy: ScrollByFunction })
| undefined;
if (container) { if (container) {
container.scrollBy({ left, top: 0, behavior: 'smooth' }); container.scrollBy({ left, top: 0, behavior: 'smooth' });
} }
@ -114,11 +120,13 @@ export default Vue.extend({
}, },
}); });
type ScrollByFunction = (arg: { left: number, top: number, behavior: 'smooth' | 'instant' | 'auto' }) => void; type ScrollByFunction = (arg: {
left: number;
top: number;
behavior: 'smooth' | 'instant' | 'auto';
}) => void;
</script> </script>
<style lang="scss" module> <style lang="scss" module>
.container { .container {
position: relative; position: relative;
@ -205,5 +213,4 @@ type ScrollByFunction = (arg: { left: number, top: number, behavior: 'smooth' |
composes: button; composes: button;
right: 0; right: 0;
} }
</style> </style>

View file

@ -17,8 +17,7 @@ const Template = (args, { argTypes }) => ({
components: { components: {
N8nTag, N8nTag,
}, },
template: template: '<n8n-tag v-bind="$props"></n8n-tag>',
'<n8n-tag v-bind="$props"></n8n-tag>',
}); });
export const Tag = Template.bind({}); export const Tag = Template.bind({});

View file

@ -29,7 +29,11 @@ export default Vue.extend({
transition: background-color 0.3s ease; transition: background-color 0.3s ease;
&:hover { &:hover {
background-color: hsl(var(--color-background-base-h), var(--color-background-base-s), calc(var(--color-background-base-l) - 4%)); background-color: hsl(
var(--color-background-base-h),
var(--color-background-base-s),
calc(var(--color-background-base-l) - 4%)
);
} }
} }
</style> </style>

View file

@ -3,8 +3,7 @@ import N8nTags from './Tags.vue';
export default { export default {
title: 'Atoms/Tags', title: 'Atoms/Tags',
component: N8nTags, component: N8nTags,
argTypes: { argTypes: {},
},
}; };
const Template = (args, { argTypes }) => ({ const Template = (args, { argTypes }) => ({
@ -12,8 +11,7 @@ const Template = (args, { argTypes }) => ({
components: { components: {
N8nTags, N8nTags,
}, },
template: template: '<n8n-tags v-bind="$props"></n8n-tags>',
'<n8n-tags v-bind="$props"></n8n-tags>',
}); });
export const Tags = Template.bind({}); export const Tags = Template.bind({});
@ -34,7 +32,6 @@ Tags.args = {
], ],
}; };
export const Truncated = Template.bind({}); export const Truncated = Template.bind({});
Truncated.args = { Truncated.args = {
truncate: true, truncate: true,

View file

@ -1,6 +1,11 @@
<template> <template>
<div :class="['n8n-tags', $style.tags]"> <div :class="['n8n-tags', $style.tags]">
<n8n-tag v-for="tag in visibleTags" :key="tag.id" :text="tag.name" @click="$emit('click', tag.id, $event)"/> <n8n-tag
v-for="tag in visibleTags"
:key="tag.id"
:text="tag.name"
@click="$emit('click', tag.id, $event)"
/>
<n8n-link <n8n-link
v-if="truncate && !showAll && hiddenTagsLength > 0" v-if="truncate && !showAll && hiddenTagsLength > 0"
theme="text" theme="text"
@ -16,9 +21,9 @@
<script lang="ts"> <script lang="ts">
import N8nTag from '../N8nTag'; import N8nTag from '../N8nTag';
import N8nLink from '../N8nLink'; import N8nLink from '../N8nLink';
import Locale from "../../mixins/locale"; import Locale from '../../mixins/locale';
import Vue, {PropType} from 'vue'; import { PropType } from 'vue';
import mixins from "vue-typed-mixins"; import mixins from 'vue-typed-mixins';
interface ITag { interface ITag {
id: string; id: string;

View file

@ -13,7 +13,15 @@ export default {
color: { color: {
control: { control: {
type: 'select', type: 'select',
options: ['primary', 'text-dark', 'text-base', 'text-light', 'text-xlight', 'danger', 'success'], options: [
'primary',
'text-dark',
'text-base',
'text-light',
'text-xlight',
'danger',
'success',
],
}, },
}, },
}, },

View file

@ -17,11 +17,22 @@ export default Vue.extend({
size: { size: {
type: String, type: String,
default: 'medium', default: 'medium',
validator: (value: string): boolean => ['xsmall', 'small', 'mini', 'medium', 'large', 'xlarge'].includes(value), validator: (value: string): boolean =>
['xsmall', 'small', 'mini', 'medium', 'large', 'xlarge'].includes(value),
}, },
color: { color: {
type: String, type: String,
validator: (value: string): boolean => ['primary', 'text-dark', 'text-base', 'text-light', 'text-xlight', 'danger', 'success', 'warning'].includes(value), validator: (value: string): boolean =>
[
'primary',
'text-dark',
'text-base',
'text-light',
'text-xlight',
'danger',
'success',
'warning',
].includes(value),
}, },
align: { align: {
type: String, type: String,
@ -40,6 +51,7 @@ export default Vue.extend({
classes() { classes() {
const applied = []; const applied = [];
if (this.align) { if (this.align) {
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
applied.push(`align-${this.align}`); applied.push(`align-${this.align}`);
} }
if (this.color) { if (this.color) {
@ -50,9 +62,10 @@ export default Vue.extend({
applied.push('compact'); applied.push('compact');
} }
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
applied.push(`size-${this.size}`); applied.push(`size-${this.size}`);
applied.push(this.bold? 'bold': 'regular'); applied.push(this.bold ? 'bold' : 'regular');
return applied.map((c) => (this.$style as { [key: string]: string })[c]); return applied.map((c) => (this.$style as { [key: string]: string })[c]);
}, },
@ -141,5 +154,4 @@ export default Vue.extend({
.align-center { .align-center {
text-align: center; text-align: center;
} }
</style> </style>

View file

@ -1,8 +1,13 @@
<template> <template>
<el-tooltip v-bind="$attrs"> <el-tooltip v-bind="$attrs">
<template v-for="(_, slotName) in $slots" #[slotName]> <template v-for="(_, slotName) in $slots" #[slotName]>
<slot :name="slotName"/> <slot :name="slotName" />
<div :key="slotName" v-if="slotName === 'content' && buttons.length" :class="$style.buttons" :style="{ justifyContent: justifyButtons }"> <div
:key="slotName"
v-if="slotName === 'content' && buttons.length"
:class="$style.buttons"
:style="{ justifyContent: justifyButtons }"
>
<n8n-button <n8n-button
v-for="button in buttons" v-for="button in buttons"
:key="button.attrs.label" :key="button.attrs.label"
@ -15,9 +20,9 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import Vue, { PropType } from "vue"; import Vue, { PropType } from 'vue';
import ElTooltip from 'element-ui/lib/tooltip'; import ElTooltip from 'element-ui/lib/tooltip';
import type { IN8nButton } from "@/types"; import type { IN8nButton } from '@/types';
import N8nButton from '../N8nButton'; import N8nButton from '../N8nButton';
export default Vue.extend({ export default Vue.extend({
@ -31,7 +36,19 @@ export default Vue.extend({
justifyButtons: { justifyButtons: {
type: String, type: String,
default: 'flex-end', default: 'flex-end',
validator: (value: string): boolean => ['flex-start', 'flex-end', 'start', 'end', 'left', 'right', 'center', 'space-between', 'space-around', 'space-evenly'].includes(value), validator: (value: string): boolean =>
[
'flex-start',
'flex-end',
'start',
'end',
'left',
'right',
'center',
'space-between',
'space-around',
'space-evenly',
].includes(value),
}, },
buttons: { buttons: {
type: Array as PropType<IN8nButton[]>, type: Array as PropType<IN8nButton[]>,

View file

@ -1,14 +1,12 @@
/* tslint:disable:variable-name */
import N8nTree from './Tree.vue'; import N8nTree from './Tree.vue';
import {StoryFn} from "@storybook/vue"; import type { StoryFn } from '@storybook/vue';
export default { export default {
title: 'Atoms/Tree', title: 'Atoms/Tree',
component: N8nTree, component: N8nTree,
}; };
export const Default: StoryFn = (args, {argTypes}) => ({ export const Default: StoryFn = (args, { argTypes }) => ({
props: Object.keys(argTypes), props: Object.keys(argTypes),
components: { components: {
N8nTree, N8nTree,
@ -26,32 +24,26 @@ export const Default: StoryFn = (args, {argTypes}) => ({
Default.args = { Default.args = {
value: { value: {
objectKey: { objectKey: {
nestedArrayKey: [ nestedArrayKey: ['in progress', 33958053],
'in progress',
33958053,
],
stringKey: 'word', stringKey: 'word',
aLongKey: 'Lorem ipsum dolor sit consectetur adipiscing elit. Sed dignissim aliquam ipsum mattis pellentesque. Phasellus ut ligula fermentum orci elementum dignissim. Vivamus interdum risus eget nibh placerat ultrices. Vivamus orci arcu, iaculis in nulla non, blandit molestie magna. Praesent tristique feugiat odio non vehicula. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse fermentum purus diam, nec auctor elit consectetur nec. Vestibulum ultrices diam magna, in faucibus odio bibendum id. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut sollicitudin lacus neque.', aLongKey:
'Lorem ipsum dolor sit consectetur adipiscing elit. Sed dignissim aliquam ipsum mattis pellentesque. Phasellus ut ligula fermentum orci elementum dignissim. Vivamus interdum risus eget nibh placerat ultrices. Vivamus orci arcu, iaculis in nulla non, blandit molestie magna. Praesent tristique feugiat odio non vehicula. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse fermentum purus diam, nec auctor elit consectetur nec. Vestibulum ultrices diam magna, in faucibus odio bibendum id. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut sollicitudin lacus neque.',
objectKey: { objectKey: {
myKey: 'what\'s for lunch', myKey: "what's for lunch",
yourKey: 'prolle rewe wdyt', yourKey: 'prolle rewe wdyt',
}, },
id: 123, id: 123,
}, },
hello: "world", hello: 'world',
test: { test: {
label: "A cool folder", label: 'A cool folder',
children: [ children: [
{ {
label: "A cool sub-folder 1", label: 'A cool sub-folder 1',
children: [ children: [{ label: 'A cool sub-sub-folder 1' }, { label: 'A cool sub-sub-folder 2' }],
{ label: "A cool sub-sub-folder 1" },
{ label: "A cool sub-sub-folder 2" },
],
}, },
{ label: "This one is not that cool" }, { label: 'This one is not that cool' },
], ],
}, },
}, },
}; };

View file

@ -1,17 +1,36 @@
<template> <template>
<div class="n8n-tree"> <div class="n8n-tree">
<div v-for="(label, i) in Object.keys(value || {})" :key="i" :class="{[nodeClass]: !!nodeClass, [$style.indent]: depth > 0}"> <div
v-for="(label, i) in Object.keys(value || {})"
:key="i"
:class="{ [nodeClass]: !!nodeClass, [$style.indent]: depth > 0 }"
>
<div :class="$style.simple" v-if="isSimple(value[label])"> <div :class="$style.simple" v-if="isSimple(value[label])">
<slot v-if="$scopedSlots.label" name="label" v-bind:label="label" v-bind:path="getPath(label)" /> <slot
v-if="$scopedSlots.label"
name="label"
v-bind:label="label"
v-bind:path="getPath(label)"
/>
<span v-else>{{ label }}</span> <span v-else>{{ label }}</span>
<span>:</span> <span>:</span>
<slot v-if="$scopedSlots.value" name="value" v-bind:value="value[label]" /> <slot v-if="$scopedSlots.value" name="value" v-bind: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="$scopedSlots.label" name="label" v-bind:label="label" v-bind:path="getPath(label)" /> <slot
v-if="$scopedSlots.label"
name="label"
v-bind:label="label"
v-bind:path="getPath(label)"
/>
<span v-else>{{ label }}</span> <span v-else>{{ label }}</span>
<n8n-tree :path="getPath(label)" :depth="depth + 1" :value="value[label]" :nodeClass="nodeClass"> <n8n-tree
:path="getPath(label)"
:depth="depth + 1"
:value="value[label]"
:nodeClass="nodeClass"
>
<template v-for="(index, name) in $scopedSlots" v-slot:[name]="data"> <template v-for="(index, name) in $scopedSlots" v-slot:[name]="data">
<slot :name="name" v-bind="data"></slot> <slot :name="name" v-bind="data"></slot>
</template> </template>
@ -26,11 +45,9 @@ import Vue from 'vue';
export default Vue.extend({ export default Vue.extend({
name: 'n8n-tree', name: 'n8n-tree',
components: { components: {},
},
props: { props: {
value: { value: {},
},
path: { path: {
type: Array, type: Array,
default: () => [], default: () => [],
@ -70,7 +87,6 @@ export default Vue.extend({
</script> </script>
<style lang="scss" module> <style lang="scss" module>
$--spacing: var(--spacing-s); $--spacing: var(--spacing-s);
.indent { .indent {
@ -82,5 +98,4 @@ $--spacing: var(--spacing-s);
margin-left: $--spacing; margin-left: $--spacing;
max-width: 300px; max-width: 300px;
} }
</style> </style>

View file

@ -7,7 +7,7 @@ describe('components', () => {
const wrapper = render(N8nTree, { const wrapper = render(N8nTree, {
props: { props: {
value: { value: {
"hello": "world", hello: 'world',
}, },
}, },
}); });
@ -18,13 +18,10 @@ describe('components', () => {
const wrapper = render(N8nTree, { const wrapper = render(N8nTree, {
props: { props: {
value: { value: {
"hello": { hello: {
"test": "world", test: 'world',
}, },
"options": [ options: ['yes', 'no'],
"yes",
"no",
],
}, },
}, },
}); });
@ -35,37 +32,30 @@ describe('components', () => {
const wrapper = render(N8nTree, { const wrapper = render(N8nTree, {
props: { props: {
value: { value: {
"hello": { hello: {
"test": "world", test: 'world',
}, },
"options": [ options: ['yes', 'no'],
"yes",
"no",
],
}, },
}, },
slots: { slots: {
label: "<span>label</span>", label: '<span>label</span>',
value: "<span>value</span>", value: '<span>value</span>',
}, },
}); });
expect(wrapper.html()).toMatchSnapshot(); expect(wrapper.html()).toMatchSnapshot();
}); });
it('should render each tree with node class', () => { it('should render each tree with node class', () => {
const wrapper = render(N8nTree, { const wrapper = render(N8nTree, {
props: { props: {
value: { value: {
"hello": { hello: {
"test": "world", test: 'world',
}, },
"options": [ options: ['yes', 'no'],
"yes",
"no",
],
}, },
nodeClass: "nodeClass", nodeClass: 'nodeClass',
}, },
}); });
expect(wrapper.html()).toMatchSnapshot(); expect(wrapper.html()).toMatchSnapshot();

View file

@ -13,8 +13,7 @@ const Template = (args, { argTypes }) => ({
components: { components: {
N8nUserInfo, N8nUserInfo,
}, },
template: template: '<n8n-user-info v-bind="$props" />',
'<n8n-user-info v-bind="$props" />',
}); });
export const Member = Template.bind({}); export const Member = Template.bind({});

View file

@ -5,23 +5,25 @@
</div> </div>
<div v-if="isPendingUser" :class="$style.pendingUser"> <div v-if="isPendingUser" :class="$style.pendingUser">
<n8n-text :bold="true">{{email}}</n8n-text> <n8n-text :bold="true">{{ email }}</n8n-text>
<span :class="$style.pendingBadge"><n8n-badge :bold="true">Pending</n8n-badge></span> <span :class="$style.pendingBadge"><n8n-badge :bold="true">Pending</n8n-badge></span>
</div> </div>
<div v-else :class="$style.infoContainer"> <div v-else :class="$style.infoContainer">
<div> <div>
<n8n-text :bold="true" color="text-dark">{{firstName}} {{lastName}} {{isCurrentUser ? this.t('nds.userInfo.you') : ''}}</n8n-text> <n8n-text :bold="true" color="text-dark"
>{{ firstName }} {{ lastName }}
{{ isCurrentUser ? this.t('nds.userInfo.you') : '' }}</n8n-text
>
</div> </div>
<div> <div>
<n8n-text size="small" color="text-light">{{email}}</n8n-text> <n8n-text size="small" color="text-light">{{ email }}</n8n-text>
</div> </div>
</div> </div>
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts">
import Vue from 'vue'; import 'vue';
import N8nText from '../N8nText'; import N8nText from '../N8nText';
import N8nAvatar from '../N8nAvatar'; import N8nAvatar from '../N8nAvatar';
import N8nBadge from '../N8nBadge'; import N8nBadge from '../N8nBadge';
@ -67,7 +69,6 @@ export default mixins(Locale).extend({
}); });
</script> </script>
<style lang="scss" module> <style lang="scss" module>
.container { .container {
display: inline-flex; display: inline-flex;
@ -84,7 +85,7 @@ export default mixins(Locale).extend({
.infoContainer { .infoContainer {
flex-grow: 1; flex-grow: 1;
display: inline-flex; display: inline-flex;
flex-direction: column;; flex-direction: column;
justify-content: center; justify-content: center;
margin-left: var(--spacing-xs); margin-left: var(--spacing-xs);
} }

View file

@ -4,8 +4,7 @@ import { action } from '@storybook/addon-actions';
export default { export default {
title: 'Modules/UserSelect', title: 'Modules/UserSelect',
component: N8nUserSelect, component: N8nUserSelect,
argTypes: { argTypes: {},
},
parameters: { parameters: {
backgrounds: { default: '--color-background-light' }, backgrounds: { default: '--color-background-light' },
}, },
@ -36,34 +35,34 @@ export const UserSelect = Template.bind({});
UserSelect.args = { UserSelect.args = {
users: [ users: [
{ {
id: "1", id: '1',
firstName: 'Sunny', firstName: 'Sunny',
lastName: 'Side', lastName: 'Side',
email: "sunny@n8n.io", email: 'sunny@n8n.io',
globalRole: { globalRole: {
name: 'owner', name: 'owner',
id: "1", id: '1',
}, },
}, },
{ {
id: "2", id: '2',
firstName: 'Kobi', firstName: 'Kobi',
lastName: 'Dog', lastName: 'Dog',
email: "kobi@n8n.io", email: 'kobi@n8n.io',
globalRole: { globalRole: {
name: 'member', name: 'member',
id: "2", id: '2',
}, },
}, },
{ {
id: "3", id: '3',
email: "invited@n8n.io", email: 'invited@n8n.io',
globalRole: { globalRole: {
name: 'member', name: 'member',
id: "2", id: '2',
}, },
}, },
], ],
placeholder: 'Select user to transfer to', placeholder: 'Select user to transfer to',
currentUserId: "1", currentUserId: '1',
}; };

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