mirror of
https://github.com/n8n-io/n8n.git
synced 2024-12-24 20:24:05 -08:00
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:
parent
69d6b7827f
commit
0a69a9eb9c
38342
package-lock.json
generated
38342
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -35,6 +35,11 @@ module.exports = {
|
|||
],
|
||||
});
|
||||
|
||||
config.resolve.alias = {
|
||||
...config.resolve.alias,
|
||||
"@/": path.resolve(__dirname, "../src/"),
|
||||
};
|
||||
|
||||
return config;
|
||||
},
|
||||
};
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
1
packages/design-system/src/__tests__/setup.ts
Normal file
1
packages/design-system/src/__tests__/setup.ts
Normal file
|
@ -0,0 +1 @@
|
|||
import '@testing-library/jest-dom';
|
|
@ -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';
|
||||
|
|
|
@ -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,}))$/;
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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.',
|
||||
};
|
150
packages/design-system/src/components/N8nNotice/Notice.vue
Normal file
150
packages/design-system/src/components/N8nNotice/Notice.vue
Normal 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>
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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>"
|
||||
`;
|
3
packages/design-system/src/components/N8nNotice/index.ts
Normal file
3
packages/design-system/src/components/N8nNotice/index.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
import N8nNotice from './Notice.vue';
|
||||
|
||||
export default N8nNotice;
|
|
@ -11,7 +11,7 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import RadioButton from './RadioButton';
|
||||
import RadioButton from './RadioButton.vue';
|
||||
|
||||
export default {
|
||||
name: 'n8n-radio-buttons',
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
|
@ -1 +0,0 @@
|
|||
declare module './N8nButton';
|
|
@ -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,
|
|
@ -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',
|
||||
|
|
17
packages/design-system/src/main.d.ts
vendored
17
packages/design-system/src/main.d.ts
vendored
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
1
packages/design-system/src/mixins/index.ts
Normal file
1
packages/design-system/src/mixins/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export { default as localeMixin } from './locale';
|
|
@ -2,7 +2,7 @@ import { t } from '../locale';
|
|||
|
||||
export default {
|
||||
methods: {
|
||||
t(...args) {
|
||||
t(...args: string[]) {
|
||||
return t.apply(this, args);
|
||||
},
|
||||
},
|
44
packages/design-system/src/shims-element-ui.d.ts
vendored
44
packages/design-system/src/shims-element-ui.d.ts
vendored
|
@ -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';
|
||||
|
|
46
packages/design-system/src/types/form.ts
Normal file
46
packages/design-system/src/types/form.ts
Normal 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;
|
||||
};
|
2
packages/design-system/src/types/index.ts
Normal file
2
packages/design-system/src/types/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
|||
export * from './form';
|
||||
export * from './user';
|
8
packages/design-system/src/types/user.ts
Normal file
8
packages/design-system/src/types/user.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
export interface IUser {
|
||||
id: string;
|
||||
firstName?: string;
|
||||
lastName?: string;
|
||||
email?: string;
|
||||
isPending: boolean;
|
||||
isOwner: boolean;
|
||||
}
|
2
packages/design-system/src/utils/index.ts
Normal file
2
packages/design-system/src/utils/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
|||
export * from './markdown';
|
||||
export * from './uid';
|
9
packages/design-system/src/utils/uid.ts
Normal file
9
packages/design-system/src/utils/uid.ts
Normal 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)}`;
|
||||
}
|
|
@ -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)
|
||||
);
|
||||
|
||||
|
|
|
@ -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"]
|
||||
}
|
||||
|
|
42
packages/design-system/vite.config.ts
Normal file
42
packages/design-system/vite.config.ts
Normal 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',
|
||||
],
|
||||
},
|
||||
};
|
|
@ -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;
|
||||
|
|
|
@ -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)"
|
||||
|
|
|
@ -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);
|
||||
|
|
12
packages/editor-ui/src/shims-vue.d.ts
vendored
12
packages/editor-ui/src/shims-vue.d.ts
vendored
|
@ -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>;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue