mirror of
https://github.com/n8n-io/n8n.git
synced 2025-01-11 04:47:29 -08:00
feat: migrate editor-ui to Vite.js and various DX improvements (N8N-2277) (#4061)
* feat: Added vite.js dependencies. * chore: Removed tests folder to follow same structure as design-system * chore: Removed unused testing config. * chore: Created vite.js index.html * refactor: Updated scss structure and imports. * refactor: Updated workflow building. * fix: Cleared up all workflow dependency cycles. Added proper package.json imports config. * feat: Got a working build using Vite. Need to fix issues next. * fix: Progress! Getting process.env error. * fix: Changed process.env to import.meta.env. * fix: Fixed circular imports that used require(). Fixed monaco editor. * chore: Removed commented code. * chore: Cleaned up package.json * feat: Made necessary changes to replace base path in css files. * feat: Serve CSS files for `editor-ui` Vite migration (#4069) ⚡ Serve CSS files for Vite migration * chore: Fixed package-lock.json. * fix: Fixed build after centralized tsconfig update. * fix: Removed lodash-es replacement. * fix: Commented out vitest test command. * style: Fixed linting issues. * fix: Added lodash-es hotfix back. * chore: Updated package-lock.json * refactor: Renamed all n8n scss variables to no longer be defined as private. * feat(editor): add application-wide el-button replacement. * fix(editor): Fix import in page alert after merge. * chore(editor): update package-lock.json. * fix: Case sensitive lodash-es replacement for vue-agile. * fix: add alias for lodash-es camelcase import. * fix: add patch-package support for fixing quill * feat: add patch-package on postinstall * fix: update quill patch path. * refactor: rename quill patch * fix: update quill version. * fix: update quill patch * fix: fix linting rules after installing eslint in design-system * fix: update date picker button to have primary color * test: update callout component snapshots * fix(editor): fix linting issues in editor after enabling eslint * fix(cli): add /assets/* to auth ignore endpoints in server * chore: update package-lock.json * chore: update package-lock.json * fix(editor): fix linting issues * feat: add vite-legacy support * fix: update workflow package interface imports to type imports. * chore: update package-lock.json * fix(editor) fix importing translations other than english * fix(editor): remove test command until vitest is added * fix: increase memory allocation for vite build * fix: add patch-package patches to n8n-custom docker build * fix: add performance and load time improvements * fix: add proper typing to setNodeType * chore: update package-lock.json * style: use generic type for reduce in setNodeType Co-authored-by: Iván Ovejero <ivov.src@gmail.com>
This commit is contained in:
parent
e709cb5fe2
commit
27e2ce0470
|
@ -9,6 +9,7 @@ RUN \
|
||||||
|
|
||||||
COPY turbo.json package.json package-lock.json tsconfig.json ./
|
COPY turbo.json package.json package-lock.json tsconfig.json ./
|
||||||
COPY packages ./packages
|
COPY packages ./packages
|
||||||
|
COPY patches ./patches
|
||||||
|
|
||||||
RUN chown -R node:node .
|
RUN chown -R node:node .
|
||||||
RUN npm config set legacy-peer-deps true
|
RUN npm config set legacy-peer-deps true
|
||||||
|
|
15698
package-lock.json
generated
15698
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -11,6 +11,7 @@
|
||||||
"lint": "turbo run lint",
|
"lint": "turbo run lint",
|
||||||
"lintfix": "turbo run lintfix",
|
"lintfix": "turbo run lintfix",
|
||||||
"optimize-svg": "find ./packages -name '*.svg' ! -name 'pipedrive.svg' -print0 | xargs -0 -P16 -L20 npx svgo",
|
"optimize-svg": "find ./packages -name '*.svg' ! -name 'pipedrive.svg' -print0 | xargs -0 -P16 -L20 npx svgo",
|
||||||
|
"postinstall": "patch-package",
|
||||||
"start": "run-script-os",
|
"start": "run-script-os",
|
||||||
"start:default": "cd packages/cli/bin && ./n8n",
|
"start:default": "cd packages/cli/bin && ./n8n",
|
||||||
"start:tunnel": "./packages/cli/bin/n8n start --tunnel",
|
"start:tunnel": "./packages/cli/bin/n8n start --tunnel",
|
||||||
|
@ -21,6 +22,7 @@
|
||||||
"worker": "./packages/cli/bin/n8n worker"
|
"worker": "./packages/cli/bin/n8n worker"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"patch-package": "^6.4.7",
|
||||||
"rimraf": "^3.0.2",
|
"rimraf": "^3.0.2",
|
||||||
"run-script-os": "^1.0.7",
|
"run-script-os": "^1.0.7",
|
||||||
"turbo": "1.2.15"
|
"turbo": "1.2.15"
|
||||||
|
|
|
@ -147,6 +147,7 @@ import {
|
||||||
WebhookServer,
|
WebhookServer,
|
||||||
WorkflowExecuteAdditionalData,
|
WorkflowExecuteAdditionalData,
|
||||||
} from '.';
|
} from '.';
|
||||||
|
import glob from 'fast-glob';
|
||||||
import { ResponseError } from './ResponseHelper';
|
import { ResponseError } from './ResponseHelper';
|
||||||
|
|
||||||
require('body-parser-xml')(bodyParser);
|
require('body-parser-xml')(bodyParser);
|
||||||
|
@ -389,6 +390,7 @@ class App {
|
||||||
const excludeEndpoints = config.getEnv('security.excludeEndpoints');
|
const excludeEndpoints = config.getEnv('security.excludeEndpoints');
|
||||||
|
|
||||||
const ignoredEndpoints = [
|
const ignoredEndpoints = [
|
||||||
|
'assets',
|
||||||
'healthz',
|
'healthz',
|
||||||
'metrics',
|
'metrics',
|
||||||
this.endpointWebhook,
|
this.endpointWebhook,
|
||||||
|
@ -1753,11 +1755,28 @@ class App {
|
||||||
const editorUiPath = require.resolve('n8n-editor-ui');
|
const editorUiPath = require.resolve('n8n-editor-ui');
|
||||||
const filePath = pathJoin(pathDirname(editorUiPath), 'dist', 'index.html');
|
const filePath = pathJoin(pathDirname(editorUiPath), 'dist', 'index.html');
|
||||||
const n8nPath = config.getEnv('path');
|
const n8nPath = config.getEnv('path');
|
||||||
|
const basePathRegEx = /\/%BASE_PATH%\//g;
|
||||||
|
|
||||||
let readIndexFile = readFileSync(filePath, 'utf8');
|
let readIndexFile = readFileSync(filePath, 'utf8');
|
||||||
readIndexFile = readIndexFile.replace(/\/%BASE_PATH%\//g, n8nPath);
|
readIndexFile = readIndexFile.replace(basePathRegEx, n8nPath);
|
||||||
readIndexFile = readIndexFile.replace(/\/favicon.ico/g, `${n8nPath}favicon.ico`);
|
readIndexFile = readIndexFile.replace(/\/favicon.ico/g, `${n8nPath}favicon.ico`);
|
||||||
|
|
||||||
|
const cssPath = pathJoin(pathDirname(editorUiPath), 'dist', '**/*.css');
|
||||||
|
const cssFiles: Record<string, string> = {};
|
||||||
|
glob.sync(cssPath).forEach((filePath) => {
|
||||||
|
let readFile = readFileSync(filePath, 'utf8');
|
||||||
|
readFile = readFile.replace(basePathRegEx, n8nPath);
|
||||||
|
cssFiles[filePath.replace(pathJoin(pathDirname(editorUiPath), 'dist'), '')] = readFile;
|
||||||
|
});
|
||||||
|
|
||||||
|
const jsPath = pathJoin(pathDirname(editorUiPath), 'dist', '**/*.js');
|
||||||
|
const jsFiles: Record<string, string> = {};
|
||||||
|
glob.sync(jsPath).forEach((filePath) => {
|
||||||
|
let readFile = readFileSync(filePath, 'utf8');
|
||||||
|
readFile = readFile.replace(basePathRegEx, n8nPath);
|
||||||
|
jsFiles[filePath.replace(pathJoin(pathDirname(editorUiPath), 'dist'), '')] = readFile;
|
||||||
|
});
|
||||||
|
|
||||||
const hooksUrls = config.getEnv('externalFrontendHooksUrls');
|
const hooksUrls = config.getEnv('externalFrontendHooksUrls');
|
||||||
|
|
||||||
let scriptsString = '';
|
let scriptsString = '';
|
||||||
|
@ -1793,6 +1812,14 @@ class App {
|
||||||
res.send(readIndexFile);
|
res.send(readIndexFile);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.app.get('/assets/*.css', async (req: express.Request, res: express.Response) => {
|
||||||
|
res.type('text/css').send(cssFiles[req.url]);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.app.get('/assets/*.js', async (req: express.Request, res: express.Response) => {
|
||||||
|
res.type('text/javascript').send(jsFiles[req.url]);
|
||||||
|
});
|
||||||
|
|
||||||
// Serve the website
|
// Serve the website
|
||||||
this.app.use(
|
this.app.use(
|
||||||
'/',
|
'/',
|
||||||
|
|
|
@ -1,36 +0,0 @@
|
||||||
'use strict';
|
|
||||||
|
|
||||||
const gulp = require('gulp');
|
|
||||||
const sass = require('gulp-dart-sass');
|
|
||||||
const autoprefixer = require('gulp-autoprefixer');
|
|
||||||
const cleanCSS = require('gulp-clean-css');
|
|
||||||
|
|
||||||
gulp.task('build:theme', gulp.series([compileTheme, copyThemeFonts]));
|
|
||||||
|
|
||||||
gulp.task(
|
|
||||||
'watch:theme',
|
|
||||||
gulp.series([
|
|
||||||
'build:theme',
|
|
||||||
() => {
|
|
||||||
gulp.watch('./theme/src/**/*.scss', gulp.series(['build:theme']));
|
|
||||||
},
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
|
|
||||||
function compileTheme() {
|
|
||||||
return gulp
|
|
||||||
.src('./theme/src/index.scss')
|
|
||||||
.pipe(sass.sync())
|
|
||||||
.pipe(
|
|
||||||
autoprefixer({
|
|
||||||
browsers: ['ie > 9', 'last 2 versions'],
|
|
||||||
cascade: false,
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.pipe(cleanCSS())
|
|
||||||
.pipe(gulp.dest('./theme/dist'));
|
|
||||||
}
|
|
||||||
|
|
||||||
function copyThemeFonts() {
|
|
||||||
return gulp.src('./theme/src/fonts/**').pipe(gulp.dest('./theme/dist/fonts'));
|
|
||||||
}
|
|
|
@ -13,19 +13,15 @@
|
||||||
"url": "git+https://github.com/n8n-io/n8n.git"
|
"url": "git+https://github.com/n8n-io/n8n.git"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "npm run build:theme",
|
"build": "vite build",
|
||||||
"build:vue": "vite build",
|
|
||||||
"build:vue:typecheck": "vue-tsc --emitDeclarationOnly",
|
"build:vue:typecheck": "vue-tsc --emitDeclarationOnly",
|
||||||
"dev": "npm run watch:theme",
|
|
||||||
"test": "vitest run",
|
"test": "vitest run",
|
||||||
"test:ci": "vitest run --coverage",
|
"test:ci": "vitest run --coverage",
|
||||||
"test:dev": "vitest",
|
"test:dev": "vitest",
|
||||||
"build:storybook": "build-storybook",
|
"build:storybook": "build-storybook",
|
||||||
"storybook": "start-storybook -p 6006",
|
"storybook": "start-storybook -p 6006",
|
||||||
"lint": "tslint -p tsconfig.json -c tslint.json && eslint .",
|
"lint": "tslint -p tsconfig.json -c tslint.json && eslint .",
|
||||||
"lintfix": "tslint --fix -p tsconfig.json -c tslint.json && eslint . --fix",
|
"lintfix": "tslint --fix -p tsconfig.json -c tslint.json && eslint . --fix"
|
||||||
"build:theme": "gulp build:theme",
|
|
||||||
"watch:theme": "gulp watch:theme"
|
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@fortawesome/fontawesome-svg-core": "1.x",
|
"@fortawesome/fontawesome-svg-core": "1.x",
|
||||||
|
@ -54,10 +50,9 @@
|
||||||
"babel-loader": "^8.2.2",
|
"babel-loader": "^8.2.2",
|
||||||
"c8": "7.11.0",
|
"c8": "7.11.0",
|
||||||
"core-js": "^3.6.5",
|
"core-js": "^3.6.5",
|
||||||
"gulp": "^4.0.0",
|
"eslint": "^8.0.0",
|
||||||
"gulp-autoprefixer": "^4.0.0",
|
"eslint-plugin-prettier": "^3.4.0",
|
||||||
"gulp-clean-css": "^4.3.0",
|
"eslint-plugin-vue": "^7.16.0",
|
||||||
"gulp-dart-sass": "^1.0.2",
|
|
||||||
"jsdom": "19.0.0",
|
"jsdom": "19.0.0",
|
||||||
"markdown-it": "^12.3.2",
|
"markdown-it": "^12.3.2",
|
||||||
"markdown-it-emoji": "^2.0.0",
|
"markdown-it-emoji": "^2.0.0",
|
||||||
|
|
|
@ -47,9 +47,9 @@ import Vue from 'vue';
|
||||||
export default Vue.extend({
|
export default Vue.extend({
|
||||||
name: 'n8n-action-toggle',
|
name: 'n8n-action-toggle',
|
||||||
components: {
|
components: {
|
||||||
ElDropdown,
|
ElDropdown, // eslint-disable-line @typescript-eslint/no-unsafe-assignment
|
||||||
ElDropdownMenu,
|
ElDropdownMenu, // eslint-disable-line @typescript-eslint/no-unsafe-assignment
|
||||||
ElDropdownItem,
|
ElDropdownItem, // eslint-disable-line @typescript-eslint/no-unsafe-assignment
|
||||||
N8nIcon,
|
N8nIcon,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
|
|
|
@ -45,7 +45,7 @@ export default Vue.extend({
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
Avatar,
|
Avatar, // eslint-disable-line @typescript-eslint/no-unsafe-assignment
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
initials() {
|
initials() {
|
||||||
|
|
|
@ -96,25 +96,25 @@ export default Vue.extend({
|
||||||
return this.disabled ? 'true' : 'false';
|
return this.disabled ? 'true' : 'false';
|
||||||
},
|
},
|
||||||
classes(): string {
|
classes(): string {
|
||||||
return `button ${this.$style['button']} ${this.$style[this.type]}` +
|
return `button ${this.$style.button} ${this.$style[this.type]}` +
|
||||||
`${this.size ? ` ${this.$style[this.size]}` : ''}` +
|
`${this.size ? ` ${this.$style[this.size]}` : ''}` +
|
||||||
`${this.outline ? ` ${this.$style['outline']}` : ''}` +
|
`${this.outline ? ` ${this.$style.outline}` : ''}` +
|
||||||
`${this.loading ? ` ${this.$style['loading']}` : ''}` +
|
`${this.loading ? ` ${this.$style.loading}` : ''}` +
|
||||||
`${this.float ? ` ${this.$style[`float-${this.float}`]}` : ''}` +
|
`${this.float ? ` ${this.$style[`float-${this.float}`]}` : ''}` +
|
||||||
`${this.text ? ` ${this.$style['text']}` : ''}` +
|
`${this.text ? ` ${this.$style.text}` : ''}` +
|
||||||
`${this.disabled ? ` ${this.$style['disabled']}` : ''}` +
|
`${this.disabled ? ` ${this.$style.disabled}` : ''}` +
|
||||||
`${this.block ? ` ${this.$style['block']}` : ''}` +
|
`${this.block ? ` ${this.$style.block}` : ''}` +
|
||||||
`${this.active ? ` ${this.$style['active']}` : ''}` +
|
`${this.active ? ` ${this.$style.active}` : ''}` +
|
||||||
`${this.icon || this.loading ? ` ${this.$style['icon']}` : ''}` +
|
`${this.icon || this.loading ? ` ${this.$style.icon}` : ''}` +
|
||||||
`${this.square ? ` ${this.$style['square']}` : ''}`;
|
`${this.square ? ` ${this.$style.square}` : ''}`;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" module>
|
<style lang="scss" module>
|
||||||
@import '../../../theme/src/mixins/utils';
|
@import '../../css/mixins/utils';
|
||||||
@import '../../../theme/src/common/var';
|
@import '../../css/common/var';
|
||||||
|
|
||||||
.button {
|
.button {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
import ElButton from "./ElButton.vue";
|
||||||
|
|
||||||
|
export default ElButton;
|
|
@ -19,7 +19,6 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import N8nIcon from '../N8nIcon';
|
import N8nIcon from '../N8nIcon';
|
||||||
import N8nText from '../N8nText';
|
|
||||||
|
|
||||||
const CALLOUT_DEFAULT_ICONS: { [key: string]: string } = {
|
const CALLOUT_DEFAULT_ICONS: { [key: string]: string } = {
|
||||||
info: 'info-circle',
|
info: 'info-circle',
|
||||||
|
@ -32,7 +31,6 @@ export default Vue.extend({
|
||||||
name: 'n8n-callout',
|
name: 'n8n-callout',
|
||||||
components: {
|
components: {
|
||||||
N8nIcon,
|
N8nIcon,
|
||||||
N8nText,
|
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
theme: {
|
theme: {
|
||||||
|
@ -43,14 +41,14 @@ export default Vue.extend({
|
||||||
},
|
},
|
||||||
icon: {
|
icon: {
|
||||||
type: String,
|
type: String,
|
||||||
default: 'info-circle'
|
default: 'info-circle',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
classes(): string[] {
|
classes(): string[] {
|
||||||
return [
|
return [
|
||||||
'n8n-callout',
|
'n8n-callout',
|
||||||
this.$style['callout'],
|
this.$style.callout,
|
||||||
this.$style[this.theme],
|
this.$style[this.theme],
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
|
@ -61,7 +59,7 @@ export default Vue.extend({
|
||||||
|
|
||||||
return this.icon;
|
return this.icon;
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,7 @@ exports[`components > N8nCallout > should render additional slots correctly 1`]
|
||||||
<div class=\\"_icon_p74de_40\\">
|
<div class=\\"_icon_p74de_40\\">
|
||||||
<n8n-icon-stub icon=\\"code-branch\\" size=\\"large\\"></n8n-icon-stub>
|
<n8n-icon-stub icon=\\"code-branch\\" size=\\"large\\"></n8n-icon-stub>
|
||||||
</div>
|
</div>
|
||||||
<n8n-text-stub size=\\"small\\" tag=\\"span\\">This is a secondary callout.</n8n-text-stub> <n8n-link-stub size=\\"small\\">Do something!</n8n-link-stub>
|
<n8n-text-stub size=\\"small\\">This is a secondary callout.</n8n-text-stub> <n8n-link-stub size=\\"small\\">Do something!</n8n-link-stub>
|
||||||
</div>
|
</div>
|
||||||
<n8n-link-stub theme=\\"secondary\\" size=\\"small\\" bold=\\"true\\" underline=\\"true\\" to=\\"https://n8n.io\\">Learn more</n8n-link-stub>
|
<n8n-link-stub theme=\\"secondary\\" size=\\"small\\" bold=\\"true\\" underline=\\"true\\" to=\\"https://n8n.io\\">Learn more</n8n-link-stub>
|
||||||
</div>"
|
</div>"
|
||||||
|
@ -18,7 +18,7 @@ exports[`components > N8nCallout > should render custom theme correctly 1`] = `
|
||||||
<div class=\\"_icon_p74de_40\\">
|
<div class=\\"_icon_p74de_40\\">
|
||||||
<n8n-icon-stub icon=\\"code-branch\\" size=\\"large\\"></n8n-icon-stub>
|
<n8n-icon-stub icon=\\"code-branch\\" size=\\"large\\"></n8n-icon-stub>
|
||||||
</div>
|
</div>
|
||||||
<n8n-text-stub size=\\"small\\" tag=\\"span\\">This is a secondary callout.</n8n-text-stub>
|
<n8n-text-stub size=\\"small\\">This is a secondary callout.</n8n-text-stub>
|
||||||
</div>
|
</div>
|
||||||
</div>"
|
</div>"
|
||||||
`;
|
`;
|
||||||
|
@ -29,7 +29,7 @@ exports[`components > N8nCallout > should render danger theme correctly 1`] = `
|
||||||
<div class=\\"_icon_p74de_40\\">
|
<div class=\\"_icon_p74de_40\\">
|
||||||
<n8n-icon-stub icon=\\"times-circle\\" size=\\"large\\"></n8n-icon-stub>
|
<n8n-icon-stub icon=\\"times-circle\\" size=\\"large\\"></n8n-icon-stub>
|
||||||
</div>
|
</div>
|
||||||
<n8n-text-stub size=\\"small\\" tag=\\"span\\">This is a danger callout.</n8n-text-stub>
|
<n8n-text-stub size=\\"small\\">This is a danger callout.</n8n-text-stub>
|
||||||
</div>
|
</div>
|
||||||
</div>"
|
</div>"
|
||||||
`;
|
`;
|
||||||
|
@ -40,7 +40,7 @@ exports[`components > N8nCallout > should render info theme correctly 1`] = `
|
||||||
<div class=\\"_icon_p74de_40\\">
|
<div class=\\"_icon_p74de_40\\">
|
||||||
<n8n-icon-stub icon=\\"info-circle\\" size=\\"large\\"></n8n-icon-stub>
|
<n8n-icon-stub icon=\\"info-circle\\" size=\\"large\\"></n8n-icon-stub>
|
||||||
</div>
|
</div>
|
||||||
<n8n-text-stub size=\\"small\\" tag=\\"span\\">This is an info callout.</n8n-text-stub>
|
<n8n-text-stub size=\\"small\\">This is an info callout.</n8n-text-stub>
|
||||||
</div>
|
</div>
|
||||||
</div>"
|
</div>"
|
||||||
`;
|
`;
|
||||||
|
@ -51,7 +51,7 @@ exports[`components > N8nCallout > should render secondary theme correctly 1`] =
|
||||||
<div class=\\"_icon_p74de_40\\">
|
<div class=\\"_icon_p74de_40\\">
|
||||||
<n8n-icon-stub icon=\\"info-circle\\" size=\\"medium\\"></n8n-icon-stub>
|
<n8n-icon-stub icon=\\"info-circle\\" size=\\"medium\\"></n8n-icon-stub>
|
||||||
</div>
|
</div>
|
||||||
<n8n-text-stub size=\\"small\\" tag=\\"span\\">This is a secondary callout.</n8n-text-stub>
|
<n8n-text-stub size=\\"small\\">This is a secondary callout.</n8n-text-stub>
|
||||||
</div>
|
</div>
|
||||||
</div>"
|
</div>"
|
||||||
`;
|
`;
|
||||||
|
@ -62,7 +62,7 @@ exports[`components > N8nCallout > should render success theme correctly 1`] = `
|
||||||
<div class=\\"_icon_p74de_40\\">
|
<div class=\\"_icon_p74de_40\\">
|
||||||
<n8n-icon-stub icon=\\"check-circle\\" size=\\"large\\"></n8n-icon-stub>
|
<n8n-icon-stub icon=\\"check-circle\\" size=\\"large\\"></n8n-icon-stub>
|
||||||
</div>
|
</div>
|
||||||
<n8n-text-stub size=\\"small\\" tag=\\"span\\">This is a success callout.</n8n-text-stub>
|
<n8n-text-stub size=\\"small\\">This is a success callout.</n8n-text-stub>
|
||||||
</div>
|
</div>
|
||||||
</div>"
|
</div>"
|
||||||
`;
|
`;
|
||||||
|
@ -73,7 +73,7 @@ exports[`components > N8nCallout > should render warning theme correctly 1`] = `
|
||||||
<div class=\\"_icon_p74de_40\\">
|
<div class=\\"_icon_p74de_40\\">
|
||||||
<n8n-icon-stub icon=\\"exclamation-triangle\\" size=\\"large\\"></n8n-icon-stub>
|
<n8n-icon-stub icon=\\"exclamation-triangle\\" size=\\"large\\"></n8n-icon-stub>
|
||||||
</div>
|
</div>
|
||||||
<n8n-text-stub size=\\"small\\" tag=\\"span\\">This is a warning callout.</n8n-text-stub>
|
<n8n-text-stub size=\\"small\\">This is a warning callout.</n8n-text-stub>
|
||||||
</div>
|
</div>
|
||||||
</div>"
|
</div>"
|
||||||
`;
|
`;
|
||||||
|
|
|
@ -24,7 +24,7 @@ import N8nInputLabel from '../N8nInputLabel';
|
||||||
export default Vue.extend({
|
export default Vue.extend({
|
||||||
name: 'n8n-checkbox',
|
name: 'n8n-checkbox',
|
||||||
components: {
|
components: {
|
||||||
ElCheckbox,
|
ElCheckbox, // eslint-disable-line @typescript-eslint/no-unsafe-assignment
|
||||||
N8nInputLabel,
|
N8nInputLabel,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
|
@ -59,7 +59,7 @@ export default Vue.extend({
|
||||||
onChange(event: Event) {
|
onChange(event: Event) {
|
||||||
this.$emit("input", event);
|
this.$emit("input", event);
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -179,7 +179,7 @@ export default mixins(Locale).extend({
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
getValidationError(): ReturnType<IValidator['validate']> {
|
getValidationError(): ReturnType<IValidator['validate']> {
|
||||||
const rules = (this.validationRules || []) as (Rule | RuleGroup)[];
|
const rules = (this.validationRules || []) as Array<Rule | RuleGroup>;
|
||||||
const validators = {
|
const validators = {
|
||||||
...VALIDATORS,
|
...VALIDATORS,
|
||||||
...(this.validators || {}),
|
...(this.validators || {}),
|
||||||
|
|
|
@ -67,7 +67,7 @@ export default Vue.extend({
|
||||||
});
|
});
|
||||||
|
|
||||||
if (this.eventBus) {
|
if (this.eventBus) {
|
||||||
this.eventBus.$on('submit', this.onSubmit);
|
this.eventBus.$on('submit', this.onSubmit); // eslint-disable-line @typescript-eslint/unbound-method
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
@ -75,11 +75,11 @@ export default Vue.extend({
|
||||||
return (this.inputs as IFormInput[]).filter(
|
return (this.inputs as IFormInput[]).filter(
|
||||||
(input) => typeof input.shouldDisplay === 'function'
|
(input) => typeof input.shouldDisplay === 'function'
|
||||||
? input.shouldDisplay(this.values)
|
? input.shouldDisplay(this.values)
|
||||||
: true
|
: true,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
isReadyToSubmit(): boolean {
|
isReadyToSubmit(): boolean {
|
||||||
for (let key in this.validity) {
|
for (const key in this.validity) {
|
||||||
if (!this.validity[key]) {
|
if (!this.validity[key]) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -92,9 +92,9 @@ export default Vue.extend({
|
||||||
onInput(name: string, value: any) {
|
onInput(name: string, value: any) {
|
||||||
this.values = {
|
this.values = {
|
||||||
...this.values,
|
...this.values,
|
||||||
[name]: value,
|
[name]: value, // eslint-disable-line @typescript-eslint/no-unsafe-assignment
|
||||||
};
|
};
|
||||||
this.$emit('input', {name, value});
|
this.$emit('input', {name, value}); // eslint-disable-line @typescript-eslint/no-unsafe-assignment
|
||||||
},
|
},
|
||||||
onValidate(name: string, valid: boolean) {
|
onValidate(name: string, valid: boolean) {
|
||||||
Vue.set(this.validity, name, valid);
|
Vue.set(this.validity, name, valid);
|
||||||
|
@ -102,7 +102,7 @@ export default Vue.extend({
|
||||||
onSubmit() {
|
onSubmit() {
|
||||||
this.showValidationWarnings = true;
|
this.showValidationWarnings = true;
|
||||||
if (this.isReadyToSubmit) {
|
if (this.isReadyToSubmit) {
|
||||||
const toSubmit = (this.filteredInputs as IFormInput[]).reduce<{ [key: string]: unknown }>((accu, input) => {
|
const toSubmit = (this.filteredInputs ).reduce<{ [key: string]: unknown }>((accu, input) => {
|
||||||
if (this.values[input.name]) {
|
if (this.values[input.name]) {
|
||||||
accu[input.name] = this.values[input.name];
|
accu[input.name] = this.values[input.name];
|
||||||
}
|
}
|
||||||
|
|
|
@ -47,7 +47,7 @@ export default Vue.extend({
|
||||||
applied.push(this.bold? 'bold': 'regular');
|
applied.push(this.bold? 'bold': 'regular');
|
||||||
|
|
||||||
return applied.map((c) => (this.$style as { [key: string]: string })[c]);
|
return applied.map((c) => (this.$style as { [key: string]: string })[c]);
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -29,7 +29,7 @@ import Vue from 'vue';
|
||||||
export default Vue.extend({
|
export default Vue.extend({
|
||||||
name: 'n8n-input',
|
name: 'n8n-input',
|
||||||
components: {
|
components: {
|
||||||
ElInput,
|
ElInput, // eslint-disable-line @typescript-eslint/no-unsafe-assignment
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
value: {
|
value: {
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import N8nInputNumber from 'element-ui/lib/input-number';
|
import N8nInputNumber from 'element-ui/lib/input-number';
|
||||||
|
|
||||||
N8nInputNumber.name = 'n8n-input-number';
|
N8nInputNumber.name = 'n8n-input-number'; // eslint-disable-line @typescript-eslint/no-unsafe-member-access
|
||||||
|
|
||||||
export default N8nInputNumber;
|
export default N8nInputNumber;
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -48,8 +48,8 @@ import Vue from 'vue';
|
||||||
export default Vue.extend({
|
export default Vue.extend({
|
||||||
name: 'n8n-loading',
|
name: 'n8n-loading',
|
||||||
components: {
|
components: {
|
||||||
ElSkeleton,
|
ElSkeleton, // eslint-disable-line @typescript-eslint/no-unsafe-assignment
|
||||||
ElSkeletonItem,
|
ElSkeletonItem, // eslint-disable-line @typescript-eslint/no-unsafe-assignment
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
animated: {
|
animated: {
|
||||||
|
|
|
@ -25,9 +25,13 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import N8nLoading from '../N8nLoading';
|
import N8nLoading from '../N8nLoading';
|
||||||
import Markdown from 'markdown-it';
|
import Markdown from 'markdown-it';
|
||||||
const markdownLink = require('markdown-it-link-attributes');
|
|
||||||
const markdownEmoji = require('markdown-it-emoji');
|
// @ts-ignore
|
||||||
const markdownTasklists = require('markdown-it-task-lists');
|
import markdownLink from 'markdown-it-link-attributes';
|
||||||
|
// @ts-ignore
|
||||||
|
import markdownEmoji from 'markdown-it-emoji';
|
||||||
|
// @ts-ignore
|
||||||
|
import markdownTasklists from 'markdown-it-task-lists';
|
||||||
|
|
||||||
import xss, { friendlyAttrValue } from 'xss';
|
import xss, { friendlyAttrValue } from 'xss';
|
||||||
import { escapeMarkdown } from '../../utils/markdown';
|
import { escapeMarkdown } from '../../utils/markdown';
|
||||||
|
@ -143,8 +147,8 @@ export default Vue.extend({
|
||||||
}
|
}
|
||||||
// Return nothing, means keep the default handling measure
|
// Return nothing, means keep the default handling measure
|
||||||
},
|
},
|
||||||
onTag: function (tag, html, options) {
|
onTag (tag, code, options) {
|
||||||
if (tag === 'img' && html.includes(`alt="workflow-screenshot"`)) {
|
if (tag === 'img' && code.includes(`alt="workflow-screenshot"`)) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
// return nothing, keep tag
|
// return nothing, keep tag
|
||||||
|
@ -156,10 +160,10 @@ export default Vue.extend({
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
md: new Markdown(this.options.markdown)
|
md: new Markdown(this.options.markdown) // eslint-disable-line @typescript-eslint/no-unsafe-member-access
|
||||||
.use(markdownLink, this.options.linkAttributes)
|
.use(markdownLink, this.options.linkAttributes) // eslint-disable-line @typescript-eslint/no-unsafe-member-access
|
||||||
.use(markdownEmoji)
|
.use(markdownEmoji)
|
||||||
.use(markdownTasklists, this.options.tasklists),
|
.use(markdownTasklists, this.options.tasklists), // eslint-disable-line @typescript-eslint/no-unsafe-member-access
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
@ -177,8 +181,8 @@ export default Vue.extend({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.$emit('markdown-click', clickedLink, event);
|
this.$emit('markdown-click', clickedLink, event);
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -37,7 +37,7 @@ export default Vue.extend({
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
ElMenu,
|
ElMenu, // eslint-disable-line @typescript-eslint/no-unsafe-assignment
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import ElMenuItem from 'element-ui/lib/menu-item';
|
import ElMenuItem from 'element-ui/lib/menu-item';
|
||||||
|
|
||||||
ElMenuItem.name = 'n8n-menu-item';
|
ElMenuItem.name = 'n8n-menu-item'; // eslint-disable-line @typescript-eslint/no-unsafe-member-access
|
||||||
|
|
||||||
export default ElMenuItem;
|
export default ElMenuItem;
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -58,7 +58,7 @@ export default Vue.extend({
|
||||||
classes(): string[] {
|
classes(): string[] {
|
||||||
return [
|
return [
|
||||||
'notice',
|
'notice',
|
||||||
this.$style['notice'],
|
this.$style.notice,
|
||||||
this.$style[this.theme],
|
this.$style[this.theme],
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import ElOption from 'element-ui/lib/option';
|
import ElOption from 'element-ui/lib/option';
|
||||||
|
|
||||||
ElOption.name = 'n8n-option';
|
ElOption.name = 'n8n-option'; // eslint-disable-line @typescript-eslint/no-unsafe-member-access
|
||||||
|
|
||||||
export default ElOption;
|
export default ElOption;
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import ElPopover from 'element-ui/lib/popover';
|
import ElPopover from 'element-ui/lib/popover';
|
||||||
|
|
||||||
ElPopover.name = 'n8n-popover';
|
ElPopover.name = 'n8n-popover'; // eslint-disable-line @typescript-eslint/no-unsafe-member-access
|
||||||
|
|
||||||
export default ElPopover;
|
export default ElPopover;
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -12,6 +12,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
/* eslint-disable @typescript-eslint/unbound-method */
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
|
|
||||||
function closestNumber(value: number, divisor: number): number {
|
function closestNumber(value: number, divisor: number): number {
|
||||||
|
@ -94,7 +95,7 @@ export default Vue.extend({
|
||||||
enabledDirections() {
|
enabledDirections() {
|
||||||
const availableDirections = Object.keys(directionsCursorMaps);
|
const availableDirections = Object.keys(directionsCursorMaps);
|
||||||
|
|
||||||
if(this.isResizingEnabled === false) return [];
|
if(!this.isResizingEnabled) return [];
|
||||||
if(this.supportedDirections.length === 0) return availableDirections;
|
if(this.supportedDirections.length === 0) return availableDirections;
|
||||||
|
|
||||||
return this.supportedDirections;
|
return this.supportedDirections;
|
||||||
|
|
|
@ -39,7 +39,7 @@ export default Vue.extend({
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (typeof this.to === 'string') {
|
if (typeof this.to === 'string') {
|
||||||
return this.to.startsWith('/');
|
return (this.to as string).startsWith('/');
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.to !== undefined;
|
return this.to !== undefined;
|
||||||
|
@ -49,7 +49,7 @@ export default Vue.extend({
|
||||||
return this.newWindow;
|
return this.newWindow;
|
||||||
}
|
}
|
||||||
if (typeof this.to === 'string') {
|
if (typeof this.to === 'string') {
|
||||||
return !this.to.startsWith('/');
|
return !(this.to as string).startsWith('/');
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
|
|
|
@ -38,7 +38,7 @@ export interface IProps {
|
||||||
export default Vue.extend({
|
export default Vue.extend({
|
||||||
name: 'n8n-select',
|
name: 'n8n-select',
|
||||||
components: {
|
components: {
|
||||||
ElSelect,
|
ElSelect, // eslint-disable-line @typescript-eslint/no-unsafe-assignment
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
value: {
|
value: {
|
||||||
|
|
|
@ -23,13 +23,13 @@ export default Vue.extend({
|
||||||
props: {
|
props: {
|
||||||
size: {
|
size: {
|
||||||
type: String,
|
type: String,
|
||||||
validator: function (value: string): boolean {
|
validator (value: string): boolean {
|
||||||
return ['small', 'medium', 'large'].includes(value);
|
return ['small', 'medium', 'large'].includes(value);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
type: {
|
type: {
|
||||||
type: String,
|
type: String,
|
||||||
validator: function (value: string): boolean {
|
validator (value: string): boolean {
|
||||||
return ['dots', 'ring'].includes(value);
|
return ['dots', 'ring'].includes(value);
|
||||||
},
|
},
|
||||||
default: 'dots',
|
default: 'dots',
|
||||||
|
|
|
@ -144,8 +144,8 @@ export default mixins(Locale).extend({
|
||||||
},
|
},
|
||||||
styles(): { height: string, width: string } {
|
styles(): { height: string, width: string } {
|
||||||
return {
|
return {
|
||||||
height: this.resHeight + 'px',
|
height: `${this.resHeight}px`,
|
||||||
width: this.resWidth + 'px',
|
width: `${this.resWidth}px`,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
shouldShowFooter(): boolean {
|
shouldShowFooter(): boolean {
|
||||||
|
|
|
@ -57,7 +57,8 @@ export default Vue.extend({
|
||||||
const width = container.clientWidth;
|
const width = container.clientWidth;
|
||||||
const scrollWidth = container.scrollWidth;
|
const scrollWidth = container.scrollWidth;
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
this.scrollPosition = event.srcElement.scrollLeft;
|
this.scrollPosition = event.srcElement.scrollLeft; // eslint-disable-line @typescript-eslint/no-unsafe-assignment
|
||||||
|
|
||||||
this.canScrollRight = scrollWidth - width > this.scrollPosition;
|
this.canScrollRight = scrollWidth - width > this.scrollPosition;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -55,7 +55,7 @@ export default Vue.extend({
|
||||||
applied.push(this.bold? 'bold': 'regular');
|
applied.push(this.bold? 'bold': 'regular');
|
||||||
|
|
||||||
return applied.map((c) => (this.$style as { [key: string]: string })[c]);
|
return applied.map((c) => (this.$style as { [key: string]: string })[c]);
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import ElTooltip from 'element-ui/lib/tooltip';
|
import ElTooltip from 'element-ui/lib/tooltip';
|
||||||
|
|
||||||
ElTooltip.name = 'n8n-tooltip';
|
ElTooltip.name = 'n8n-tooltip'; // eslint-disable-line @typescript-eslint/no-unsafe-member-access
|
||||||
|
|
||||||
export default ElTooltip;
|
export default ElTooltip;
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -29,6 +29,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
/* tslint:disable: @typescript-eslint/no-unsafe-assignment */
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import N8nUserInfo from '../N8nUserInfo';
|
import N8nUserInfo from '../N8nUserInfo';
|
||||||
import { IUser } from '../../types';
|
import { IUser } from '../../types';
|
||||||
|
@ -42,8 +43,8 @@ export default mixins(Locale).extend({
|
||||||
name: 'n8n-user-select',
|
name: 'n8n-user-select',
|
||||||
components: {
|
components: {
|
||||||
N8nUserInfo,
|
N8nUserInfo,
|
||||||
ElSelect,
|
ElSelect, // eslint-disable-line @typescript-eslint/no-unsafe-assignment
|
||||||
ElOption,
|
ElOption, // eslint-disable-line @typescript-eslint/no-unsafe-assignment
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
users: {
|
users: {
|
||||||
|
@ -104,7 +105,7 @@ export default mixins(Locale).extend({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
sortedUsers(): IUser[] {
|
sortedUsers(): IUser[] {
|
||||||
return [...(this.fitleredUsers as IUser[])].sort((a: IUser, b: IUser) => {
|
return [...(this.fitleredUsers )].sort((a: IUser, b: IUser) => {
|
||||||
if (a.lastName && b.lastName && a.lastName !== b.lastName) {
|
if (a.lastName && b.lastName && a.lastName !== b.lastName) {
|
||||||
return a.lastName > b.lastName ? 1 : -1;
|
return a.lastName > b.lastName ? 1 : -1;
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,6 +36,11 @@ import Locale from '../../mixins/locale';
|
||||||
import mixins from 'vue-typed-mixins';
|
import mixins from 'vue-typed-mixins';
|
||||||
import { t } from '../../locale';
|
import { t } from '../../locale';
|
||||||
|
|
||||||
|
export interface IUserListAction {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
export default mixins(Locale).extend({
|
export default mixins(Locale).extend({
|
||||||
name: 'n8n-users-list',
|
name: 'n8n-users-list',
|
||||||
components: {
|
components: {
|
||||||
|
@ -102,19 +107,19 @@ export default mixins(Locale).extend({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return a.email! > b.email! ? 1 : -1;
|
return a.email > b.email ? 1 : -1;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
getActions(user: IUser): Array<{ label: string, value: string }> {
|
getActions(user: IUser): IUserListAction[] {
|
||||||
const DELETE = {
|
const DELETE: IUserListAction = {
|
||||||
label: this.deleteLabel,
|
label: this.deleteLabel as string,
|
||||||
value: 'delete',
|
value: 'delete',
|
||||||
};
|
};
|
||||||
|
|
||||||
const REINVITE = {
|
const REINVITE: IUserListAction = {
|
||||||
label: this.reinviteLabel,
|
label: this.reinviteLabel as string,
|
||||||
value: 'reinvite',
|
value: 'reinvite',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -64,7 +64,7 @@ export default Vue.extend({
|
||||||
},
|
},
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
if (this.$props.enabled) {
|
if (this.$props.enabled) {
|
||||||
this.$data.observer.disconnect();
|
this.$data.observer.disconnect(); // eslint-disable-line
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
3
packages/design-system/src/css/base.scss
Normal file
3
packages/design-system/src/css/base.scss
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
@forward "common/var.scss";
|
||||||
|
@import "common/transition.scss";
|
||||||
|
@import "icon.scss";
|
|
@ -10,3 +10,12 @@
|
||||||
@use "./input.scss";
|
@use "./input.scss";
|
||||||
@use "./scrollbar.scss";
|
@use "./scrollbar.scss";
|
||||||
@use "./popper";
|
@use "./popper";
|
||||||
|
|
||||||
|
.el-picker-panel__footer {
|
||||||
|
.el-picker-panel__link-btn {
|
||||||
|
&:last-child {
|
||||||
|
background: var(--color-primary);
|
||||||
|
color: var(--color-foreground-xlight)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue