diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 286cf7a98e..e98f9c97ce 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,7 +2,6 @@ Great that you are here and you want to contribute to n8n - ## Contents - [Code of Conduct](#code-of-conduct) @@ -15,16 +14,14 @@ Great that you are here and you want to contribute to n8n - [Extend Documentation](#extend-documentation) - [Contributor License Agreement](#contributor-license-agreement) - -## Code of Conduct +## Code of conduct This project and everyone participating in it are governed by the Code of Conduct which can be found in the file [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md). By participating, you are expected to uphold this code. Please report unacceptable behavior to jan@n8n.io. - -## Directory Structure +## Directory structure n8n is split up in different modules which are all in a single mono repository. @@ -36,7 +33,8 @@ The most important directories: - [/packages/cli](/packages/cli) - CLI code to run front- & backend - [/packages/core](/packages/core) - Core code which handles workflow execution, active webhooks and - workflows + workflows. **Contact n8n before + starting on any changes here** - [/packages/design-system](/packages/design-system) - Vue frontend components - [/packages/editor-ui](/packages/editor-ui) - Vue frontend workflow editor - [/packages/node-dev](/packages/node-dev) - CLI to create new n8n-nodes @@ -44,17 +42,14 @@ The most important directories: - [/packages/workflow](/packages/workflow) - Workflow code with interfaces which get used by front- & backend - -## Development Setup +## Development setup If you want to change or extend n8n you have to make sure that all needed dependencies are installed and the packages get linked correctly. Here a short guide on how that can be done: - ### Requirements - -#### Build Tools +#### Build tools The packages which n8n uses depend on a few build tools: @@ -86,18 +81,23 @@ So for the setup to work correctly lerna has to be installed globally like this: npm install -g lerna ``` - ### Actual n8n setup > **IMPORTANT**: All the steps bellow have to get executed at least once to get the development setup up and running! - Now that everything n8n requires to run is installed the actual n8n code can be checked out and set up: -1. Clone the repository +1. [Fork](https://guides.github.com/activities/forking/#fork) the n8n repository + +1. Clone your forked repository ``` - git clone https://github.com/n8n-io/n8n.git + git clone https://github.com//n8n.git + ``` + +1. Add the original n8n repository as `upstream` to your forked repository + ``` + git remote add upstream https://github.com/n8n-io/n8n.git ``` 1. Go into repository folder @@ -115,8 +115,6 @@ checked out and set up: npm run build ``` - - ### Start To start n8n execute: @@ -130,7 +128,7 @@ To start n8n with tunnel: ./packages/cli/bin/n8n start --tunnel ``` -## Development Cycle +## Development cycle While iterating on n8n modules code, you can run `npm run dev`. It will then automatically build your code, restart the backend and refresh the frontend @@ -147,12 +145,11 @@ automatically build your code, restart the backend and refresh the frontend npm run start ``` 1. Create tests -1. Run all tests +1. Run all [tests](#test-suite) ``` npm run test ``` -1. Commit code and create pull request - +1. Commit code and [create a pull request](https://docs.github.com/en/github/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request-from-a-fork) ### Test suite @@ -165,34 +162,26 @@ If that gets executed in one of the package folders it will only run the tests of this package. If it gets executed in the n8n-root folder it will run all tests of all packages. +## Create custom nodes - -## Create Custom Nodes +> **IMPORTANT**: Avoid use of external libraries to ensure your custom nodes can be reviewed and merged quickly. Learn about [using the node dev CLI](https://docs.n8n.io/nodes/creating-nodes/node-dev-cli.html) to create custom nodes for n8n. -More information can -be found in the documentation of [n8n-node-dev](https://github.com/n8n-io/n8n/tree/master/packages/node-dev), which is a small CLI which -helps with n8n-node-development. - - +More information can be found in the documentation of [n8n-node-dev](https://github.com/n8n-io/n8n/tree/master/packages/node-dev), a small CLI which helps with n8n-node-development. ## Create a new node to contribute to n8n Follow this tutorial on [creating your first node](https://docs.n8n.io/nodes/creating-nodes/create-node.html) for n8n. - - ## Checklist before submitting a new node There are several things to keep in mind when creating a node. To help you, we prepared a [checklist](https://docs.n8n.io/nodes/creating-nodes/node-review-checklist.html) that covers the requirements for creating nodes, from preparation to submission. This will help us be quicker to review and merge your PR. - -## Extend Documentation +## Extend documentation The repository for the n8n documentation on [docs.n8n.io](https://docs.n8n.io) can be found [here](https://github.com/n8n-io/n8n-docs). - ## Contributor License Agreement That we do not have any potential problems later it is sadly necessary to sign a [Contributor License Agreement](CONTRIBUTOR_LICENSE_AGREEMENT.md). That can be done literally with the push of a button. diff --git a/package.json b/package.json index 802fa4f791..10abc22a4c 100644 --- a/package.json +++ b/package.json @@ -8,8 +8,8 @@ "dev": "lerna exec npm run dev --parallel", "clean:dist": "lerna exec -- rimraf ./dist", "format": "lerna exec npm run format", - "lint": "lerna exec npm run lint", - "lintfix": "lerna exec npm run lintfix", + "lint": "lerna exec npm run lint", + "lintfix": "lerna exec npm run lintfix", "optimize-svg": "find ./packages -name '*.svg' ! -name 'pipedrive.svg' -print0 | xargs -0 -P16 -L20 npx svgo", "start": "run-script-os", "start:default": "cd packages/cli/bin && ./n8n", diff --git a/packages/cli/config/index.ts b/packages/cli/config/index.ts index 2ada64eed9..40cd13093a 100644 --- a/packages/cli/config/index.ts +++ b/packages/cli/config/index.ts @@ -148,6 +148,12 @@ const config = convict({ env: 'CREDENTIALS_OVERWRITE_ENDPOINT', }, }, + defaultName: { + doc: 'Default name for credentials', + format: String, + default: 'My credentials', + env: 'CREDENTIALS_DEFAULT_NAME', + }, }, workflows: { diff --git a/packages/cli/package.json b/packages/cli/package.json index 02f8786ad4..23aa8c3b02 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -88,7 +88,7 @@ "basic-auth": "^2.0.1", "bcryptjs": "^2.4.3", "body-parser": "^1.18.3", - "body-parser-xml": "^1.1.0", + "body-parser-xml": "^2.0.3", "bull": "^3.19.0", "callsites": "^3.1.0", "class-validator": "^0.13.1", @@ -106,6 +106,7 @@ "json-diff": "^0.5.4", "jsonwebtoken": "^8.5.1", "jwks-rsa": "~1.12.1", + "libphonenumber-js": "1.9.7", "localtunnel": "^2.0.0", "lodash.get": "^4.4.2", "mysql2": "~2.3.0", diff --git a/packages/cli/src/GenericHelpers.ts b/packages/cli/src/GenericHelpers.ts index 82cf992b07..7fe8c50ac4 100644 --- a/packages/cli/src/GenericHelpers.ts +++ b/packages/cli/src/GenericHelpers.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ /* eslint-disable @typescript-eslint/restrict-template-expressions */ /* eslint-disable @typescript-eslint/no-unsafe-return */ /* eslint-disable no-param-reassign */ @@ -11,7 +12,11 @@ import { IDataObject } from 'n8n-workflow'; import * as config from '../config'; // eslint-disable-next-line import/no-cycle -import { IPackageVersions } from '.'; +import { Db, ICredentialsDb, IPackageVersions } from '.'; +// eslint-disable-next-line import/order +import { Like } from 'typeorm'; +// eslint-disable-next-line import/no-cycle +import { WorkflowEntity } from './databases/entities/WorkflowEntity'; let versionCache: IPackageVersions | undefined; @@ -170,3 +175,56 @@ export function getConfigValueSync(configKey: string): string | boolean | number return data; } + +/** + * Generate a unique name for a workflow or credentials entity. + * + * - If the name does not yet exist, it returns the requested name. + * - If the name already exists once, it returns the requested name suffixed with 2. + * - If the name already exists more than once with suffixes, it looks for the max suffix + * and returns the requested name with max suffix + 1. + */ +// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types +export async function generateUniqueName( + requestedName: string, + entityType: 'workflow' | 'credentials', +) { + const findConditions = { + select: ['name' as const], + where: { + name: Like(`${requestedName}%`), + }, + }; + + const found: Array = + entityType === 'workflow' + ? await Db.collections.Workflow!.find(findConditions) + : await Db.collections.Credentials!.find(findConditions); + + // name is unique + if (found.length === 0) { + return { name: requestedName }; + } + + const maxSuffix = found.reduce((acc, { name }) => { + const parts = name.split(`${requestedName} `); + + if (parts.length > 2) return acc; + + const suffix = Number(parts[1]); + + // eslint-disable-next-line no-restricted-globals + if (!isNaN(suffix) && Math.ceil(suffix) > acc) { + acc = Math.ceil(suffix); + } + + return acc; + }, 0); + + // name is duplicate but no numeric suffixes exist yet + if (maxSuffix === 0) { + return { name: `${requestedName} 2` }; + } + + return { name: `${requestedName} ${maxSuffix + 1}` }; +} diff --git a/packages/cli/src/Interfaces.ts b/packages/cli/src/Interfaces.ts index 23c71ecceb..13948d6079 100644 --- a/packages/cli/src/Interfaces.ts +++ b/packages/cli/src/Interfaces.ts @@ -121,6 +121,7 @@ export interface ICredentialsBase { export interface ICredentialsDb extends ICredentialsBase, ICredentialsEncrypted { id: number | string; + name: string; } export interface ICredentialsResponse extends ICredentialsDb { diff --git a/packages/cli/src/LoadNodesAndCredentials.ts b/packages/cli/src/LoadNodesAndCredentials.ts index d39453fec8..68715fe47a 100644 --- a/packages/cli/src/LoadNodesAndCredentials.ts +++ b/packages/cli/src/LoadNodesAndCredentials.ts @@ -151,6 +151,14 @@ class LoadNodesAndCredentialsClass { let tempCredential: ICredentialType; try { tempCredential = new tempModule[credentialName]() as ICredentialType; + + if (tempCredential.icon && tempCredential.icon.startsWith('file:')) { + // If a file icon gets used add the full path + tempCredential.icon = `file:${path.join( + path.dirname(filePath), + tempCredential.icon.substr(5), + )}`; + } } catch (e) { if (e instanceof TypeError) { throw new Error( diff --git a/packages/cli/src/Server.ts b/packages/cli/src/Server.ts index aaec3bf56d..5ce0426811 100644 --- a/packages/cli/src/Server.ts +++ b/packages/cli/src/Server.ts @@ -52,9 +52,16 @@ import { createHash, createHmac } from 'crypto'; import { compare } from 'bcryptjs'; import * as promClient from 'prom-client'; -import { Credentials, LoadNodeParameterOptions, UserSettings } from 'n8n-core'; +import { + Credentials, + ICredentialTestFunctions, + LoadNodeParameterOptions, + NodeExecuteFunctions, + UserSettings, +} from 'n8n-core'; import { + ICredentialsDecrypted, ICredentialsEncrypted, ICredentialType, IDataObject, @@ -66,6 +73,8 @@ import { IWorkflowBase, IWorkflowCredentials, LoggerProxy, + NodeCredentialTestRequest, + NodeCredentialTestResult, Workflow, WorkflowExecuteMode, } from 'n8n-workflow'; @@ -131,7 +140,7 @@ import * as config from '../config'; import * as TagHelpers from './TagHelpers'; import { TagEntity } from './databases/entities/TagEntity'; import { WorkflowEntity } from './databases/entities/WorkflowEntity'; -import { WorkflowNameRequest } from './WorkflowHelpers'; +import { NameRequest } from './WorkflowHelpers'; require('body-parser-xml')(bodyParser); @@ -156,6 +165,8 @@ class App { defaultWorkflowName: string; + defaultCredentialsName: string; + saveDataErrorExecution: string; saveDataSuccessExecution: string; @@ -196,6 +207,7 @@ class App { this.endpointWebhookTest = config.get('endpoints.webhookTest') as string; this.defaultWorkflowName = config.get('workflows.defaultName') as string; + this.defaultCredentialsName = config.get('credentials.defaultName') as string; this.saveDataErrorExecution = config.get('executions.saveDataOnError') as string; this.saveDataSuccessExecution = config.get('executions.saveDataOnSuccess') as string; @@ -527,6 +539,7 @@ class App { // support application/x-www-form-urlencoded post data this.app.use( bodyParser.urlencoded({ + limit: `${this.payloadSizeMax}mb`, extended: false, verify: (req, res, buf) => { // @ts-ignore @@ -719,41 +732,11 @@ class App { this.app.get( `/${this.restEndpoint}/workflows/new`, ResponseHelper.send( - async (req: WorkflowNameRequest, res: express.Response): Promise<{ name: string }> => { - const nameToReturn = + async (req: NameRequest, res: express.Response): Promise<{ name: string }> => { + const requestedName = req.query.name && req.query.name !== '' ? req.query.name : this.defaultWorkflowName; - const workflows = await Db.collections.Workflow!.find({ - select: ['name'], - where: { name: Like(`${nameToReturn}%`) }, - }); - - // name is unique - if (workflows.length === 0) { - return { name: nameToReturn }; - } - - const maxSuffix = workflows.reduce((acc: number, { name }) => { - const parts = name.split(`${nameToReturn} `); - - if (parts.length > 2) return acc; - - const suffix = Number(parts[1]); - - // eslint-disable-next-line no-restricted-globals - if (!isNaN(suffix) && Math.ceil(suffix) > acc) { - acc = Math.ceil(suffix); - } - - return acc; - }, 0); - - // name is duplicate but no numeric suffixes exist yet - if (maxSuffix === 0) { - return { name: `${nameToReturn} 2` }; - } - - return { name: `${nameToReturn} ${maxSuffix + 1}` }; + return await GenericHelpers.generateUniqueName(requestedName, 'workflow'); }, ), ); @@ -1236,6 +1219,18 @@ class App { // Credentials // ---------------------------------------- + this.app.get( + `/${this.restEndpoint}/credentials/new`, + ResponseHelper.send( + async (req: NameRequest, res: express.Response): Promise<{ name: string }> => { + const requestedName = + req.query.name && req.query.name !== '' ? req.query.name : this.defaultCredentialsName; + + return await GenericHelpers.generateUniqueName(requestedName, 'credentials'); + }, + ), + ); + // Deletes a specific credential this.app.delete( `/${this.restEndpoint}/credentials/:id`, @@ -1322,6 +1317,67 @@ class App { ), ); + // Test credentials + this.app.post( + `/${this.restEndpoint}/credentials-test`, + ResponseHelper.send( + async (req: express.Request, res: express.Response): Promise => { + const incomingData = req.body as NodeCredentialTestRequest; + const credentialType = incomingData.credentials.type; + + // Find nodes that can test this credential. + const nodeTypes = NodeTypes(); + const allNodes = nodeTypes.getAll(); + + let foundTestFunction: + | (( + this: ICredentialTestFunctions, + credential: ICredentialsDecrypted, + ) => Promise) + | undefined; + const nodeThatCanTestThisCredential = allNodes.find((node) => { + if ( + incomingData.nodeToTestWith && + node.description.name !== incomingData.nodeToTestWith + ) { + return false; + } + const credentialTestable = node.description.credentials?.find((credential) => { + const testFunctionSearch = + credential.name === credentialType && !!credential.testedBy; + if (testFunctionSearch) { + foundTestFunction = node.methods!.credentialTest![credential.testedBy!]; + } + return testFunctionSearch; + }); + return !!credentialTestable; + }); + + if (!nodeThatCanTestThisCredential) { + return Promise.resolve({ + status: 'Error', + message: 'There are no nodes that can test this credential.', + }); + } + + if (foundTestFunction === undefined) { + return Promise.resolve({ + status: 'Error', + message: 'No testing function found for this credential.', + }); + } + + const credentialTestFunctions = NodeExecuteFunctions.getCredentialTestFunctions(); + + const output = await foundTestFunction.call( + credentialTestFunctions, + incomingData.credentials, + ); + return Promise.resolve(output); + }, + ), + ); + // Updates existing credentials this.app.patch( `/${this.restEndpoint}/credentials/:id`, @@ -1539,6 +1595,42 @@ class App { ), ); + this.app.get( + `/${this.restEndpoint}/credential-icon/:credentialType`, + async (req: express.Request, res: express.Response): Promise => { + try { + const credentialName = req.params.credentialType; + + const credentialType = CredentialTypes().getByName(credentialName); + + if (credentialType === undefined) { + res.status(404).send('The credentialType is not known.'); + return; + } + + if (credentialType.icon === undefined) { + res.status(404).send('No icon found for credential.'); + return; + } + + if (!credentialType.icon.startsWith('file:')) { + res.status(404).send('Credential does not have a file icon.'); + return; + } + + const filepath = credentialType.icon.substr(5); + + const maxAge = 7 * 24 * 60 * 60 * 1000; // 7 days + res.setHeader('Cache-control', `private max-age=${maxAge}`); + + res.sendFile(filepath); + } catch (error) { + // Error response + return ResponseHelper.sendErrorResponse(res, error); + } + }, + ); + // ---------------------------------------- // OAuth1-Credential/Auth // ---------------------------------------- diff --git a/packages/cli/src/WorkflowHelpers.ts b/packages/cli/src/WorkflowHelpers.ts index 138965994e..2a1a33edee 100644 --- a/packages/cli/src/WorkflowHelpers.ts +++ b/packages/cli/src/WorkflowHelpers.ts @@ -408,9 +408,8 @@ export function throwDuplicateEntryError(error: Error) { throw new ResponseHelper.ResponseError(errorMessage, undefined, 400); } -export type WorkflowNameRequest = Express.Request & { +export type NameRequest = Express.Request & { query: { name?: string; - offset?: string; }; }; diff --git a/packages/core/src/Interfaces.ts b/packages/core/src/Interfaces.ts index b9783c6a45..79db584e49 100644 --- a/packages/core/src/Interfaces.ts +++ b/packages/core/src/Interfaces.ts @@ -2,6 +2,7 @@ import { IAllExecuteFunctions, IBinaryData, + ICredentialTestFunctions as ICredentialTestFunctionsBase, ICredentialType, IDataObject, IExecuteFunctions as IExecuteFunctionsBase, @@ -158,6 +159,12 @@ export interface ILoadOptionsFunctions extends ILoadOptionsFunctionsBase { }; } +export interface ICredentialTestFunctions extends ICredentialTestFunctionsBase { + helpers: { + request: requestPromise.RequestPromiseAPI; + }; +} + export interface IHookFunctions extends IHookFunctionsBase { helpers: { request: requestPromise.RequestPromiseAPI; diff --git a/packages/core/src/NodeExecuteFunctions.ts b/packages/core/src/NodeExecuteFunctions.ts index fcdb728db0..0ed1d6670f 100644 --- a/packages/core/src/NodeExecuteFunctions.ts +++ b/packages/core/src/NodeExecuteFunctions.ts @@ -65,6 +65,7 @@ import { lookup } from 'mime-types'; // eslint-disable-next-line import/no-cycle import { BINARY_ENCODING, + ICredentialTestFunctions, IHookFunctions, ILoadOptionsFunctions, IResponseError, @@ -1280,6 +1281,14 @@ export function getExecuteSingleFunctions( })(workflow, runExecutionData, connectionInputData, inputData, node, itemIndex); } +export function getCredentialTestFunctions(): ICredentialTestFunctions { + return { + helpers: { + request: requestPromiseWithDefaults, + }, + }; +} + /** * Returns the execute functions regular nodes have access to in load-options-function. * diff --git a/packages/design-system/src/components/N8nButton/Button.vue b/packages/design-system/src/components/N8nButton/Button.vue index fc10624442..2827b8f8b7 100644 --- a/packages/design-system/src/components/N8nButton/Button.vue +++ b/packages/design-system/src/components/N8nButton/Button.vue @@ -6,7 +6,7 @@ :size="props.size" :loading="props.loading" :title="props.title || props.label" - :class="$style[$options.getClass(props)]" + :class="$options.getClass(props, $style)" :round="!props.circle && props.round" :circle="props.circle" :style="$options.styles(props)" @@ -91,6 +91,10 @@ export default { type: Boolean, default: false, }, + transparentBackground: { + type: Boolean, + default: false, + }, }, components: { ElButton, @@ -106,10 +110,16 @@ export default { ...(props.fullWidth ? { width: '100%' } : {}), }; }, - getClass(props: { type: string; theme?: string }): string { - return props.type === 'text' + getClass(props: { type: string; theme?: string, transparentBackground: boolean }, $style: any): string { + const theme = props.type === 'text' ? 'text' : `${props.type}-${props.theme || 'primary'}`; + + if (props.transparentBackground) { + return `${$style[theme]} ${$style['transparent']}`; + } + + return $style[theme]; }, }; @@ -289,6 +299,11 @@ $color-danger-shade: lightness( --button-active-border-color: transparent; } +.transparent { + --button-background-color: transparent; + --button-active-background-color: transparent; +} + .icon { display: inline-flex; diff --git a/packages/design-system/src/components/N8nInfoTip/InfoTip.stories.js b/packages/design-system/src/components/N8nInfoTip/InfoTip.stories.js new file mode 100644 index 0000000000..382a46dac6 --- /dev/null +++ b/packages/design-system/src/components/N8nInfoTip/InfoTip.stories.js @@ -0,0 +1,17 @@ +import N8nInfoTip from './InfoTip.vue'; + +export default { + title: 'Atoms/InfoTip', + component: N8nInfoTip, +}; + +const Template = (args, { argTypes }) => ({ + props: Object.keys(argTypes), + components: { + N8nInfoTip, + }, + template: + 'Need help doing something? Open docs', +}); + +export const InputLabel = Template.bind({}); diff --git a/packages/design-system/src/components/N8nInfoTip/InfoTip.vue b/packages/design-system/src/components/N8nInfoTip/InfoTip.vue new file mode 100644 index 0000000000..049e29e56a --- /dev/null +++ b/packages/design-system/src/components/N8nInfoTip/InfoTip.vue @@ -0,0 +1,36 @@ + + + + + diff --git a/packages/design-system/src/components/N8nInfoTip/index.js b/packages/design-system/src/components/N8nInfoTip/index.js new file mode 100644 index 0000000000..f91ddecfd4 --- /dev/null +++ b/packages/design-system/src/components/N8nInfoTip/index.js @@ -0,0 +1,3 @@ +import InfoTip from './InfoTip.vue'; + +export default InfoTip; diff --git a/packages/design-system/src/components/N8nInputLabel/InputLabel.vue b/packages/design-system/src/components/N8nInputLabel/InputLabel.vue index c800f73de8..f2cb6a2180 100644 --- a/packages/design-system/src/components/N8nInputLabel/InputLabel.vue +++ b/packages/design-system/src/components/N8nInputLabel/InputLabel.vue @@ -1,16 +1,18 @@ @@ -20,6 +22,8 @@ import Vue from 'vue'; import N8nTooltip from '../N8nTooltip'; import N8nIcon from '../N8nIcon'; +import { addTargetBlank } from '../utils/helpers'; + Vue.component('N8nIcon', N8nIcon); Vue.component('N8nTooltip', N8nTooltip); @@ -33,6 +37,12 @@ export default { tooltipText: { type: String, }, + required: { + type: Boolean, + }, + }, + methods: { + addTargetBlank, }, }; @@ -48,10 +58,22 @@ export default { font-weight: var(--font-weight-bold); font-size: var(--font-size-s); margin-bottom: var(--spacing-2xs); + + * { + margin-right: var(--spacing-4xs); + } } .infoIcon { color: var(--color-text-light); display: var(--info-icon-display, none); } + +.required { + color: var(--color-primary); +} + +.tooltipPopper { + max-width: 400px; +} diff --git a/packages/design-system/src/components/N8nMenu/Menu.stories.js b/packages/design-system/src/components/N8nMenu/Menu.stories.js new file mode 100644 index 0000000000..b7cab35354 --- /dev/null +++ b/packages/design-system/src/components/N8nMenu/Menu.stories.js @@ -0,0 +1,46 @@ +import N8nMenu from './Menu.vue'; +import N8nMenuItem from '../N8nMenuItem'; + +import { action } from '@storybook/addon-actions'; + +export default { + title: 'Atoms/Menu', + component: N8nMenu, + argTypes: { + type: { + control: 'select', + options: ['primary', 'secondary'], + }, + }, + parameters: { + backgrounds: { default: '--color-background-xlight' }, + }, +}; + +const methods = { + onSelect: action('select'), +}; + +const Template = (args, { argTypes }) => ({ + props: Object.keys(argTypes), + components: { + N8nMenu, + N8nMenuItem, + }, + template: + ` + Item 1 + Item 2 + `, + methods, +}); + +export const Primary = Template.bind({}); +Primary.parameters = { + backgrounds: { default: '--color-background-light' }, +}; + +export const Secondary = Template.bind({}); +Secondary.args = { + type: 'secondary', +}; diff --git a/packages/design-system/src/components/N8nMenu/Menu.vue b/packages/design-system/src/components/N8nMenu/Menu.vue new file mode 100644 index 0000000000..4d34c375d5 --- /dev/null +++ b/packages/design-system/src/components/N8nMenu/Menu.vue @@ -0,0 +1,70 @@ + + + + + diff --git a/packages/design-system/src/components/N8nMenu/index.js b/packages/design-system/src/components/N8nMenu/index.js new file mode 100644 index 0000000000..1f0d66f3ce --- /dev/null +++ b/packages/design-system/src/components/N8nMenu/index.js @@ -0,0 +1,3 @@ +import N8nMenu from './Menu.vue'; + +export default N8nMenu; diff --git a/packages/design-system/src/components/N8nMenuItem/MenuItem.vue b/packages/design-system/src/components/N8nMenuItem/MenuItem.vue new file mode 100644 index 0000000000..03aa18ed67 --- /dev/null +++ b/packages/design-system/src/components/N8nMenuItem/MenuItem.vue @@ -0,0 +1,7 @@ + diff --git a/packages/design-system/src/components/N8nMenuItem/index.js b/packages/design-system/src/components/N8nMenuItem/index.js new file mode 100644 index 0000000000..ca20d253e6 --- /dev/null +++ b/packages/design-system/src/components/N8nMenuItem/index.js @@ -0,0 +1,3 @@ +import N8nMenuItem from './MenuItem.vue'; + +export default N8nMenuItem; diff --git a/packages/design-system/src/components/index.js b/packages/design-system/src/components/index.js index fea7b995ac..205ab81a0d 100644 --- a/packages/design-system/src/components/index.js +++ b/packages/design-system/src/components/index.js @@ -2,22 +2,28 @@ import N8nButton from './N8nButton'; import N8nIcon from './N8nIcon'; import N8nIconButton from './N8nIconButton'; import N8nInput from './N8nInput'; -import N8nInputLabel from './N8nInputLabel'; +import N8nInfoTip from './N8nInfoTip'; import N8nInputNumber from './N8nInputNumber'; -import N8nOption from './N8nOption'; +import N8nInputLabel from './N8nInputLabel'; +import N8nMenu from './N8nMenu'; +import N8nMenuItem from './N8nMenuItem'; import N8nSelect from './N8nSelect'; import N8nSpinner from './N8nSpinner'; import N8nTooltip from './N8nTooltip'; +import N8nOption from './N8nOption'; export { N8nButton, N8nIcon, N8nIconButton, + N8nInfoTip, N8nInput, N8nInputLabel, N8nInputNumber, - N8nOption, + N8nMenu, + N8nMenuItem, N8nSelect, N8nSpinner, N8nTooltip, + N8nOption, }; diff --git a/packages/design-system/src/components/utils/helpers.ts b/packages/design-system/src/components/utils/helpers.ts new file mode 100644 index 0000000000..c751adf785 --- /dev/null +++ b/packages/design-system/src/components/utils/helpers.ts @@ -0,0 +1,5 @@ +export function addTargetBlank(html: string) { + return html.includes('href=') + ? html.replace(/href=/g, 'target="_blank" href=') + : html; +} diff --git a/packages/design-system/src/shims-element-ui.d.ts b/packages/design-system/src/shims-element-ui.d.ts index 6af21baf4b..e59e86f062 100644 --- a/packages/design-system/src/shims-element-ui.d.ts +++ b/packages/design-system/src/shims-element-ui.d.ts @@ -4,4 +4,6 @@ declare module 'element-ui/lib/tooltip'; declare module 'element-ui/lib/input-number'; declare module 'element-ui/lib/select'; declare module 'element-ui/lib/option'; +declare module 'element-ui/lib/menu'; +declare module 'element-ui/lib/menu-item'; diff --git a/packages/design-system/theme/src/_tokens.scss b/packages/design-system/theme/src/_tokens.scss index 8206e42095..0953d31111 100644 --- a/packages/design-system/theme/src/_tokens.scss +++ b/packages/design-system/theme/src/_tokens.scss @@ -53,7 +53,7 @@ ); --color-success-h: 150.4; - --color-success-s: 73.8%; + --color-success-s: 60%; --color-success-l: 40.4%; --color-success: hsl( var(--color-success-h), diff --git a/packages/design-system/theme/src/checkbox.scss b/packages/design-system/theme/src/checkbox.scss index a699b7e39c..369a44f1af 100644 --- a/packages/design-system/theme/src/checkbox.scss +++ b/packages/design-system/theme/src/checkbox.scss @@ -5,7 +5,6 @@ @include mixins.b(checkbox) { color: var.$checkbox-font-color; - font-weight: var.$checkbox-font-weight; font-size: var.$font-size-base; position: relative; cursor: pointer; @@ -156,10 +155,6 @@ transform: rotate(45deg) scaleY(1); } } - - & + .el-checkbox__label { - color: var.$checkbox-checked-font-color; - } } @include mixins.when(focus) { /*focus时 视觉上区分*/ @@ -253,7 +248,6 @@ @include mixins.e(inner) { display: inline-block; line-height: 1; - font-weight: var.$checkbox-font-weight; white-space: nowrap; vertical-align: middle; cursor: pointer; diff --git a/packages/design-system/theme/src/common/popup.scss b/packages/design-system/theme/src/common/popup.scss index a26c3eea96..da91b42151 100644 --- a/packages/design-system/theme/src/common/popup.scss +++ b/packages/design-system/theme/src/common/popup.scss @@ -12,16 +12,20 @@ @keyframes v-modal-in { 0% { opacity: 0; + backdrop-filter: blur(4px) opacity(0); } 100% { + backdrop-filter: blur(4px) opacity(1); } } @keyframes v-modal-out { 0% { + backdrop-filter: blur(4px) opacity(1); } 100% { opacity: 0; + backdrop-filter: blur(4px) opacity(0); } } @@ -31,8 +35,8 @@ top: 0; width: 100%; height: 100%; - opacity: var.$popup-modal-opacity; - background: var.$popup-modal-background-color; + background-color: var.$popup-modal-background-color; + backdrop-filter: blur(4px) opacity(1); } @include mixins.b(popup-parent) { diff --git a/packages/design-system/theme/src/common/var.scss b/packages/design-system/theme/src/common/var.scss index fc009d8af6..46b58ecd5c 100644 --- a/packages/design-system/theme/src/common/var.scss +++ b/packages/design-system/theme/src/common/var.scss @@ -277,10 +277,8 @@ $icon-color-base: var(--color-info); -------------------------- */ /// fontSize||Font|1 $checkbox-font-size: 14px; -/// fontWeight||Font|1 -$checkbox-font-weight: $font-weight-primary; /// color||Color|0 -$checkbox-font-color: var(--color-text-dark); +$checkbox-font-color: var(--color-text-base); $checkbox-input-height: 14px; $checkbox-input-width: 14px; /// borderRadius||Border|2 @@ -761,7 +759,8 @@ $dialog-content-font-size: 14px; /// fontLineHeight||LineHeight|2 $dialog-font-line-height: $font-line-height-primary; /// padding||Spacing|3 -$dialog-padding-primary: 20px; +$dialog-padding-primary: var(--spacing-l); +$dialog-close-top: var(--dialog-close-top, var(--spacing-l)); /* Table -------------------------- */ @@ -804,9 +803,9 @@ $pagination-hover-color: var(--color-primary); /* Popup -------------------------- */ /// color||Color|0 -$popup-modal-background-color: $color-black; +$popup-modal-background-color: hsla(247,14%, 70%, 0.75); /// opacity||Other|1 -$popup-modal-opacity: 0.5; +$popup-modal-opacity: 0.65; /* Popover -------------------------- */ @@ -852,9 +851,9 @@ $tag-warning-color: var(--color-warning); /// color||Color|0 $tag-danger-color: var(--color-danger); /// fontSize||Font|1 -$tag-font-size: 12px; +$tag-font-size: var(--font-size-s); $tag-border-radius: 4px; -$tag-padding: 0 10px; +$tag-padding: 16px; /* Tree -------------------------- */ @@ -924,11 +923,17 @@ $steps-padding: 20px; --------------------------*/ /// fontSize||Font|1 $menu-item-font-size: $font-size-base; +$menu-item-font-weight: var(--menu-item-font-color, 300); /// color||Color|0 -$menu-item-font-color: var(--color-text-dark); +$menu-item-font-color: var(--menu-font-color, var(--color-text-dark)); /// color||Color|0 -$menu-background-color: $color-white; -$menu-item-hover-fill: $color-primary-light-9; +$menu-background-color: var(--menu-background-color, var(--color-background-xlight)); +$menu-item-hover-fill: var(--menu-item-hover-fill, transparent); +$menu-item-hover-font-color: var(--menu-item-hover-font-color, var(--color-text-dark)); +$menu-item-active-font-color: var(--menu-item-active-font-color, var(--color-primary)); +$menu-item-active-background-color: var(--menu-item-active-background-color, transparent); +$menu-item-border-radius: var(--menu-item-border-radius, 0); +$menu-item-height: var(--menu-item-height, 56px); /* Rate --------------------------*/ diff --git a/packages/design-system/theme/src/dialog.scss b/packages/design-system/theme/src/dialog.scss index 087d35b97a..d3aad6608a 100644 --- a/packages/design-system/theme/src/dialog.scss +++ b/packages/design-system/theme/src/dialog.scss @@ -32,12 +32,12 @@ @include mixins.e(header) { padding: var.$dialog-padding-primary; - padding-bottom: 10px; + padding-bottom: 0px; } @include mixins.e(headerbtn) { position: absolute; - top: var.$dialog-padding-primary; + top: var.$dialog-close-top; right: var.$dialog-padding-primary; padding: 0; background: transparent; @@ -65,8 +65,8 @@ } @include mixins.e(body) { - padding: (var.$dialog-padding-primary + 10px) var.$dialog-padding-primary; - color: var(--color-text-dark); + padding: var.$dialog-padding-primary; + color: var(--color-text-base); font-size: var.$dialog-content-font-size; word-break: break-all; } diff --git a/packages/design-system/theme/src/drawer.scss b/packages/design-system/theme/src/drawer.scss index 993ab4ff2d..6abb96fbc2 100644 --- a/packages/design-system/theme/src/drawer.scss +++ b/packages/design-system/theme/src/drawer.scss @@ -113,8 +113,7 @@ $directions: rtl, ltr, ttb, btt; background-color: var.$dialog-background-color; display: flex; flex-direction: column; - box-shadow: 0 8px 10px -5px rgba(0, 0, 0, 0.2), - 0 16px 24px 2px rgba(0, 0, 0, 0.14), 0 6px 30px 5px rgba(0, 0, 0, 0.12); + box-shadow: 0px 6px 16px rgb(68 28 23 / 6%); overflow: hidden; outline: 0; @@ -183,10 +182,12 @@ $directions: rtl, ltr, ttb, btt; &.ltr { left: 0; + border-right: var(--border-base); } &.rtl { right: 0; + border-left: var(--border-base); } &.ttb { diff --git a/packages/design-system/theme/src/menu.scss b/packages/design-system/theme/src/menu.scss index 24fc102549..b8ca547788 100644 --- a/packages/design-system/theme/src/menu.scss +++ b/packages/design-system/theme/src/menu.scss @@ -4,10 +4,11 @@ @use "common/transition"; @mixin menu-item { - height: 56px; - line-height: 56px; + height: var.$menu-item-height; + line-height: var.$menu-item-height; font-size: var.$menu-item-font-size; color: var.$menu-item-font-color; + font-weight: var.$menu-item-font-weight; padding: 0 20px; list-style: none; cursor: pointer; @@ -15,10 +16,7 @@ transition: border-color 0.3s, background-color 0.3s, color 0.3s; box-sizing: border-box; white-space: nowrap; - - * { - vertical-align: middle; - } + border-radius: var.$menu-item-border-radius; i { color: var(--color-text-light); @@ -28,6 +26,7 @@ &:focus { outline: none; background-color: var.$menu-item-hover-fill; + color: var.$menu-item-hover-font-color; } @include mixins.when(disabled) { @@ -38,7 +37,6 @@ } @include mixins.b(menu) { - border-right: solid 1px #e6e6e6; list-style: none; position: relative; margin: 0; @@ -208,7 +206,9 @@ vertical-align: middle; } @include mixins.when(active) { - color: var(--color-primary); + color: var.$menu-item-active-font-color; + background-color: var.$menu-item-active-background-color; + i { color: inherit; } diff --git a/packages/design-system/theme/src/reset.scss b/packages/design-system/theme/src/reset.scss index 1a0e69a4c2..56cdd877c8 100644 --- a/packages/design-system/theme/src/reset.scss +++ b/packages/design-system/theme/src/reset.scss @@ -226,7 +226,7 @@ hr { height: 1px; border: 0; border-top: 1px solid var(--color-foreground-light); - margin: 1em 0; + margin: 0; padding: 0; } diff --git a/packages/design-system/theme/src/tag.scss b/packages/design-system/theme/src/tag.scss index 2a2a81893e..2ab1ce4b3b 100644 --- a/packages/design-system/theme/src/tag.scss +++ b/packages/design-system/theme/src/tag.scss @@ -5,6 +5,7 @@ background-color: var.$color-primary-lighter; border-color: var.$color-primary-light-5; color: var.$color-primary-light-1; + font-weight: var(--font-weight-regular); @include mixins.when(hit) { border-color: var.$tag-primary-color; @@ -37,9 +38,9 @@ } &.el-tag--success { - background-color: var.$color-success-lighter; - border-color: var.$color-success-light-5; - color: var.$color-success-light-3; + background-color: var(--color-success-tint-2); + border-color: var(--color-success-tint-1); + color: var(--color-success); @include mixins.when(hit) { border-color: var.$tag-success-color; @@ -94,9 +95,7 @@ @include mixins.b(tag) { @include genTheme(); display: inline-block; - height: 32px; padding: var.$tag-padding; - line-height: 30px; font-size: var.$tag-font-size; color: var.$tag-primary-color; border-width: 1px; @@ -132,8 +131,7 @@ } @include mixins.m(medium) { - height: 28px; - line-height: 26px; + padding: 12px; .el-icon-close { transform: scale(0.8); diff --git a/packages/design-system/theme/src/tooltip.scss b/packages/design-system/theme/src/tooltip.scss index cbcc3c7c08..f1feffb396 100644 --- a/packages/design-system/theme/src/tooltip.scss +++ b/packages/design-system/theme/src/tooltip.scss @@ -7,10 +7,6 @@ outline-width: 0; } - a { - font-weight: var(--font-weight-bold); - } - @include mixins.e(popper) { position: absolute; border-radius: 4px; @@ -20,6 +16,11 @@ line-height: 1.2; min-width: 10px; word-wrap: break-word; + font-weight: var(--font-weight-regular); + + a { + font-weight: var(--font-weight-bold); + } .popper__arrow, .popper__arrow::after { diff --git a/packages/editor-ui/public/google-signin.png b/packages/editor-ui/public/google-signin-light.png similarity index 100% rename from packages/editor-ui/public/google-signin.png rename to packages/editor-ui/public/google-signin-light.png diff --git a/packages/editor-ui/src/Interface.ts b/packages/editor-ui/src/Interface.ts index 8df78dabc5..57c0bd59b6 100644 --- a/packages/editor-ui/src/Interface.ts +++ b/packages/editor-ui/src/Interface.ts @@ -140,19 +140,10 @@ export interface IRestApi { getWorkflow(id: string): Promise; getWorkflows(filter?: object): Promise; getWorkflowFromUrl(url: string): Promise; - createNewCredentials(sendData: ICredentialsDecrypted): Promise; - deleteCredentials(id: string): Promise; - updateCredentials(id: string, data: ICredentialsDecrypted): Promise; - getAllCredentials(filter?: object): Promise; - getCredentials(id: string, includeData?: boolean): Promise; - getCredentialTypes(): Promise; getExecution(id: string): Promise; deleteExecutions(sendData: IExecutionDeleteFilter): Promise; retryExecution(id: string, loadWorkflow?: boolean): Promise; getTimezones(): Promise; - oAuth1CredentialAuthorize(sendData: ICredentialsResponse): Promise; - oAuth2CredentialAuthorize(sendData: ICredentialsResponse): Promise; - oAuth2Callback(code: string, state: string): Promise; } export interface IBinaryDisplayData { @@ -163,13 +154,6 @@ export interface IBinaryDisplayData { runIndex: number; } -export interface ICredentialsCreatedEvent { - data: ICredentialsDecryptedResponse; - options: { - closeDialog: boolean, - }; -} - export interface IStartRunData { workflowData: IWorkflowData; startNodes?: string[]; @@ -585,8 +569,6 @@ export interface IRootState { activeActions: string[]; activeNode: string | null; baseUrl: string; - credentials: ICredentialsResponse[] | null; - credentialTypes: ICredentialType[] | null; endpointWebhook: string; endpointWebhookTest: string; executionId: string | null; @@ -618,6 +600,19 @@ export interface IRootState { instanceId: string; } +export interface ICredentialTypeMap { + [name: string]: ICredentialType; +} + +export interface ICredentialMap { + [name: string]: ICredentialsResponse; +} + +export interface ICredentialsState { + credentialTypes: ICredentialTypeMap; + credentials: ICredentialMap; +} + export interface ITagsState { tags: { [id: string]: ITag }; isLoading: boolean; @@ -627,6 +622,8 @@ export interface ITagsState { export interface IModalState { open: boolean; + mode?: string | null; + activeId?: string | null; } export interface IUiState { diff --git a/packages/editor-ui/src/api/credentials.ts b/packages/editor-ui/src/api/credentials.ts new file mode 100644 index 0000000000..aa0f58058c --- /dev/null +++ b/packages/editor-ui/src/api/credentials.ts @@ -0,0 +1,53 @@ +import { ICredentialsDecryptedResponse, ICredentialsResponse, IRestApiContext } from '@/Interface'; +import { makeRestApiRequest } from './helpers'; +import { + ICredentialsDecrypted, + ICredentialType, + IDataObject, + NodeCredentialTestRequest, + NodeCredentialTestResult, +} from 'n8n-workflow'; + +export async function getCredentialTypes(context: IRestApiContext): Promise { + return await makeRestApiRequest(context, 'GET', '/credential-types'); +} + +export async function getCredentialsNewName(context: IRestApiContext, name?: string): Promise<{name: string}> { + return await makeRestApiRequest(context, 'GET', '/credentials/new', name ? { name } : {}); +} + +export async function getAllCredentials(context: IRestApiContext): Promise { + return await makeRestApiRequest(context, 'GET', '/credentials'); +} + +export async function createNewCredential(context: IRestApiContext, data: ICredentialsDecrypted): Promise { + return makeRestApiRequest(context, 'POST', `/credentials`, data as unknown as IDataObject); +} + +export async function deleteCredential(context: IRestApiContext, id: string): Promise { + return makeRestApiRequest(context, 'DELETE', `/credentials/${id}`); +} + +export async function updateCredential(context: IRestApiContext, id: string, data: ICredentialsDecrypted): Promise { + return makeRestApiRequest(context, 'PATCH', `/credentials/${id}`, data as unknown as IDataObject); +} + +export async function getCredentialData(context: IRestApiContext, id: string): Promise { + return makeRestApiRequest(context, 'GET', `/credentials/${id}`, { + includeData: true, + }); +} + +// Get OAuth1 Authorization URL using the stored credentials +export async function oAuth1CredentialAuthorize(context: IRestApiContext, data: ICredentialsResponse): Promise { + return makeRestApiRequest(context, 'GET', `/oauth1-credential/auth`, data as unknown as IDataObject); +} + +// Get OAuth2 Authorization URL using the stored credentials +export async function oAuth2CredentialAuthorize(context: IRestApiContext, data: ICredentialsResponse): Promise { + return makeRestApiRequest(context, 'GET', `/oauth2-credential/auth`, data as unknown as IDataObject); +} + +export async function testCredential(context: IRestApiContext, data: NodeCredentialTestRequest): Promise { + return makeRestApiRequest(context, 'POST', '/credentials-test', data as unknown as IDataObject); +} diff --git a/packages/editor-ui/src/components/Banner.vue b/packages/editor-ui/src/components/Banner.vue new file mode 100644 index 0000000000..d3c7a835c6 --- /dev/null +++ b/packages/editor-ui/src/components/Banner.vue @@ -0,0 +1,147 @@ + + + + + diff --git a/packages/editor-ui/src/components/CopyInput.vue b/packages/editor-ui/src/components/CopyInput.vue new file mode 100644 index 0000000000..64866c5e6c --- /dev/null +++ b/packages/editor-ui/src/components/CopyInput.vue @@ -0,0 +1,96 @@ + + + + + diff --git a/packages/editor-ui/src/components/CredentialEdit/CredentialConfig.vue b/packages/editor-ui/src/components/CredentialEdit/CredentialConfig.vue new file mode 100644 index 0000000000..3ad1712676 --- /dev/null +++ b/packages/editor-ui/src/components/CredentialEdit/CredentialConfig.vue @@ -0,0 +1,189 @@ + + + + + diff --git a/packages/editor-ui/src/components/CredentialEdit/CredentialEdit.vue b/packages/editor-ui/src/components/CredentialEdit/CredentialEdit.vue new file mode 100644 index 0000000000..b4cf53aa60 --- /dev/null +++ b/packages/editor-ui/src/components/CredentialEdit/CredentialEdit.vue @@ -0,0 +1,853 @@ + + + + + diff --git a/packages/editor-ui/src/components/CredentialEdit/CredentialInfo.vue b/packages/editor-ui/src/components/CredentialEdit/CredentialInfo.vue new file mode 100644 index 0000000000..d42212cc30 --- /dev/null +++ b/packages/editor-ui/src/components/CredentialEdit/CredentialInfo.vue @@ -0,0 +1,91 @@ + + + + + diff --git a/packages/editor-ui/src/components/CredentialEdit/CredentialInputs.vue b/packages/editor-ui/src/components/CredentialEdit/CredentialInputs.vue new file mode 100644 index 0000000000..6eaa19dc49 --- /dev/null +++ b/packages/editor-ui/src/components/CredentialEdit/CredentialInputs.vue @@ -0,0 +1,52 @@ + + + + + diff --git a/packages/editor-ui/src/components/CredentialEdit/OauthButton.vue b/packages/editor-ui/src/components/CredentialEdit/OauthButton.vue new file mode 100644 index 0000000000..8e89b20f01 --- /dev/null +++ b/packages/editor-ui/src/components/CredentialEdit/OauthButton.vue @@ -0,0 +1,41 @@ + + + + + diff --git a/packages/editor-ui/src/components/CredentialIcon.vue b/packages/editor-ui/src/components/CredentialIcon.vue new file mode 100644 index 0000000000..f4d5e302d8 --- /dev/null +++ b/packages/editor-ui/src/components/CredentialIcon.vue @@ -0,0 +1,74 @@ + + + + + diff --git a/packages/editor-ui/src/components/CredentialsEdit.vue b/packages/editor-ui/src/components/CredentialsEdit.vue deleted file mode 100644 index 7ad24e8785..0000000000 --- a/packages/editor-ui/src/components/CredentialsEdit.vue +++ /dev/null @@ -1,388 +0,0 @@ - - - - - diff --git a/packages/editor-ui/src/components/CredentialsInput.vue b/packages/editor-ui/src/components/CredentialsInput.vue deleted file mode 100644 index e638b345a7..0000000000 --- a/packages/editor-ui/src/components/CredentialsInput.vue +++ /dev/null @@ -1,618 +0,0 @@ - - - - - diff --git a/packages/editor-ui/src/components/CredentialsList.vue b/packages/editor-ui/src/components/CredentialsList.vue index cf5f58afb4..ad0437eff2 100644 --- a/packages/editor-ui/src/components/CredentialsList.vue +++ b/packages/editor-ui/src/components/CredentialsList.vue @@ -1,7 +1,5 @@ + + + + + + + + + diff --git a/packages/editor-ui/src/components/MultipleParameter.vue b/packages/editor-ui/src/components/MultipleParameter.vue index cec26d9c61..8e31c6965e 100644 --- a/packages/editor-ui/src/components/MultipleParameter.vue +++ b/packages/editor-ui/src/components/MultipleParameter.vue @@ -21,7 +21,7 @@
- +
@@ -75,6 +75,7 @@ export default mixins(genericHelpers) }, }, methods: { + addTargetBlank, addItem () { const name = this.getPath(); let currentValue = get(this.nodeValues, name); @@ -92,7 +93,6 @@ export default mixins(genericHelpers) this.$emit('valueChanged', parameterData); }, - addTargetBlank, deleteItem (index: number) { const parameterData = { name: this.getPath(index), diff --git a/packages/editor-ui/src/components/Node.vue b/packages/editor-ui/src/components/Node.vue index 4955b5c13b..4983b5c8cd 100644 --- a/packages/editor-ui/src/components/Node.vue +++ b/packages/editor-ui/src/components/Node.vue @@ -37,7 +37,7 @@ - +
diff --git a/packages/editor-ui/src/components/NodeCredentials.vue b/packages/editor-ui/src/components/NodeCredentials.vue index 3bf59ccfed..fb6c0b651e 100644 --- a/packages/editor-ui/src/components/NodeCredentials.vue +++ b/packages/editor-ui/src/components/NodeCredentials.vue @@ -1,27 +1,30 @@ @@ -317,6 +281,7 @@ export default mixins( display: flex; justify-content: center; align-items: center; + color: var(--color-text-base); } } diff --git a/packages/editor-ui/src/components/NodeIcon.vue b/packages/editor-ui/src/components/NodeIcon.vue index d30ff4495b..9bdc8fb4e6 100644 --- a/packages/editor-ui/src/components/NodeIcon.vue +++ b/packages/editor-ui/src/components/NodeIcon.vue @@ -27,6 +27,7 @@ export default Vue.extend({ 'size', 'shrink', 'disabled', + 'circle', ], computed: { iconStyleData (): object { @@ -43,7 +44,7 @@ export default Vue.extend({ height: size + 'px', 'font-size': Math.floor(parseInt(this.size, 10) * 0.6) + 'px', 'line-height': size + 'px', - 'border-radius': Math.ceil(size / 2) + 'px', + 'border-radius': this.circle ? '50%': '4px', }; }, isSvgIcon (): boolean { diff --git a/packages/editor-ui/src/components/NodeSettings.vue b/packages/editor-ui/src/components/NodeSettings.vue index 8ab01090d5..14b5e404c6 100644 --- a/packages/editor-ui/src/components/NodeSettings.vue +++ b/packages/editor-ui/src/components/NodeSettings.vue @@ -511,24 +511,17 @@ export default mixins( diff --git a/packages/editor-ui/src/components/ParameterInputExpanded.vue b/packages/editor-ui/src/components/ParameterInputExpanded.vue new file mode 100644 index 0000000000..d12788abc4 --- /dev/null +++ b/packages/editor-ui/src/components/ParameterInputExpanded.vue @@ -0,0 +1,68 @@ + + + diff --git a/packages/editor-ui/src/components/ParameterInputFull.vue b/packages/editor-ui/src/components/ParameterInputFull.vue index d584b80c21..d7cd061d5c 100644 --- a/packages/editor-ui/src/components/ParameterInputFull.vue +++ b/packages/editor-ui/src/components/ParameterInputFull.vue @@ -8,7 +8,7 @@ - + @@ -47,6 +47,7 @@ export default Vue }, props: [ 'displayOptions', + 'isReadOnly', 'parameter', 'path', 'value', diff --git a/packages/editor-ui/src/components/ParameterInputList.vue b/packages/editor-ui/src/components/ParameterInputList.vue index 8d1401eaa4..e49a09bc92 100644 --- a/packages/editor-ui/src/components/ParameterInputList.vue +++ b/packages/editor-ui/src/components/ParameterInputList.vue @@ -70,6 +70,7 @@ :value="getParameterValue(nodeValues, parameter.name, path)" :displayOptions="true" :path="getPath(parameter.name)" + :isReadOnly="isReadOnly" @valueChanged="valueChanged" />
diff --git a/packages/editor-ui/src/components/RunData.vue b/packages/editor-ui/src/components/RunData.vue index a5f4450fba..3caaef6d3e 100644 --- a/packages/editor-ui/src/components/RunData.vue +++ b/packages/editor-ui/src/components/RunData.vue @@ -26,10 +26,9 @@ -  / + / {{ dataCount }}
-   - | Output:  + | Output: @@ -51,7 +50,7 @@ - | Data of Execution:  + | Data of Execution: @@ -270,6 +269,9 @@ export default mixins( MAX_DISPLAY_ITEMS_AUTO_ALL, }; }, + mounted() { + this.init(); + }, computed: { hasNodeRun(): boolean { return Boolean(this.node && this.workflowRunData && this.workflowRunData.hasOwnProperty(this.node.name)); @@ -423,6 +425,18 @@ export default mixins( }, }, methods: { + init() { + // Reset the selected output index every time another node gets selected + this.outputIndex = 0; + this.maxDisplayItems = 25; + this.refreshDataSize(); + if (this.displayMode === 'Binary') { + this.closeBinaryDataDisplay(); + if (this.binaryData.length === 0) { + this.displayMode = 'Table'; + } + } + }, closeBinaryDataDisplay () { this.binaryDataDisplayVisible = false; this.binaryDataDisplayData = null; @@ -607,17 +621,8 @@ export default mixins( }, }, watch: { - node (newNode, oldNode) { - // Reset the selected output index every time another node gets selected - this.outputIndex = 0; - this.maxDisplayItems = 25; - this.refreshDataSize(); - if (this.displayMode === 'Binary') { - this.closeBinaryDataDisplay(); - if (this.binaryData.length === 0) { - this.displayMode = 'Table'; - } - } + node() { + this.init(); }, jsonData () { this.refreshDataSize(); @@ -630,8 +635,6 @@ export default mixins( this.runIndex = Math.min(this.runIndex, this.maxRunIndex); }, }, - mounted () { - }, }); @@ -639,14 +642,8 @@ export default mixins( .run-data-view { position: relative; - bottom: 0; - left: 0; - margin-left: 350px; - width: calc(100% - 350px); + width: 100%; height: 100%; - z-index: 100; - color: #555; - font-size: 14px; background-color: #f9f9f9; .data-display-content { @@ -657,6 +654,7 @@ export default mixins( right: 0; overflow-y: auto; line-height: 1.5; + word-break: normal; .binary-data-row { display: inline-flex; @@ -795,6 +793,10 @@ export default mixins( .title-text { display: inline-flex; align-items: center; + + > * { + margin-right: 2px; + } } .title-data-display-selector { diff --git a/packages/editor-ui/src/components/SaveButton.vue b/packages/editor-ui/src/components/SaveButton.vue new file mode 100644 index 0000000000..d951b0307b --- /dev/null +++ b/packages/editor-ui/src/components/SaveButton.vue @@ -0,0 +1,58 @@ + + + + + diff --git a/packages/editor-ui/src/components/SaveWorkflowButton.vue b/packages/editor-ui/src/components/SaveWorkflowButton.vue deleted file mode 100644 index 30d9f5be5e..0000000000 --- a/packages/editor-ui/src/components/SaveWorkflowButton.vue +++ /dev/null @@ -1,51 +0,0 @@ - - - - - diff --git a/packages/editor-ui/src/components/TimeAgo.vue b/packages/editor-ui/src/components/TimeAgo.vue index 58caa4c68d..1b9011867a 100644 --- a/packages/editor-ui/src/components/TimeAgo.vue +++ b/packages/editor-ui/src/components/TimeAgo.vue @@ -1,15 +1,59 @@ \ No newline at end of file + diff --git a/packages/editor-ui/src/components/WorkflowOpen.vue b/packages/editor-ui/src/components/WorkflowOpen.vue index 9ced9332cf..adc87415b8 100644 --- a/packages/editor-ui/src/components/WorkflowOpen.vue +++ b/packages/editor-ui/src/components/WorkflowOpen.vue @@ -2,6 +2,7 @@