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]
indent_style = space
indent_size = 2
[*.ts]
quote_type = single

View file

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

View file

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

View file

@ -14,10 +14,15 @@ module.exports = {
parser: 'vue-eslint-parser',
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: {
'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: {
project: ['./tsconfig.json'],
tsconfigRootDir: __dirname,
extraFileExtensions: ['.vue'],
},
rules: {
// TODO: Remove these
'import/no-default-export': 'off',
'import/no-extraneous-dependencies': 'off',
'import/order': 'off',
'prettier/prettier': 'off',
'@typescript-eslint/member-delimiter-style': 'off',
'@typescript-eslint/naming-convention': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-unsafe-argument': 'off',
'@typescript-eslint/no-unsafe-return': 'off',
'@typescript-eslint/no-unused-vars': 'off',
'@typescript-eslint/prefer-nullish-coalescing': 'off',
'@typescript-eslint/prefer-optional-chain': 'off',
'@typescript-eslint/restrict-template-expressions': 'off',
'@typescript-eslint/ban-ts-comment': ['warn', { 'ts-ignore': true }],
}
'@typescript-eslint/no-explicit-any': 'warn',
'@typescript-eslint/no-unsafe-argument': 'warn',
'@typescript-eslint/no-unsafe-return': 'warn',
'@typescript-eslint/no-unsafe-member-access': 'warn',
},
overrides: [
{
files: ['src/**/*.stories.{js,ts}'],
rules: {
'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

View file

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

View file

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

View file

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

View file

@ -21,15 +21,22 @@
"test:dev": "vitest",
"build:storybook": "build-storybook",
"storybook": "start-storybook -p 6006",
"format": "prettier **/**.{ts,vue} --write",
"lint": "tslint -p tsconfig.json -c tslint.json && eslint .",
"lintfix": "tslint --fix -p tsconfig.json -c tslint.json && eslint . --fix"
"format": "prettier **/**.{js,ts,vue,css,scss,mdx,html} --write .",
"lint": "eslint --ext .js,.ts,.vue src",
"lintfix": "eslint --ext .js,.ts,.vue src --fix"
},
"peerDependencies": {
"@fortawesome/fontawesome-svg-core": "1.x",
"@fortawesome/free-solid-svg-icons": "5.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": {
"@fortawesome/fontawesome-svg-core": "^1.2.35",
@ -43,14 +50,12 @@
"@testing-library/jest-dom": "^5.16.4",
"@testing-library/vue": "^5.8.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",
"c8": "7.11.0",
"core-js": "^3.6.5",
"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",
"sass": "^1.55.0",
"sass-loader": "^10.1.1",
@ -60,16 +65,13 @@
"vite": "^2.9.5",
"@vitejs/plugin-vue2": "^1.1.2",
"vitest": "^0.9.3",
"vue": "^2.7",
"vue-class-component": "^7.2.3",
"vue-loader": "^15.9.7",
"vue-property-decorator": "^9.1.2",
"vue-template-compiler": "^2.7",
"vue-tsc": "^0.34.8",
"vue-typed-mixins": "^0.2.0",
"vue2-boring-avatars": "0.3.4",
"webpack": "^4.46.0",
"xss": "^1.0.10"
"webpack": "^4.46.0"
},
"dependencies": {
"element-ui": "~2.15.7",

View file

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

View file

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

View file

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

View file

@ -1,8 +1,13 @@
<template>
<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">
<n8n-icon :icon="activatorIcon"/>
<n8n-icon :icon="activatorIcon" />
</div>
<el-dropdown-menu slot="dropdown" :class="$style.userActionsMenu">
<el-dropdown-item
@ -12,13 +17,15 @@
:disabled="item.disabled"
:divided="item.divided"
>
<div :class="{
[$style.itemContainer]: true,
[$style.hasCustomStyling]: item.customClass !== undefined,
[item.customClass]: item.customClass !== undefined,
}">
<div
:class="{
[$style.itemContainer]: true,
[$style.hasCustomStyling]: item.customClass !== undefined,
[item.customClass]: item.customClass !== undefined,
}"
>
<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 :class="$style.label">
{{ item.label }}
@ -31,7 +38,7 @@
</template>
<script lang="ts">
import Vue, { PropType } from "vue";
import Vue, { PropType } from 'vue';
import ElDropdown from 'element-ui/lib/dropdown';
import ElDropdownMenu from 'element-ui/lib/dropdown-menu';
import ElDropdownItem from 'element-ui/lib/dropdown-item';
@ -56,8 +63,8 @@ export default Vue.extend({
name: 'n8n-action-dropdown',
components: {
ElDropdownMenu, // eslint-disable-line @typescript-eslint/no-unsafe-assignment
ElDropdown, // eslint-disable-line @typescript-eslint/no-unsafe-assignment
ElDropdownItem, // eslint-disable-line @typescript-eslint/no-unsafe-assignment
ElDropdown, // eslint-disable-line @typescript-eslint/no-unsafe-assignment
ElDropdownItem, // eslint-disable-line @typescript-eslint/no-unsafe-assignment
N8nIcon,
},
props: {
@ -78,22 +85,22 @@ export default Vue.extend({
iconSize: {
type: String,
default: 'medium',
validator: (value: string): boolean =>
['small', 'medium', 'large'].includes(value),
validator: (value: string): boolean => ['small', 'medium', 'large'].includes(value),
},
trigger: {
type: String,
default: 'click',
validator: (value: string): boolean =>
['click', 'hover'].includes(value),
validator: (value: string): boolean => ['click', 'hover'].includes(value),
},
},
methods: {
onSelect(action: string) : void {
onSelect(action: string): void {
this.$emit('select', action);
},
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
if (elementDropdown && event.relatedTarget === null) {
elementDropdown.hide();
@ -101,11 +108,9 @@ export default Vue.extend({
},
},
});
</script>
<style lang="scss" module>
.activator {
cursor: pointer;
padding: var(--spacing-2xs);
@ -131,7 +136,9 @@ export default Vue.extend({
text-align: center;
margin-right: var(--spacing-2xs);
svg { width: 1.2em !important; }
svg {
width: 1.2em !important;
}
}
:global(li.is-disabled) {
@ -139,5 +146,4 @@ export default Vue.extend({
color: inherit !important;
}
}
</style>

View file

@ -28,7 +28,8 @@ const Template = (args, { argTypes }) => ({
components: {
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,
});

View file

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

View file

@ -1,5 +1,5 @@
<template>
<span :class="['n8n-avatar', $style.container]" v-on="$listeners">
<span :class="['n8n-avatar', $style.container]" v-on="$listeners">
<avatar
v-if="firstName"
:size="getSize(size)"
@ -7,19 +7,15 @@
variant="marble"
:colors="getColors(colors)"
/>
<div
v-else
:class="[$style.empty, $style[size]]"
>
</div>
<span v-if="firstName" :class="$style.initials">{{initials}}</span>
<div v-else :class="[$style.empty, $style[size]]"></div>
<span v-if="firstName" :class="$style.initials">{{ initials }}</span>
</span>
</template>
<script lang="ts">
import Avatar from 'vue2-boring-avatars';
const sizes: {[size: string]: number} = {
const sizes: { [size: string]: number } = {
small: 28,
large: 48,
medium: 40,
@ -41,7 +37,13 @@ export default Vue.extend({
default: 'medium',
},
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: {
@ -49,7 +51,10 @@ export default Vue.extend({
},
computed: {
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: {
@ -75,7 +80,7 @@ export default Vue.extend({
.empty {
border-radius: 50%;
background-color: var(--color-foreground-dark);
opacity: .3;
opacity: 0.3;
}
.initials {

View file

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

View file

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

View file

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

View file

@ -1,40 +1,45 @@
<template>
<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>
</template>
<script lang="ts" setup>
type BlockUiProps = {
show: boolean;
}
};
const props = withDefaults(defineProps<BlockUiProps>(), {
withDefaults(defineProps<BlockUiProps>(), {
show: false,
});
</script>
<style lang="scss" module>
.uiBlocker {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: var(--color-background-dark);
z-index: 10;
opacity: 0.6;
border-radius: var(--border-radius-large);
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: var(--color-background-dark);
z-index: 10;
opacity: 0.6;
border-radius: var(--border-radius-large);
}
</style>
<style lang="scss" scoped>
.fade-enter-active,
.fade-leave-active {
transition: opacity 200ms;
transition: opacity 200ms;
}
.fade-enter,
.fade-leave-to {
opacity: 0;
opacity: 0;
}
</style>

View file

@ -1,7 +1,6 @@
/* tslint:disable:variable-name */
import N8nButton from './Button.vue';
import { action } from '@storybook/addon-actions';
import { StoryFn } from "@storybook/vue";
import type { StoryFn } from '@storybook/vue';
export default {
title: 'Atoms/Button',
@ -63,22 +62,6 @@ const AllSizesTemplate: StoryFn = (args, { argTypes }) => ({
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 }) => ({
props: Object.keys(argTypes),
components: {
@ -170,4 +153,3 @@ Square.args = {
label: '48',
square: true,
};

View file

@ -8,15 +8,8 @@
v-on="$listeners"
>
<span :class="$style.icon" v-if="loading || icon">
<n8n-spinner
v-if="loading"
:size="size"
/>
<n8n-icon
v-else-if="icon"
:icon="icon"
:size="size"
/>
<n8n-spinner v-if="loading" :size="size" />
<n8n-icon v-else-if="icon" :icon="icon" :size="size" />
</span>
<span v-if="label || $slots.default">
<slot>{{ label }}</slot>
@ -76,13 +69,12 @@ export default Vue.extend({
},
float: {
type: String,
validator: (value: string): boolean =>
['left', 'right'].includes(value),
validator: (value: string): boolean => ['left', 'right'].includes(value),
},
square: {
type: Boolean,
default: false,
},
square: {
type: Boolean,
default: false,
},
},
components: {
N8nSpinner,
@ -96,7 +88,8 @@ export default Vue.extend({
return this.disabled ? 'true' : 'false';
},
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.outline ? ` ${this.$style.outline}` : ''}` +
`${this.loading ? ` ${this.$style.loading}` : ''}` +
@ -106,7 +99,8 @@ export default Vue.extend({
`${this.block ? ` ${this.$style.block}` : ''}` +
`${this.active ? ` ${this.$style.active}` : ''}` +
`${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;
}
&:active, &.active {
&:active,
&.active {
color: $button-active-color;
border-color: $button-active-border-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-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 {
@ -227,7 +227,12 @@ $loading-overlay-background-color: rgba(255, 255, 255, 0);
--button-hover-background-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 {
@ -241,7 +246,12 @@ $loading-overlay-background-color: rgba(255, 255, 255, 0);
--button-hover-background-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 {
@ -256,7 +266,12 @@ $loading-overlay-background-color: rgba(255, 255, 255, 0);
--button-hover-background-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
);
}
/**
@ -440,7 +455,7 @@ $loading-overlay-background-color: rgba(255, 255, 255, 0);
.icon {
display: inline-flex;
justify-content: center;
justify-content: center;
svg {
display: block;

View file

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

View file

@ -1,17 +1,17 @@
// 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`] = `
"<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>"
`;
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>"
`;

View file

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

View file

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

View file

@ -1,8 +1,7 @@
import N8nCallout from './Callout.vue';
import N8nLink from '../N8nLink';
import N8nText from '../N8nText';
import { StoryFn } from '@storybook/vue';
import type { StoryFn } from '@storybook/vue';
export default {
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),
components: {
N8nLink,
@ -79,7 +86,6 @@ customCallout.args = {
`,
};
export const secondaryCallout = template.bind({});
secondaryCallout.args = {
theme: 'secondary',

View file

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

View file

@ -8,7 +8,7 @@ describe('components', () => {
props: {
theme: 'info',
},
stubs: [ 'n8n-icon', 'n8n-text' ],
stubs: ['n8n-icon', 'n8n-text'],
slots: {
default: '<n8n-text size="small">This is an info callout.</n8n-text>',
},
@ -20,7 +20,7 @@ describe('components', () => {
props: {
theme: 'success',
},
stubs: [ 'n8n-icon', 'n8n-text' ],
stubs: ['n8n-icon', 'n8n-text'],
slots: {
default: '<n8n-text size="small">This is a success callout.</n8n-text>',
},
@ -32,7 +32,7 @@ describe('components', () => {
props: {
theme: 'warning',
},
stubs: [ 'n8n-icon', 'n8n-text' ],
stubs: ['n8n-icon', 'n8n-text'],
slots: {
default: '<n8n-text size="small">This is a warning callout.</n8n-text>',
},
@ -44,7 +44,7 @@ describe('components', () => {
props: {
theme: 'danger',
},
stubs: [ 'n8n-icon', 'n8n-text' ],
stubs: ['n8n-icon', 'n8n-text'],
slots: {
default: '<n8n-text size="small">This is a danger callout.</n8n-text>',
},
@ -56,7 +56,7 @@ describe('components', () => {
props: {
theme: 'secondary',
},
stubs: [ 'n8n-icon', 'n8n-text' ],
stubs: ['n8n-icon', 'n8n-text'],
slots: {
default: '<n8n-text size="small">This is a secondary callout.</n8n-text>',
},
@ -69,7 +69,7 @@ describe('components', () => {
theme: 'custom',
icon: 'code-branch',
},
stubs: [ 'n8n-icon', 'n8n-text' ],
stubs: ['n8n-icon', 'n8n-text'],
slots: {
default: '<n8n-text size="small">This is a secondary callout.</n8n-text>',
},
@ -82,11 +82,12 @@ describe('components', () => {
theme: 'custom',
icon: 'code-branch',
},
stubs: [ 'n8n-icon', 'n8n-text', 'n8n-link' ],
stubs: ['n8n-icon', 'n8n-text', 'n8n-link'],
slots: {
default: '<n8n-text size="small">This is a secondary callout.</n8n-text>',
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();

View file

@ -1,9 +1,9 @@
// Vitest Snapshot v1
exports[`components > N8nCallout > should render additional slots correctly 1`] = `
"<div role=\\"alert\\" class=\\"n8n-callout _callout_p74de_1 _custom_p74de_16\\">
<div class=\\"_message-section_p74de_12\\">
<div class=\\"_icon_p74de_40\\">
"<div role=\\"alert\\" class=\\"n8n-callout _callout_dfd91_1 _custom_dfd91_17\\">
<div class=\\"_message-section_dfd91_12\\">
<div class=\\"_icon_dfd91_41\\">
<n8n-icon-stub icon=\\"code-branch\\" size=\\"large\\"></n8n-icon-stub>
</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>
@ -13,9 +13,9 @@ exports[`components > N8nCallout > should render additional slots correctly 1`]
`;
exports[`components > N8nCallout > should render custom theme correctly 1`] = `
"<div role=\\"alert\\" class=\\"n8n-callout _callout_p74de_1 _custom_p74de_16\\">
<div class=\\"_message-section_p74de_12\\">
<div class=\\"_icon_p74de_40\\">
"<div role=\\"alert\\" class=\\"n8n-callout _callout_dfd91_1 _custom_dfd91_17\\">
<div class=\\"_message-section_dfd91_12\\">
<div class=\\"_icon_dfd91_41\\">
<n8n-icon-stub icon=\\"code-branch\\" size=\\"large\\"></n8n-icon-stub>
</div>
<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`] = `
"<div role=\\"alert\\" class=\\"n8n-callout _callout_p74de_1 _danger_p74de_34\\">
<div class=\\"_message-section_p74de_12\\">
<div class=\\"_icon_p74de_40\\">
"<div role=\\"alert\\" class=\\"n8n-callout _callout_dfd91_1 _danger_dfd91_35\\">
<div class=\\"_message-section_dfd91_12\\">
<div class=\\"_icon_dfd91_41\\">
<n8n-icon-stub icon=\\"times-circle\\" size=\\"large\\"></n8n-icon-stub>
</div>
<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`] = `
"<div role=\\"alert\\" class=\\"n8n-callout _callout_p74de_1 _info_p74de_16\\">
<div class=\\"_message-section_p74de_12\\">
<div class=\\"_icon_p74de_40\\">
"<div role=\\"alert\\" class=\\"n8n-callout _callout_dfd91_1 _info_dfd91_16\\">
<div class=\\"_message-section_dfd91_12\\">
<div class=\\"_icon_dfd91_41\\">
<n8n-icon-stub icon=\\"info-circle\\" size=\\"large\\"></n8n-icon-stub>
</div>
<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`] = `
"<div role=\\"alert\\" class=\\"n8n-callout _callout_p74de_1 _secondary_p74de_44\\">
<div class=\\"_message-section_p74de_12\\">
<div class=\\"_icon_p74de_40\\">
"<div role=\\"alert\\" class=\\"n8n-callout _callout_dfd91_1 _secondary_dfd91_45\\">
<div class=\\"_message-section_dfd91_12\\">
<div class=\\"_icon_dfd91_41\\">
<n8n-icon-stub icon=\\"info-circle\\" size=\\"medium\\"></n8n-icon-stub>
</div>
<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`] = `
"<div role=\\"alert\\" class=\\"n8n-callout _callout_p74de_1 _success_p74de_28\\">
<div class=\\"_message-section_p74de_12\\">
<div class=\\"_icon_p74de_40\\">
"<div role=\\"alert\\" class=\\"n8n-callout _callout_dfd91_1 _success_dfd91_29\\">
<div class=\\"_message-section_dfd91_12\\">
<div class=\\"_icon_dfd91_41\\">
<n8n-icon-stub icon=\\"check-circle\\" size=\\"large\\"></n8n-icon-stub>
</div>
<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`] = `
"<div role=\\"alert\\" class=\\"n8n-callout _callout_p74de_1 _warning_p74de_22\\">
<div class=\\"_message-section_p74de_12\\">
<div class=\\"_icon_p74de_40\\">
"<div role=\\"alert\\" class=\\"n8n-callout _callout_dfd91_1 _warning_dfd91_23\\">
<div class=\\"_message-section_dfd91_12\\">
<div class=\\"_icon_dfd91_41\\">
<n8n-icon-stub icon=\\"exclamation-triangle\\" size=\\"large\\"></n8n-icon-stub>
</div>
<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 {StoryFn} from "@storybook/vue";
import N8nButton from "../N8nButton/Button.vue";
import N8nIcon from "../N8nIcon/Icon.vue";
import N8nText from "../N8nText/Text.vue";
import type { StoryFn } from '@storybook/vue';
import N8nButton from '../N8nButton/Button.vue';
import N8nIcon from '../N8nIcon/Icon.vue';
import N8nText from '../N8nText/Text.vue';
export default {
title: 'Atoms/Card',
component: N8nCard,
};
export const Default: StoryFn = (args, {argTypes}) => ({
export const Default: StoryFn = (args, { argTypes }) => ({
props: Object.keys(argTypes),
components: {
N8nCard,
@ -19,7 +17,7 @@ export const Default: StoryFn = (args, {argTypes}) => ({
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),
components: {
N8nCard,
@ -38,8 +36,7 @@ Hoverable.args = {
hoverable: true,
};
export const WithSlots: StoryFn = (args, {argTypes}) => ({
export const WithSlots: StoryFn = (args, { argTypes }) => ({
props: Object.keys(argTypes),
components: {
N8nCard,

View file

@ -1,21 +1,21 @@
<template>
<div :class="classes" v-on="$listeners">
<div :class="$style.icon" v-if="$slots.prepend">
<slot name="prepend"/>
<slot name="prepend" />
</div>
<div :class="$style.content">
<div :class="$style.header" v-if="$slots.header">
<slot name="header"/>
<slot name="header" />
</div>
<div :class="$style.body" v-if="$slots.default">
<slot/>
<slot />
</div>
<div :class="$style.footer" v-if="$slots.footer">
<slot name="footer"/>
<slot name="footer" />
</div>
</div>
<div :class="$style.actions" v-if="$slots.append">
<slot name="append"/>
<slot name="append" />
</div>
</div>
</template>
@ -90,15 +90,15 @@ export default Vue.extend({
}
.hoverable {
cursor: pointer;
transition-property: border, color;
transition-duration: 0.3s;
transition-timing-function: ease;
cursor: pointer;
transition-property: border, color;
transition-duration: 0.3s;
transition-timing-function: ease;
&:hover,
&:focus {
color: var(--color-primary);
border-color: var(--color-primary);
}
&:hover,
&:focus {
color: var(--color-primary);
border-color: var(--color-primary);
}
}
</style>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -71,12 +71,12 @@ FormInputs.args = {
{
name: 'agree',
properties: {
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 ❤️',
labelSize: 'small',
tooltipText: 'Check this if you agree to be contacted by our marketing team'
}
}
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 ❤️',
labelSize: 'small',
tooltipText: 'Check this if you agree to be contacted by our marketing team',
},
},
],
};

View file

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

View file

@ -21,11 +21,15 @@ export default Vue.extend({
size: {
type: String,
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: {
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: {
type: String,
@ -36,15 +40,17 @@ export default Vue.extend({
classes() {
const applied = [];
if (this.align) {
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
applied.push(`align-${this.align}`);
}
if (this.color) {
applied.push(this.color);
}
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
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]);
},
@ -121,5 +127,4 @@ export default Vue.extend({
.align-center {
text-align: center;
}
</style>

View file

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

View file

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

View file

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

View file

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

View file

@ -1,25 +1,41 @@
<template>
<div :class="['accordion', $style.container]" >
<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-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 :class="['accordion', $style.container]">
<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-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 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 -->
<div v-if="items.length > 0" :class="$style.accordionItems">
<div v-for="item in items" :key="item.id" :class="$style.accordionItem">
<n8n-tooltip :disabled="!item.tooltip">
<div 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"/>
<div
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-text size="small" color="text-base">{{ item.label }}</n8n-text>
</div>
</div>
</div>
<n8n-text color="text-base" size="small" align="left">
<span v-html="description"></span>
</n8n-text>
<slot name="customContent"></slot>
<slot name="customContent"></slot>
</div>
</div>
</template>
@ -59,7 +75,7 @@ export default Vue.extend({
default: false,
},
headerIcon: {
type: Object as () => { icon: string, color: string },
type: Object as PropType<{ icon: string; color: string }>,
required: false,
},
},
@ -128,5 +144,4 @@ export default Vue.extend({
font-weight: var(--font-weight-bold);
}
}
</style>

View file

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

View file

@ -1,20 +1,27 @@
<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
v-if="type === 'tooltip'"
:placement="tooltipPlacement"
:popper-class="$style.tooltipPopper"
:disabled="type !== 'tooltip'"
v-if="type === 'tooltip'"
:placement="tooltipPlacement"
:popper-class="$style.tooltipPopper"
:disabled="type !== 'tooltip'"
>
<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 slot="content">
<slot />
</span>
</n8n-tooltip>
<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>
<slot />
</span>
@ -44,8 +51,7 @@ export default Vue.extend({
type: {
type: String,
default: 'note',
validator: (value: string): boolean =>
['note', 'tooltip'].includes(value),
validator: (value: string): boolean => ['note', 'tooltip'].includes(value),
},
bold: {
type: Boolean,
@ -91,7 +97,7 @@ export default Vue.extend({
.iconText {
display: inline-flex;
align-items: flex-start;
align-items: flex-start;
}
.info-light {

View file

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

View file

@ -1,8 +1,7 @@
/* tslint:disable:variable-name */
import N8nInput from './Input.vue';
import N8nIcon from '../N8nIcon';
import { action } from '@storybook/addon-actions';
import { StoryFn } from '@storybook/vue';
import type { StoryFn } from '@storybook/vue';
export default {
title: 'Atoms/Input',
@ -41,7 +40,8 @@ const Template: StoryFn = (args, { argTypes }) => ({
components: {
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() {
return {
val: '',
@ -82,14 +82,14 @@ TextArea.args = {
placeholder: 'placeholder...',
};
const WithPrefix: StoryFn = (args, { argTypes }) => ({
props: Object.keys(argTypes),
components: {
N8nIcon,
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() {
return {
val: '',
@ -109,7 +109,8 @@ const WithSuffix: StoryFn = (args, { argTypes }) => ({
N8nIcon,
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() {
return {
val: '',

View file

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

View file

@ -1,5 +1,5 @@
import {render} from '@testing-library/vue';
import N8nInput from "../Input.vue";
import { render } from '@testing-library/vue';
import N8nInput from '../Input.vue';
describe('N8nInput', () => {
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>
</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-icon icon="question-circle" size="small" />
<div slot="content" v-html="addTargetBlank(tooltipText)" />
</n8n-tooltip>
</span>
<div v-if="$slots.options && label" :class="{[$style.overlay]: true, [$style.visible]: showOptions}" />
<div v-if="$slots.options" :class="{[$style.options]: true, [$style.visible]: showOptions}">
<slot name="options"/>
<div
v-if="$slots.options && label"
:class="{ [$style.overlay]: true, [$style.visible]: showOptions }"
/>
<div v-if="$slots.options" :class="{ [$style.options]: true, [$style.visible]: showOptions }">
<slot name="options" />
</div>
</label>
<slot />
@ -71,8 +77,7 @@ export default Vue.extend({
size: {
type: String,
default: 'medium',
validator: (value: string): boolean =>
['small', 'medium'].includes(value),
validator: (value: string): boolean => ['small', 'medium'].includes(value),
},
underline: {
type: Boolean,
@ -98,7 +103,8 @@ export default Vue.extend({
.inputLabel {
display: block;
}
.container:hover,.inputLabel:hover {
.container:hover,
.inputLabel:hover {
.infoIcon {
opacity: 1;
}
@ -136,7 +142,7 @@ export default Vue.extend({
.options {
opacity: 0;
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;
@ -147,7 +153,7 @@ export default Vue.extend({
position: relative;
flex-grow: 1;
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 {
position: absolute;
@ -157,7 +163,11 @@ export default Vue.extend({
right: 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);
}
}
</style>

View file

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

View file

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

View file

@ -1,40 +1,36 @@
<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">
<div v-if="variant === 'h1'">
<div
v-for="(item, index) in rows"
:key="index"
:class="{
v-for="(item, index) in rows"
:key="index"
:class="{
[$style.h1Last]: item === rows && rows > 1 && shrinkLast,
}"
>
<el-skeleton-item
:variant="variant"
/>
<el-skeleton-item :variant="variant" />
</div>
</div>
<div v-else-if="variant === 'p'">
<div
v-for="(item, index) in rows"
:key="index"
:class="{
v-for="(item, index) in rows"
:key="index"
:class="{
[$style.pLast]: item === rows && rows > 1 && shrinkLast,
}">
<el-skeleton-item
:variant="variant"
/>
}"
>
<el-skeleton-item :variant="variant" />
</div>
</div>
<div :class="$style.custom" v-else-if="variant === 'custom'">
<el-skeleton-item
:variant="variant"
/>
<el-skeleton-item :variant="variant" />
</div>
<el-skeleton-item
v-else
:variant="variant"
/>
<el-skeleton-item v-else :variant="variant" />
</template>
</el-skeleton>
</template>
@ -71,7 +67,20 @@ export default Vue.extend({
variant: {
type: String,
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),
},
},
});
@ -79,23 +88,23 @@ export default Vue.extend({
<style lang="scss" module>
.h1Last {
width: 40%;
width: 40%;
}
.pLast {
width: 61%;
width: 61%;
}
.custom {
width: 100%;
height: 100%;
width: 100%;
height: 100%;
}
</style>
<style lang="scss">
.n8n-loading-custom .el-skeleton {
&,
.el-skeleton__item {
width: 100%;
height: 100%;
}
&,
.el-skeleton__item {
width: 100%;
height: 100%;
}
}
</style>

View file

@ -41,8 +41,10 @@ export const Markdown = Template.bind({});
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`,
loading: false,
images: [{
id: 1,
url: 'https://community.n8n.io/uploads/default/optimized/2X/b/b737a95de4dfe0825d50ca098171e9f33a459e74_2_690x288.png',
}],
images: [
{
id: 1,
url: 'https://community.n8n.io/uploads/default/optimized/2X/b/b737a95de4dfe0825d50ca098171e9f33a459e74_2_690x288.png',
},
],
};

View file

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

View file

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

View file

@ -1,22 +1,20 @@
<template>
<div :class="{
['menu-container']: true,
[$style.container]: true,
[$style.menuCollapsed]: collapsed
}">
<div
:class="{
['menu-container']: true,
[$style.container]: true,
[$style.menuCollapsed]: collapsed,
}"
>
<div v-if="$slots.header" :class="$style.menuHeader">
<slot name="header"></slot>
</div>
<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">
<slot name="menuPrefix"></slot>
</div>
<el-menu
:defaultActive="defaultActive"
:collapse="collapsed"
v-on="$listeners"
>
<el-menu :defaultActive="defaultActive" :collapse="collapsed" v-on="$listeners">
<n8n-menu-item
v-for="item in upperMenuItems"
:key="item.id"
@ -30,11 +28,7 @@
</el-menu>
</div>
<div :class="[$style.lowerContent, 'pb-2xs']">
<el-menu
:defaultActive="defaultActive"
:collapse="collapsed"
v-on="$listeners"
>
<el-menu :defaultActive="defaultActive" :collapse="collapsed" v-on="$listeners">
<n8n-menu-item
v-for="item in lowerMenuItems"
:key="item.id"
@ -62,7 +56,6 @@ import ElMenu from 'element-ui/lib/menu';
import N8nMenuItem from '../N8nMenuItem';
import Vue, { PropType } from 'vue';
import { Route } from 'vue-router';
import { IMenuItem } from '../../types';
export default Vue.extend({
@ -108,23 +101,31 @@ export default Vue.extend({
},
mounted() {
if (this.mode === 'router') {
const found = this.items.find(item => {
return Array.isArray(item.activateOnRouteNames) && item.activateOnRouteNames.includes(this.$route.name || '') ||
Array.isArray(item.activateOnRoutePaths) && item.activateOnRoutePaths.includes(this.$route.path);
const found = this.items.find((item) => {
return (
(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 : '';
} else {
this.activeTab = this.items.length > 0 ? this.items[0].id : '';
this.activeTab = this.items.length > 0 ? this.items[0].id : '';
}
this.$emit('input', this.activeTab);
},
computed: {
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[] {
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: {
@ -178,11 +179,13 @@ export default Vue.extend({
.menuCollapsed {
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);
}
</style>

View file

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

View file

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

View file

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

View file

@ -1,7 +1,5 @@
/* tslint:disable:variable-name */
import N8nNotice from './Notice.vue';
import {StoryFn} from "@storybook/vue";
import type { StoryFn } from '@storybook/vue';
export default {
title: 'Atoms/Notice',
@ -14,7 +12,7 @@ export default {
},
};
const SlotTemplate: StoryFn = (args, {argTypes}) => ({
const SlotTemplate: StoryFn = (args, { argTypes }) => ({
props: Object.keys(argTypes),
components: {
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>`,
});
const PropTemplate: StoryFn = (args, {argTypes}) => ({
const PropTemplate: StoryFn = (args, { argTypes }) => ({
props: Object.keys(argTypes),
components: {
N8nNotice,
@ -53,19 +51,22 @@ Info.args = {
export const Sanitized = PropTemplate.bind({});
Sanitized.args = {
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({});
Truncated.args = {
theme: 'warning',
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({});
HtmlEdgeCase.args = {
theme: 'warning',
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>
<div :id="id" :class="classes" role="alert" @click=onClick>
<div :id="id" :class="classes" role="alert" @click="onClick">
<div class="notice-content">
<n8n-text size="small" :compact="true">
<slot>
@ -18,16 +18,14 @@
<script lang="ts">
import Vue from 'vue';
import sanitizeHtml from 'sanitize-html';
import N8nText from "../../components/N8nText";
import Locale from "../../mixins/locale";
import { uid } from "../../utils";
import N8nText from '../../components/N8nText';
import Locale from '../../mixins/locale';
import { uid } from '../../utils';
export default Vue.extend({
name: 'n8n-notice',
directives: {},
mixins: [
Locale,
],
mixins: [Locale],
props: {
id: {
type: String,
@ -56,11 +54,7 @@ export default Vue.extend({
},
computed: {
classes(): string[] {
return [
'notice',
this.$style.notice,
this.$style[this.theme],
];
return ['notice', this.$style.notice, this.$style[this.theme]];
},
canTruncate(): boolean {
return this.fullContent !== undefined;
@ -71,18 +65,16 @@ export default Vue.extend({
this.showFullContent = !this.showFullContent;
},
sanitizeHtml(text: string): string {
return sanitizeHtml(
text, {
allowedAttributes: { a: ['data-key', 'href', 'target'] },
},
);
return sanitizeHtml(text, {
allowedAttributes: { a: ['data-key', 'href', 'target'] },
});
},
onClick(event: MouseEvent) {
if (!(event.target instanceof HTMLElement)) return;
if (event.target.localName !== 'a') return;
if (event.target.dataset && event.target.dataset.key) {
if (event.target.dataset?.key) {
event.stopPropagation();
event.preventDefault();
@ -97,7 +89,6 @@ export default Vue.extend({
},
},
});
</script>
<style lang="scss" module>

View file

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

View file

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

View file

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

View file

@ -1,7 +1,26 @@
<template>
<label role="radio" 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
role="radio"
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>
</template>
@ -26,8 +45,7 @@ export default Vue.extend({
size: {
type: String,
default: 'medium',
validator: (value: string): boolean =>
['small', 'medium'].includes(value),
validator: (value: string): boolean => ['small', 'medium'].includes(value),
},
disabled: {
type: Boolean,

View file

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

View file

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

View file

@ -26,7 +26,8 @@ const Template = (args, { argTypes }) => ({
return {
newWidth: this.width,
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: {
@ -38,8 +39,7 @@ const Template = (args, { argTypes }) => ({
};
},
},
template:
`<div style="width: fit-content; height: fit-content">
template: `<div style="width: fit-content; height: fit-content">
<n8n-resize-wrapper
v-bind="$props"
@resize="onResize"
@ -65,13 +65,13 @@ Resize.args = {
gridSize: 20,
isResizingEnabled: true,
supportedDirections: [
"right",
"top",
"bottom",
"left",
"topLeft",
"topRight",
"bottomLeft",
"bottomRight",
'right',
'top',
'bottom',
'left',
'topLeft',
'topRight',
'bottomLeft',
'bottomRight',
],
};

View file

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

View file

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

View file

@ -1,6 +1,4 @@
/* tslint:disable:variable-name */
import { StoryFn } from '@storybook/vue';
import type { StoryFn } from '@storybook/vue';
import N8nSelect from './Select.vue';
import N8nOption from '../N8nOption';
import N8nIcon from '../N8nIcon';
@ -54,7 +52,8 @@ const Template: StoryFn = (args, { argTypes }) => ({
N8nOption,
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() {
return {
val: '',
@ -71,7 +70,12 @@ Filterable.args = {
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 }) => ({
props: Object.keys(argTypes),
@ -96,7 +100,12 @@ Sizes.args = {
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 }) => ({
props: Object.keys(argTypes),
@ -121,7 +130,6 @@ WithIcon.args = {
placeholder: 'placeholder...',
};
const LimitedWidthTemplate: StoryFn = (args, { argTypes }) => ({
props: Object.keys(argTypes),
components: {
@ -129,7 +137,8 @@ const LimitedWidthTemplate: StoryFn = (args, { argTypes }) => ({
N8nOption,
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() {
return {
val: '',

View file

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

View file

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

View file

@ -1,12 +1,12 @@
<template>
<span class="n8n-spinner">
<div v-if="type === 'ring'" class="lds-ring"><div></div><div></div><div></div><div></div></div>
<n8n-icon
v-else
icon="spinner"
:size="size"
spin
/>
<div v-if="type === 'ring'" class="lds-ring">
<div></div>
<div></div>
<div></div>
<div></div>
</div>
<n8n-icon v-else icon="spinner" :size="size" spin />
</span>
</template>
@ -23,13 +23,13 @@ export default Vue.extend({
props: {
size: {
type: String,
validator (value: string): boolean {
validator(value: string): boolean {
return ['small', 'medium', 'large'].includes(value);
},
},
type: {
type: String,
validator (value: string): boolean {
validator(value: string): boolean {
return ['dots', 'ring'].includes(value);
},
default: 'dots',
@ -40,38 +40,37 @@ export default Vue.extend({
<style lang="scss">
.lds-ring {
display: inline-block;
position: relative;
width: 48px;
height: 48px;
display: inline-block;
position: relative;
width: 48px;
height: 48px;
}
.lds-ring div {
box-sizing: border-box;
display: block;
position: absolute;
width: 48px;
height: 48px;
border: 4px solid var(--color-foreground-xlight);
border-radius: 50%;
animation: lds-ring 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite;
border-color: var(--color-primary) transparent transparent transparent;
box-sizing: border-box;
display: block;
position: absolute;
width: 48px;
height: 48px;
border: 4px solid var(--color-foreground-xlight);
border-radius: 50%;
animation: lds-ring 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite;
border-color: var(--color-primary) transparent transparent transparent;
}
.lds-ring div:nth-child(1) {
animation-delay: -0.45s;
animation-delay: -0.45s;
}
.lds-ring div:nth-child(2) {
animation-delay: -0.3s;
animation-delay: -0.3s;
}
.lds-ring div:nth-child(3) {
animation-delay: -0.15s;
animation-delay: -0.15s;
}
@keyframes lds-ring {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
</style>

View file

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

View file

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

View file

@ -7,13 +7,18 @@
<n8n-icon icon="chevron-right" size="small" />
</div>
<div ref="tabs" :class="$style.tabs">
<div v-for="option in options"
<div
v-for="option in options"
:key="option.value"
:id="option.value"
:class="{ [$style.alignRight]: option.align === 'right' }"
>
<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
v-if="option.href"
target="_blank"
@ -23,7 +28,9 @@
>
<div>
{{ 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>
</a>
@ -56,8 +63,7 @@ export default Vue.extend({
container.addEventListener('scroll', (event: Event) => {
const width = container.clientWidth;
const scrollWidth = container.scrollWidth;
// @ts-ignore
this.scrollPosition = event.srcElement.scrollLeft; // eslint-disable-line @typescript-eslint/no-unsafe-assignment
this.scrollPosition = (event.target as Element).scrollLeft;
this.canScrollRight = scrollWidth - width > this.scrollPosition;
});
@ -87,10 +93,8 @@ export default Vue.extend({
};
},
props: {
value: {
},
options: {
},
value: {},
options: {},
},
methods: {
handleTooltipClick(tab: string, event: MouseEvent) {
@ -106,7 +110,9 @@ export default Vue.extend({
this.scroll(50);
},
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) {
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>
<style lang="scss" module>
.container {
position: relative;
@ -141,8 +149,8 @@ type ScrollByFunction = (arg: { left: number, top: number, behavior: 'smooth' |
}
/* Hide scrollbar for IE, Edge and Firefox */
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
}
.tab {
@ -205,5 +213,4 @@ type ScrollByFunction = (arg: { left: number, top: number, behavior: 'smooth' |
composes: button;
right: 0;
}
</style>

View file

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

View file

@ -29,7 +29,11 @@ export default Vue.extend({
transition: background-color 0.3s ease;
&: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>

View file

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

View file

@ -1,6 +1,11 @@
<template>
<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
v-if="truncate && !showAll && hiddenTagsLength > 0"
theme="text"
@ -16,9 +21,9 @@
<script lang="ts">
import N8nTag from '../N8nTag';
import N8nLink from '../N8nLink';
import Locale from "../../mixins/locale";
import Vue, {PropType} from 'vue';
import mixins from "vue-typed-mixins";
import Locale from '../../mixins/locale';
import { PropType } from 'vue';
import mixins from 'vue-typed-mixins';
interface ITag {
id: string;

View file

@ -13,7 +13,15 @@ export default {
color: {
control: {
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: {
type: String,
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: {
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: {
type: String,
@ -40,6 +51,7 @@ export default Vue.extend({
classes() {
const applied = [];
if (this.align) {
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
applied.push(`align-${this.align}`);
}
if (this.color) {
@ -50,9 +62,10 @@ export default Vue.extend({
applied.push('compact');
}
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
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]);
},
@ -141,5 +154,4 @@ export default Vue.extend({
.align-center {
text-align: center;
}
</style>

View file

@ -1,13 +1,18 @@
<template>
<el-tooltip v-bind="$attrs">
<template v-for="(_, slotName) in $slots" #[slotName]>
<slot :name="slotName"/>
<div :key="slotName" v-if="slotName === 'content' && buttons.length" :class="$style.buttons" :style="{ justifyContent: justifyButtons }">
<slot :name="slotName" />
<div
:key="slotName"
v-if="slotName === 'content' && buttons.length"
:class="$style.buttons"
:style="{ justifyContent: justifyButtons }"
>
<n8n-button
v-for="button in buttons"
:key="button.attrs.label"
v-bind="button.attrs"
v-on="button.listeners"
v-for="button in buttons"
:key="button.attrs.label"
v-bind="button.attrs"
v-on="button.listeners"
/>
</div>
</template>
@ -15,9 +20,9 @@
</template>
<script lang="ts">
import Vue, { PropType } from "vue";
import Vue, { PropType } from 'vue';
import ElTooltip from 'element-ui/lib/tooltip';
import type { IN8nButton } from "@/types";
import type { IN8nButton } from '@/types';
import N8nButton from '../N8nButton';
export default Vue.extend({
@ -31,7 +36,19 @@ export default Vue.extend({
justifyButtons: {
type: String,
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: {
type: Array as PropType<IN8nButton[]>,

View file

@ -1,14 +1,12 @@
/* tslint:disable:variable-name */
import N8nTree from './Tree.vue';
import {StoryFn} from "@storybook/vue";
import type { StoryFn } from '@storybook/vue';
export default {
title: 'Atoms/Tree',
component: N8nTree,
};
export const Default: StoryFn = (args, {argTypes}) => ({
export const Default: StoryFn = (args, { argTypes }) => ({
props: Object.keys(argTypes),
components: {
N8nTree,
@ -26,32 +24,26 @@ export const Default: StoryFn = (args, {argTypes}) => ({
Default.args = {
value: {
objectKey: {
nestedArrayKey: [
'in progress',
33958053,
],
nestedArrayKey: ['in progress', 33958053],
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: {
myKey: 'what\'s for lunch',
myKey: "what's for lunch",
yourKey: 'prolle rewe wdyt',
},
id: 123,
},
hello: "world",
hello: 'world',
test: {
label: "A cool folder",
label: 'A cool folder',
children: [
{
label: "A cool sub-folder 1",
children: [
{ label: "A cool sub-sub-folder 1" },
{ label: "A cool sub-sub-folder 2" },
],
label: 'A cool sub-folder 1',
children: [{ 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>
<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])">
<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>:</span>
<slot v-if="$scopedSlots.value" name="value" v-bind:value="value[label]" />
<span v-else>{{ value[label] }}</span>
</div>
<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>
<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">
<slot :name="name" v-bind="data"></slot>
</template>
@ -26,11 +45,9 @@ import Vue from 'vue';
export default Vue.extend({
name: 'n8n-tree',
components: {
},
components: {},
props: {
value: {
},
value: {},
path: {
type: Array,
default: () => [],
@ -70,7 +87,6 @@ export default Vue.extend({
</script>
<style lang="scss" module>
$--spacing: var(--spacing-s);
.indent {
@ -82,5 +98,4 @@ $--spacing: var(--spacing-s);
margin-left: $--spacing;
max-width: 300px;
}
</style>

View file

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

View file

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

View file

@ -5,23 +5,25 @@
</div>
<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>
</div>
<div v-else :class="$style.infoContainer">
<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>
<n8n-text size="small" color="text-light">{{email}}</n8n-text>
<n8n-text size="small" color="text-light">{{ email }}</n8n-text>
</div>
</div>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import 'vue';
import N8nText from '../N8nText';
import N8nAvatar from '../N8nAvatar';
import N8nBadge from '../N8nBadge';
@ -67,7 +69,6 @@ export default mixins(Locale).extend({
});
</script>
<style lang="scss" module>
.container {
display: inline-flex;
@ -84,7 +85,7 @@ export default mixins(Locale).extend({
.infoContainer {
flex-grow: 1;
display: inline-flex;
flex-direction: column;;
flex-direction: column;
justify-content: center;
margin-left: var(--spacing-xs);
}
@ -101,6 +102,6 @@ export default mixins(Locale).extend({
}
.disabled {
opacity: 0.5;
opacity: 0.5;
}
</style>

View file

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