test(editor): Add first frontend unit-test and update notice component design (#3166)

*  Added basic Vue 2 + Vite.js setup.

* 🚧 Improved typescript support.

*  Added N8nNotice component to design system with stories and unit tests.

*  Migrated design system build to Vite.js.

* ♻️ Updated typescript definitions. Moved some interface types to remove reliance from design system on editor-ui user and validation types.

* ♻️ Changed prop name from type to theme. Updated truncation props.

* ♻️ Moved user response types back. Added n8n-notice component to editor-ui.

* 🐛 Fixed global vitest types.

*  Added this. vue type extension to editor-ui

* ♻️ Removed circular import.

*  Fixed failing n8n-notice tests.

* feat: Added support for notice truncation via typeOptions.

*  Updated warning color variables and notice warning colors.

* 🐛 Fixed n8n-notice parameter input spacing.
This commit is contained in:
Alex Grozav 2022-04-29 16:23:41 +03:00 committed by GitHub
parent 69d6b7827f
commit 0a69a9eb9c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
72 changed files with 17738 additions and 21353 deletions

38342
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -35,6 +35,11 @@ module.exports = {
],
});
config.resolve.alias = {
...config.resolve.alias,
"@/": path.resolve(__dirname, "../src/"),
};
return config;
},
};

View file

@ -14,12 +14,14 @@
},
"scripts": {
"build": "npm run build:theme",
"build:vue": "vue-cli-service build --target lib ./src/main.js --report",
"build:vue": "vite build",
"build:vue:typecheck": "vue-tsc --emitDeclarationOnly",
"dev": "npm run watch:theme",
"test": "npm run test:unit",
"test": "vitest run",
"test:ci": "vitest run --coverage",
"test:dev": "vitest",
"build:storybook": "build-storybook",
"storybook": "start-storybook -p 6006",
"test:unit": "vue-cli-service test:unit --passWithNoTests",
"lint": "tslint -p tsconfig.json -c tslint.json",
"lintfix": "tslint --fix -p tsconfig.json -c tslint.json",
"build:theme": "gulp build:theme",
@ -36,14 +38,15 @@
"@fortawesome/fontawesome-svg-core": "^1.2.35",
"@fortawesome/free-solid-svg-icons": "^5.15.3",
"@fortawesome/vue-fontawesome": "^2.0.2",
"core-js": "^3.6.5",
"element-ui": "~2.15.7",
"@storybook/addon-actions": "^6.3.6",
"@storybook/addon-essentials": "^6.3.6",
"@storybook/addon-links": "^6.3.6",
"@storybook/vue": "^6.3.6",
"@testing-library/jest-dom": "^5.16.4",
"@testing-library/vue": "^5.8.2",
"@types/jest": "^27.4.0",
"@types/markdown-it": "^12.2.3",
"@types/sanitize-html": "^2.6.2",
"@typescript-eslint/eslint-plugin": "^4.29.0",
"@typescript-eslint/parser": "^4.29.0",
"@vue/cli-plugin-babel": "~4.5.0",
@ -55,6 +58,8 @@
"@vue/eslint-config-typescript": "^7.0.0",
"@vue/test-utils": "^1.0.3",
"babel-loader": "^8.2.2",
"c8": "7.11.0",
"core-js": "^3.6.5",
"eslint": "^7.32.0",
"eslint-plugin-prettier": "^3.4.0",
"eslint-plugin-vue": "^7.16.0",
@ -62,11 +67,12 @@
"gulp-autoprefixer": "^4.0.0",
"gulp-clean-css": "^4.3.0",
"gulp-dart-sass": "^1.0.2",
"node-notifier": ">=8.0.1",
"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",
"prettier": "^2.3.2",
"sass": "^1.26.5",
"sass-loader": "^8.0.2",
@ -74,6 +80,9 @@
"storybook-addon-themes": "^6.1.0",
"trim": ">=0.0.3",
"typescript": "~4.6.0",
"vite": "2.9.5",
"vite-plugin-vue2": "1.9.3",
"vitest": "0.9.3",
"vue": "^2.6.11",
"vue-class-component": "^7.2.3",
"vue-loader": "^15.9.7",
@ -81,6 +90,12 @@
"vue-template-compiler": "^2.6.11",
"vue-typed-mixins": "^0.2.0",
"vue2-boring-avatars": "0.3.4",
"vue-tsc": "0.34.8",
"xss": "^1.0.10"
},
"dependencies": {
"element-ui": "~2.15.7",
"sanitize-html": "2.7.0",
"vue2-boring-avatars": "0.3.4"
}
}

View file

@ -0,0 +1 @@
import '@testing-library/jest-dom';

View file

@ -58,7 +58,7 @@ import N8nOption from '../N8nOption';
import N8nInputLabel from '../N8nInputLabel';
import { getValidationError, VALIDATORS } from './validators';
import { Rule, RuleGroup, IValidator } from "../../../../editor-ui/src/Interface";
import { Rule, RuleGroup, IValidator } from "../../types";
import Locale from '../../mixins/locale';
import mixins from 'vue-typed-mixins';

View file

@ -1,5 +1,5 @@
import { IValidator, RuleGroup } from "../../../../editor-ui/src/Interface";
import { IValidator, RuleGroup } 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,}))$/;

View file

@ -29,7 +29,7 @@
<script lang="ts">
import Vue from 'vue';
import N8nFormInput from '../N8nFormInput';
import { IFormInputs } from '../../Interface';
import { IFormInputs } from '../../types';
import ResizeObserver from '../ResizeObserver';
export default Vue.extend({

View file

@ -0,0 +1,71 @@
/* tslint:disable:variable-name */
import N8nNotice from './Notice.vue';
import {StoryFn} from "@storybook/vue";
export default {
title: 'Atoms/Notice',
component: N8nNotice,
argTypes: {
theme: {
control: 'select',
options: ['success', 'warning', 'danger', 'info'],
},
},
};
const SlotTemplate: StoryFn = (args, {argTypes}) => ({
props: Object.keys(argTypes),
components: {
N8nNotice,
},
template: `<n8n-notice v-bind="$props">This is a notice! Thread carefully from this point forward.</n8n-notice>`,
});
const PropTemplate: StoryFn = (args, {argTypes}) => ({
props: Object.keys(argTypes),
components: {
N8nNotice,
},
template: `<n8n-notice v-bind="$props"/>`,
});
export const Warning = SlotTemplate.bind({});
Warning.args = {
theme: 'warning',
};
export const Danger = SlotTemplate.bind({});
Danger.args = {
theme: 'danger',
};
export const Success = SlotTemplate.bind({});
Success.args = {
theme: 'success',
};
export const Info = SlotTemplate.bind({});
Info.args = {
theme: 'info',
};
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>.',
};
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.',
};
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.',
};

View file

@ -0,0 +1,150 @@
<template>
<div :id="id" :class="classes" role="alert">
<div class="notice-content">
<n8n-text size="small">
<slot>
<span
:class="expanded ? $style['expanded'] : $style['truncated']"
:id="`${id}-content`"
role="region"
v-html="sanitizedContent"
/>
<span v-if="canTruncate">
<a
role="button"
:aria-controls="`${id}-content`"
:aria-expanded="canTruncate && !expanded ? 'false' : 'true'"
@click="toggleExpanded"
>
{{ t(expanded ? 'notice.showLess' : 'notice.showMore') }}
</a>
</span>
</slot>
</n8n-text>
</div>
</div>
</template>
<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";
const DEFAULT_TRUNCATION_MAX_LENGTH = 150;
export default Vue.extend({
name: 'n8n-notice',
directives: {},
mixins: [
Locale,
],
props: {
id: {
type: String,
default: () => uid('notice'),
},
theme: {
type: String,
default: 'warning',
},
truncateAt: {
type: Number,
default: 150,
},
truncate: {
type: Boolean,
default: false,
},
content: {
type: String,
default: '',
},
},
components: {
N8nText,
},
data() {
return {
expanded: false,
};
},
computed: {
classes(): string[] {
return [
'notice',
this.$style['notice'],
this.$style[this.theme],
];
},
canTruncate(): boolean {
return this.truncate && this.content.length > this.truncateAt;
},
truncatedContent(): string {
if (!this.canTruncate || this.expanded) {
return this.content;
}
return this.content.slice(0, this.truncateAt as number) + '...';
},
sanitizedContent(): string {
return sanitizeHtml(this.truncatedContent);
},
},
methods: {
toggleExpanded() {
this.expanded = !this.expanded;
},
},
});
</script>
<style lang="scss" module>
.notice {
display: flex;
color: var(--custom-font-black);
margin: 0;
padding: var(--spacing-xs);
background-color: var(--background-color);
border-width: 1px 1px 1px 7px;
border-style: solid;
border-color: var(--border-color);
border-radius: var(--border-radius-small);
a {
font-weight: var(--font-weight-bold);
}
}
.warning {
--border-color: var(--color-warning-tint-1);
--background-color: var(--color-warning-tint-2);
}
.danger {
--border-color: var(--color-danger-tint-1);
--background-color: var(--color-danger-tint-2);
}
.success {
--border-color: var(--color-success-tint-1);
--background-color: var(--color-success-tint-2);
}
.info {
--border-color: var(--color-info-tint-1);
--background-color: var(--color-info-tint-2);
}
.expanded {
+ span {
margin-top: var(--spacing-4xs);
display: block;
}
}
.truncated {
display: inline;
}
</style>

View file

@ -0,0 +1,95 @@
import {fireEvent, render} from '@testing-library/vue';
import N8nNotice from "../Notice.vue";
describe('components', () => {
describe('N8nNotice', () => {
it('should render correctly', () => {
const wrapper = render(N8nNotice, {
props: {
id: 'notice',
},
slots: {
default: 'This is a notice.',
},
});
expect(wrapper.html()).toMatchSnapshot();
});
describe('props', () => {
describe('content', () => {
it('should render correctly with content prop', () => {
const wrapper = render(N8nNotice, {
props: {
id: 'notice',
content: 'This is a notice.',
},
});
expect(wrapper.html()).toMatchSnapshot();
});
it('should render html', () => {
const wrapper = render(N8nNotice, {
props: {
id: 'notice',
content: '<strong>Hello world!</strong> This is a notice.',
},
});
expect(wrapper.container.querySelectorAll('strong')).toHaveLength(1);
expect(wrapper.html()).toMatchSnapshot();
});
it('should sanitize rendered html', () => {
const wrapper = render(N8nNotice, {
props: {
id: 'notice',
content: '<script>alert(1);</script> This is a notice.',
},
});
expect(wrapper.container.querySelector('script')).not.toBeTruthy();
expect(wrapper.html()).toMatchSnapshot();
});
});
});
describe('truncation', () => {
it('should truncate content longer than 150 characters', async () => {
const wrapper = render(N8nNotice, {
props: {
id: 'notice',
truncate: true,
content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.',
},
});
const button = await wrapper.findByRole('button');
const region = await wrapper.findByRole('region');
expect(button).toBeVisible();
expect(button).toHaveTextContent('Show more');
expect(region).toBeVisible();
expect(region.textContent!.endsWith('...')).toBeTruthy();
});
it('should expand truncated text when clicking show more', async () => {
const wrapper = render(N8nNotice, {
props: {
id: 'notice',
truncate: true,
content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.',
},
});
const button = await wrapper.findByRole('button');
const region = await wrapper.findByRole('region');
await fireEvent.click(button);
expect(button).toHaveTextContent('Show less');
expect(region.textContent!.endsWith('...')).not.toBeTruthy();
});
});
});
});

View file

@ -0,0 +1,28 @@
// Vitest Snapshot v1
exports[`components > N8nNotice > props > content > should render correctly with content prop 1`] = `
"<div id=\\"notice\\" role=\\"alert\\" class=\\"notice _notice_4m4il_1 _warning_4m4il_16\\">
<div class=\\"notice-content\\"><span class=\\"_size-small_9dlpz_24 _regular_9dlpz_5\\"><span id=\\"notice-content\\" role=\\"region\\" class=\\"_truncated_4m4il_41\\">This is a notice.</span>
<!----></span></div>
</div>"
`;
exports[`components > N8nNotice > props > content > should render html 1`] = `
"<div id=\\"notice\\" role=\\"alert\\" class=\\"notice _notice_4m4il_1 _warning_4m4il_16\\">
<div class=\\"notice-content\\"><span class=\\"_size-small_9dlpz_24 _regular_9dlpz_5\\"><span id=\\"notice-content\\" role=\\"region\\" class=\\"_truncated_4m4il_41\\"><strong>Hello world!</strong> This is a notice.</span>
<!----></span></div>
</div>"
`;
exports[`components > N8nNotice > props > content > should sanitize rendered html 1`] = `
"<div id=\\"notice\\" role=\\"alert\\" class=\\"notice _notice_4m4il_1 _warning_4m4il_16\\">
<div class=\\"notice-content\\"><span class=\\"_size-small_9dlpz_24 _regular_9dlpz_5\\"><span id=\\"notice-content\\" role=\\"region\\" class=\\"_truncated_4m4il_41\\"> This is a notice.</span>
<!----></span></div>
</div>"
`;
exports[`components > N8nNotice > should render correctly 1`] = `
"<div id=\\"notice\\" role=\\"alert\\" class=\\"notice _notice_4m4il_1 _warning_4m4il_16\\">
<div class=\\"notice-content\\"><span class=\\"_size-small_9dlpz_24 _regular_9dlpz_5\\">This is a notice.</span></div>
</div>"
`;

View file

@ -0,0 +1,3 @@
import N8nNotice from './Notice.vue';
export default N8nNotice;

View file

@ -11,7 +11,7 @@
</template>
<script lang="ts">
import RadioButton from './RadioButton';
import RadioButton from './RadioButton.vue';
export default {
name: 'n8n-radio-buttons',

View file

@ -27,7 +27,7 @@
<script lang="ts">
import Vue from 'vue';
import N8nUserInfo from '../N8nUserInfo';
import { IUser } from '../../Interface';
import { IUser } from '../../types';
import ElSelect from 'element-ui/lib/select';
import ElOption from 'element-ui/lib/option';
import Locale from '../../mixins/locale';

View file

@ -20,7 +20,7 @@
</template>
<script lang="ts">
import { IUser } from '../../Interface';
import { IUser } from '../../types';
import Vue from 'vue';
import N8nActionToggle from '../N8nActionToggle';
import N8nBadge from '../N8nBadge';

View file

@ -1,10 +0,0 @@
import Vue from 'vue';
/** N8n component common definition */
export declare class N8nComponent extends Vue {
/** Install component into Vue */
static install(vue: typeof Vue): void;
}
/** Component size definition for button, input, etc */
export type N8nComponentSize = 'xlarge' | 'large' | 'medium' | 'small' | 'mini';

View file

@ -1 +0,0 @@
declare module './N8nButton';

View file

@ -52,6 +52,7 @@ import N8nLoading from './N8nLoading';
import N8nMarkdown from './N8nMarkdown';
import N8nMenu from './N8nMenu';
import N8nMenuItem from './N8nMenuItem';
import N8nNotice from './N8nNotice';
import N8nLink from './N8nLink';
import N8nOption from './N8nOption';
import N8nRadioButtons from './N8nRadioButtons';
@ -90,6 +91,7 @@ export {
N8nMarkdown,
N8nMenu,
N8nMenuItem,
N8nNotice,
N8nOption,
N8nRadioButtons,
N8nSelect,

View file

@ -5,6 +5,8 @@ export default {
'nds.userSelect.noMatchingUsers': 'No matching users',
'nds.usersList.deleteUser': 'Delete User',
'nds.usersList.reinviteUser': 'Resend invite',
'notice.showMore': 'Show more',
'notice.showLess': 'Show less',
'formInput.validator.fieldRequired': 'This field is required',
'formInput.validator.minCharactersRequired': 'Must be at least {minimum} characters',
'formInput.validator.maxCharactersRequired': 'Must be at most {maximum} characters',

View file

@ -1 +1,16 @@
declare module 'n8n-design-system';
import Vue from 'vue';
import * as locale from './locale';
declare module 'vue/types/vue' {
interface Vue {
$style: Record<string, string>;
}
}
declare module 'n8n-design-system' {
export * from './components';
export { N8nUserSelect, N8nUsersList } from './components'; // Workaround for circular imports, will be removed when migrated to typescript
export { locale };
}
export * from './types';

View file

@ -1,10 +1,15 @@
import * as components from './components';
import * as locale from './locale';
// @TODO Define proper plugin that loads all components
// tslint:disable-next-line:forin
for (const key in components) {
const component = components[key];
component.install = function (Vue) {
Vue.component(component.name, component);
component.install = (app) => {
app.component(component.name, component);
};
}
export { locale };
export * from './components';

View file

@ -0,0 +1 @@
export { default as localeMixin } from './locale';

View file

@ -2,7 +2,7 @@ import { t } from '../locale';
export default {
methods: {
t(...args) {
t(...args: string[]) {
return t.apply(this, args);
},
},

View file

@ -1,14 +1,38 @@
declare module 'element-ui/lib/button';
declare module 'element-ui/lib/col';
declare module 'element-ui/lib/input';
declare module 'element-ui/lib/tooltip';
declare module 'element-ui/lib/input-number';
declare module 'element-ui/lib/drawer';
declare module 'element-ui/lib/dialog';
declare module 'element-ui/lib/dropdown';
declare module 'element-ui/lib/dropdown-menu';
declare module 'element-ui/lib/dropdown-item';
declare module 'element-ui/lib/submenu';
declare module 'element-ui/lib/radio';
declare module 'element-ui/lib/radio-group';
declare module 'element-ui/lib/radio-button';
declare module 'element-ui/lib/checkbox';
declare module 'element-ui/lib/switch';
declare module 'element-ui/lib/select';
declare module 'element-ui/lib/option';
declare module 'element-ui/lib/option-group';
declare module 'element-ui/lib/pagination';
declare module 'element-ui/lib/button-group';
declare module 'element-ui/lib/table';
declare module 'element-ui/lib/table-column';
declare module 'element-ui/lib/date-picker';
declare module 'element-ui/lib/tabs';
declare module 'element-ui/lib/tab-pane';
declare module 'element-ui/lib/tag';
declare module 'element-ui/lib/row';
declare module 'element-ui/lib/col';
declare module 'element-ui/lib/badge';
declare module 'element-ui/lib/card';
declare module 'element-ui/lib/color-picker';
declare module 'element-ui/lib/container';
declare module 'element-ui/lib/loading';
declare module 'element-ui/lib/message-box';
declare module 'element-ui/lib/message';
declare module 'element-ui/lib/menu';
declare module 'element-ui/lib/menu-item';
declare module 'element-ui/lib/row';
declare module 'element-ui/lib/tag';
declare module 'element-ui/lib/skeleton';
declare module 'element-ui/lib/skeleton-item';
declare module 'element-ui/lib/notification';
declare module 'element-ui/lib/popover';
declare module 'element-ui/lib/transitions/collapse-transition';
declare module 'element-ui/lib/tooltip';
declare module 'element-ui/lib/input-number';

View file

@ -0,0 +1,46 @@
export type Rule = { name: string; config?: any}; // tslint:disable-line:no-any
export type RuleGroup = {
rules: Array<Rule | RuleGroup>;
defaultError?: {messageKey: string, options?: any}; // tslint:disable-line:no-any
};
export type IValidator = {
validate: (value: string | number | boolean | null | undefined, config: any) => false | {messageKey: string, options?: any}; // tslint:disable-line:no-any
};
export type IFormInput = {
name: string;
initialValue?: string | number | boolean | null;
properties: {
label?: string;
type?: 'text' | 'email' | 'password' | 'select' | 'multi-select' | 'info';
maxlength?: number;
required?: boolean;
showRequiredAsterisk?: boolean;
validators?: {
[name: string]: IValidator;
};
validationRules?: Array<Rule | RuleGroup>;
validateOnBlur?: boolean;
infoText?: string;
placeholder?: string;
options?: Array<{label: string; value: string}>;
autocomplete?: 'off' | 'new-password' | 'current-password' | 'given-name' | 'family-name' | 'email'; // https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/autocomplete
capitalize?: boolean;
focusInitially?: boolean;
};
shouldDisplay?: (values: {[key: string]: unknown}) => boolean;
};
export type IFormInputs = IFormInput[];
export type IFormBoxConfig = {
title: string;
buttonText?: string;
secondaryButtonText?: string;
inputs: IFormInputs;
redirectLink?: string;
redirectText?: string;
};

View file

@ -0,0 +1,2 @@
export * from './form';
export * from './user';

View file

@ -0,0 +1,8 @@
export interface IUser {
id: string;
firstName?: string;
lastName?: string;
email?: string;
isPending: boolean;
isOwner: boolean;
}

View file

@ -0,0 +1,2 @@
export * from './markdown';
export * from './uid';

View file

@ -0,0 +1,9 @@
/**
* Math.random should be unique because of its seeding algorithm.
* Convert it to base 36 (numbers + letters), and grab the first 9 characters after the decimal.
*
* @param baseId
*/
export function uid (baseId?: string): string {
return `${baseId ? `${baseId}-` : ''}${Math.random().toString(36).substring(2, 11)}`;
}

View file

@ -111,17 +111,21 @@
var(--color-warning-l)
);
--color-warning-tint-1-l: 88%;
--color-warning-tint-1-h: 35;
--color-warning-tint-1-s: 78%;
--color-warning-tint-1-l: 84%;
--color-warning-tint-1: hsl(
var(--color-warning-h),
var(--color-warning-s),
var(--color-warning-tint-1-l)
);
--color-warning-tint-2-h: 34%;
--color-warning-tint-2-s: 80%;
--color-warning-tint-2-l: 96%;
--color-warning-tint-2: hsl(
var(--color-warning-h),
var(--color-warning-s),
var(--color-warning-tint-2-h),
var(--color-warning-tint-2-s),
var(--color-warning-tint-2-l)
);

View file

@ -3,16 +3,26 @@
"target": "esnext",
"module": "esnext",
"strict": true,
"jsx": "preserve",
"importHelpers": true,
"moduleResolution": "node",
"declaration": true,
"experimentalDecorators": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowJs": true,
"allowSyntheticDefaultImports": true,
"sourceMap": true,
"baseUrl": ".",
"types": ["webpack-env", "jest"],
"outDir": "dist",
"types": [
"webpack-env",
"jest",
"vitest/globals"
],
"typeRoots": [
"@testing-library",
"@types"
],
"paths": {
"@/*": ["src/*"]
},
@ -21,9 +31,7 @@
"include": [
"src/**/*.ts",
"src/**/*.tsx",
"src/**/*.vue",
"tests/**/*.ts",
"tests/**/*.tsx"
"src/**/*.vue"
],
"exclude": ["node_modules"]
}

View file

@ -0,0 +1,42 @@
import { createVuePlugin } from 'vite-plugin-vue2';
import { resolve } from 'path';
export default {
plugins: [
createVuePlugin(),
],
resolve: {
alias: {
'@': resolve(__dirname, 'src'),
'vue2-boring-avatars': resolve(__dirname, '..', '..', 'node_modules', 'vue2-boring-avatars', 'dist', 'vue-2-boring-avatars.umd.js'),
// 'vue2-boring-avatars': 'vue2-boring-avatars/dist/vue-2-boring-avatars.umd.js',
},
},
build: {
lib: {
entry: resolve(__dirname, 'src', 'main.js'),
name: 'N8nDesignSystem',
fileName: (format) => `n8n-design-system.${format}.js`,
},
rollupOptions: {
// make sure to externalize deps that shouldn't be bundled
// into your library
external: ['vue'],
output: {
exports: 'named',
// Provide global variables to use in the UMD build
// for externalized deps
globals: {
vue: 'Vue',
},
},
},
},
test: {
globals: true,
environment: 'jsdom',
setupFiles: [
'./src/__tests__/setup.ts',
],
},
};

View file

@ -23,6 +23,8 @@ import {
WorkflowExecuteMode,
} from 'n8n-workflow';
export * from 'n8n-design-system/src/types';
declare module 'jsplumb' {
interface PaintStyle {
stroke?: string;
@ -477,12 +479,6 @@ export interface IPushDataConsoleMessage {
messages: string[];
}
export interface IVersionNotificationSettings {
enabled: boolean;
endpoint: string;
infoUrl: string;
}
export type IPersonalizationSurveyAnswersV1 = {
codingSkill?: string | null;
companyIndustry?: string[] | null;
@ -505,6 +501,34 @@ export type IPersonalizationSurveyAnswersV2 = {
otherCompanyIndustryExtended?: string[] | null;
};
export type IRole = 'default' | 'owner' | 'member';
export interface IUserResponse {
id: string;
firstName?: string;
lastName?: string;
email?: string;
globalRole?: {
name: IRole;
id: string;
};
personalizationAnswers?: IPersonalizationSurveyAnswersV1 | IPersonalizationSurveyAnswersV2 | null;
isPending: boolean;
}
export interface IUser extends IUserResponse {
isDefaultUser: boolean;
isPendingUser: boolean;
isOwner: boolean;
fullName?: string;
}
export interface IVersionNotificationSettings {
enabled: boolean;
endpoint: string;
infoUrl: string;
}
export interface IN8nPrompts {
message: string;
title: string;
@ -888,21 +912,6 @@ export interface IBounds {
export type ILogInStatus = 'LoggedIn' | 'LoggedOut';
export type IRole = 'default' | 'owner' | 'member';
export interface IUserResponse {
id: string;
firstName?: string;
lastName?: string;
email?: string;
globalRole?: {
name: IRole;
id: string;
};
personalizationAnswers?: IPersonalizationSurveyAnswersV1 | IPersonalizationSurveyAnswersV2 | null;
isPending: boolean;
}
export interface IInviteResponse {
user: {
id: string;
@ -911,59 +920,6 @@ export interface IInviteResponse {
error?: string;
}
export interface IUser extends IUserResponse {
isDefaultUser: boolean;
isPendingUser: boolean;
isOwner: boolean;
fullName?: string;
}
export type Rule = { name: string; config?: any}; // tslint:disable-line:no-any
export type RuleGroup = {
rules: Array<Rule | RuleGroup>;
defaultError?: {messageKey: string, options?: any}; // tslint:disable-line:no-any
};
export type IValidator = {
validate: (value: string | number | boolean | null | undefined, config: any) => false | {messageKey: string, options?: any}; // tslint:disable-line:no-any
};
export type IFormInput = {
name: string;
initialValue?: string | number | boolean | null;
properties: {
label?: string;
type?: 'text' | 'email' | 'password' | 'select' | 'multi-select' | 'info';
maxlength?: number;
required?: boolean;
showRequiredAsterisk?: boolean;
validators?: {
[name: string]: IValidator;
};
validationRules?: Array<Rule | RuleGroup>;
validateOnBlur?: boolean;
infoText?: string;
placeholder?: string;
options?: Array<{label: string; value: string}>;
autocomplete?: 'off' | 'new-password' | 'current-password' | 'given-name' | 'family-name' | 'email'; // https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/autocomplete
capitalize?: boolean;
focusInitially?: boolean;
};
shouldDisplay?: (values: {[key: string]: unknown}) => boolean;
};
export type IFormInputs = IFormInput[];
export type IFormBoxConfig = {
title: string;
buttonText?: string;
secondaryButtonText?: string;
inputs: IFormInputs;
redirectLink?: string;
redirectText?: string;
};
export interface ITab {
value: string | number;
label?: string;

View file

@ -14,11 +14,13 @@
/>
</div>
<div v-else-if="parameter.type === 'notice'" class="parameter-item parameter-notice">
<n8n-text size="small">
<span v-html="$locale.nodeText().inputLabelDisplayName(parameter, path)"></span>
</n8n-text>
</div>
<n8n-notice
v-else-if="parameter.type === 'notice'"
class="parameter-item"
:content="$locale.nodeText().inputLabelDisplayName(parameter, path)"
:truncate="parameter.typeOptions && parameter.typeOptions.truncate"
:truncate-at="parameter.typeOptions && parameter.typeOptions.truncateAt"
/>
<div
v-else-if="['collection', 'fixedCollection'].includes(parameter.type)"

View file

@ -61,6 +61,7 @@ import {
N8nMarkdown,
N8nMenu,
N8nMenuItem,
N8nNotice,
N8nOption,
N8nRadioButtons,
N8nSelect,
@ -98,6 +99,7 @@ Vue.use(N8nLink);
Vue.component('n8n-markdown', N8nMarkdown);
Vue.use(N8nMenu);
Vue.use(N8nMenuItem);
Vue.component('n8n-notice', N8nNotice);
Vue.use(N8nOption);
Vue.use(N8nSelect);
Vue.use(N8nSpinner);

View file

@ -1,4 +1,12 @@
import Vue from 'vue';
declare module '*.vue' {
import Vue from 'vue';
export default Vue;
import Vue from 'vue';
export default Vue;
}
declare module 'vue/types/vue' {
interface Vue {
$style: Record<string, string>;
}
}

View file

@ -861,6 +861,8 @@ export interface INodePropertyTypeOptions {
rows?: number; // Supported by: string
showAlpha?: boolean; // Supported by: color
sortable?: boolean; // Supported when "multipleValues" set to true
truncate?: boolean; // Supported by: notice
truncateAt?: number; // Supported by: notice
[key: string]: any;
}