mirror of
https://github.com/n8n-io/n8n.git
synced 2024-10-05 17:17:45 +03:00
Merge remote-tracking branch 'origin/master' into ENG-5-db-locking-for-migrations
This commit is contained in:
commit
3523813fca
41
.github/workflows/check-documentation-urls.yml
vendored
Normal file
41
.github/workflows/check-documentation-urls.yml
vendored
Normal file
@ -0,0 +1,41 @@
|
||||
name: Check Documentation URLs
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- n8n@*
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
timeout-minutes: 5
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- uses: pnpm/action-setup@v2.2.4
|
||||
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16.x
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Build nodes-base
|
||||
run: pnpm --filter n8n-workflow --filter=n8n-core --filter=n8n-nodes-base build
|
||||
|
||||
- name: Test URLS
|
||||
run: node scripts/validate-docs-links.js
|
||||
|
||||
- name: Notify Slack on failure
|
||||
uses: act10ns/slack@v2.0.0
|
||||
if: failure()
|
||||
with:
|
||||
status: ${{ job.status }}
|
||||
channel: '#updates-build-alerts'
|
||||
webhook-url: ${{ secrets.SLACK_WEBHOOK_URL }}
|
||||
message: Documentation URLs check failed (${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})
|
9
.vscode/settings.default.json
vendored
9
.vscode/settings.default.json
vendored
@ -6,6 +6,13 @@
|
||||
"dist": true,
|
||||
"pnpm-lock.yaml": true
|
||||
},
|
||||
"typescript.format.enable": false,
|
||||
"typescript.tsdk": "node_modules/typescript/lib",
|
||||
"workspace-default-settings.runOnActivation": true
|
||||
"workspace-default-settings.runOnActivation": true,
|
||||
"eslint.probe": ["javascript", "typescript", "vue"],
|
||||
"eslint.workingDirectories": [
|
||||
{
|
||||
"mode": "auto"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
24
CHANGELOG.md
24
CHANGELOG.md
@ -1,3 +1,27 @@
|
||||
# [0.209.0](https://github.com/n8n-io/n8n/compare/n8n@0.208.1...n8n@0.209.0) (2022-12-21)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **editor:** Correctly display trigger nodes without actions and with related regular node in the "On App Events" category ([#4976](https://github.com/n8n-io/n8n/issues/4976)) ([445463a](https://github.com/n8n-io/n8n/commit/445463a605f5f327f897b23a9b4504939358d0df))
|
||||
* Fix stickies resize ([#4986](https://github.com/n8n-io/n8n/issues/4986)) ([82f7635](https://github.com/n8n-io/n8n/commit/82f763589b21815e5ba91c10a4676d25f843eddd))
|
||||
* Hide trigger tooltip for nodes with static test output ([#4970](https://github.com/n8n-io/n8n/issues/4970)) ([5b11dc3](https://github.com/n8n-io/n8n/commit/5b11dc3ff9ff75eb7c65721e0d6c03707039e7ff))
|
||||
* Keep expression when dropping mapped value ([#4981](https://github.com/n8n-io/n8n/issues/4981)) ([87c7643](https://github.com/n8n-io/n8n/commit/87c76434a294f10474711ba3f023f2f4ca47f14d))
|
||||
* Prevent keyboard shortcuts in expression editor modal ([#4984](https://github.com/n8n-io/n8n/issues/4984)) ([29364ea](https://github.com/n8n-io/n8n/commit/29364ea7026e5e2a288b1866956f01e380ff05a0))
|
||||
* Redirect home to workflows always ([#4968](https://github.com/n8n-io/n8n/issues/4968)) ([90bfdfd](https://github.com/n8n-io/n8n/commit/90bfdfd577c02aee520545cf8758019042cdf99c))
|
||||
* Update mapping gifs ([#4982](https://github.com/n8n-io/n8n/issues/4982)) ([9d00b47](https://github.com/n8n-io/n8n/commit/9d00b4748b39f5c2b08721c5ee73e47b43230b9d))
|
||||
* Upgrade amqplib to address CVE-2022-0686 ([#4972](https://github.com/n8n-io/n8n/issues/4972)) ([570ed3b](https://github.com/n8n-io/n8n/commit/570ed3b52191cf3a162fcdaaabc8ab15fb0ef08c))
|
||||
* View option for binary-data shouldn't download the file on Chrome/Edge ([#4995](https://github.com/n8n-io/n8n/issues/4995)) ([e225c31](https://github.com/n8n-io/n8n/commit/e225c3190ea4cb5f68f642aab455ed0044fdecf9))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Add PR template ([#4983](https://github.com/n8n-io/n8n/issues/4983)) ([17311ca](https://github.com/n8n-io/n8n/commit/17311ca0499d90c56013e3f79458c49c0ae3dcdc))
|
||||
* **editor:** Add usage and plan pages ([#4819](https://github.com/n8n-io/n8n/issues/4819)) ([0da338f](https://github.com/n8n-io/n8n/commit/0da338f9b5f850b25e97383ae1f4cec8d0e4c17b)), closes [#4793](https://github.com/n8n-io/n8n/issues/4793) [#4842](https://github.com/n8n-io/n8n/issues/4842) [#4866](https://github.com/n8n-io/n8n/issues/4866) [#4875](https://github.com/n8n-io/n8n/issues/4875) [#4958](https://github.com/n8n-io/n8n/issues/4958) [#4979](https://github.com/n8n-io/n8n/issues/4979)
|
||||
* Update mapping pill for table/json views ([#4965](https://github.com/n8n-io/n8n/issues/4965)) ([343f53b](https://github.com/n8n-io/n8n/commit/343f53bf5393e86eb850d07de85b762476294656))
|
||||
|
||||
|
||||
|
||||
## [0.208.1](https://github.com/n8n-io/n8n/compare/n8n@0.208.0...n8n@0.208.1) (2022-12-19)
|
||||
|
||||
|
||||
|
@ -6,7 +6,7 @@ COPY .npmrc /usr/local/etc/npmrc
|
||||
|
||||
RUN \
|
||||
apk add --update git graphicsmagick tini tzdata ca-certificates && \
|
||||
npm install -g npm@latest full-icu && \
|
||||
npm install -g npm@8.19.2 full-icu && \
|
||||
rm -rf /var/cache/apk/* /root/.npm /tmp/* && \
|
||||
# Install fonts
|
||||
apk --no-cache add --virtual fonts msttcorefonts-installer fontconfig && \
|
||||
|
@ -22,7 +22,8 @@ RUN rm -rf patches .npmrc *.yaml node_modules/.cache packages/**/node_modules/.c
|
||||
|
||||
# 2. Start with a new clean image with just the code that is needed to run n8n
|
||||
FROM n8nio/base:${NODE_VERSION}
|
||||
COPY --from=builder /home/node ./
|
||||
COPY --from=builder /home/node /usr/local/lib/node_modules/n8n
|
||||
RUN ln -s /usr/local/lib/node_modules/n8n/packages/cli/bin/n8n /usr/local/bin/n8n
|
||||
COPY docker/images/n8n-custom/docker-entrypoint.sh /
|
||||
|
||||
RUN \
|
||||
|
@ -1,16 +1,8 @@
|
||||
#!/bin/sh
|
||||
if [ "$#" -gt 0 ]; then
|
||||
# Got started with arguments
|
||||
COMMAND=$1;
|
||||
|
||||
if [[ "$COMMAND" == "n8n" ]]; then
|
||||
shift
|
||||
(cd packages/cli; exec node ./bin/n8n "$@")
|
||||
else
|
||||
exec node "$@"
|
||||
fi
|
||||
|
||||
node "$@"
|
||||
else
|
||||
# Got started without arguments
|
||||
cd packages/cli; exec node ./bin/n8n
|
||||
# Got started without arguments
|
||||
n8n
|
||||
fi
|
||||
|
@ -12,7 +12,7 @@ RUN \
|
||||
# Set a custom user to not have n8n run as root
|
||||
USER root
|
||||
|
||||
RUN npm_config_user=root npm install -g npm@latest full-icu n8n@${N8N_VERSION}
|
||||
RUN npm_config_user=root npm install -g npm@8.19.2 full-icu n8n@${N8N_VERSION}
|
||||
|
||||
ENV NODE_ICU_DATA /usr/local/lib/node_modules/full-icu
|
||||
|
||||
|
@ -17,7 +17,7 @@ RUN \
|
||||
# Set a custom user to not have n8n run as root
|
||||
USER root
|
||||
|
||||
RUN npm_config_user=root npm install -g npm@latest n8n@${N8N_VERSION}
|
||||
RUN npm_config_user=root npm install -g npm@8.19.2 n8n@${N8N_VERSION}
|
||||
|
||||
WORKDIR /data
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "n8n",
|
||||
"version": "0.208.1",
|
||||
"version": "0.209.0",
|
||||
"private": true,
|
||||
"homepage": "https://n8n.io",
|
||||
"engines": {
|
||||
@ -66,7 +66,8 @@
|
||||
"browserslist": "^4.21.4",
|
||||
"ejs": "^3.1.8",
|
||||
"fork-ts-checker-webpack-plugin": "^6.0.4",
|
||||
"globby": "^11.1.0"
|
||||
"cpy@8>globby": "^11.1.0",
|
||||
"qqjs>globby": "^11.1.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,12 +2,6 @@
|
||||
* @type {import('@types/eslint').ESLint.ConfigData}
|
||||
*/
|
||||
const config = (module.exports = {
|
||||
parser: '@typescript-eslint/parser',
|
||||
parserOptions: {
|
||||
sourceType: 'module',
|
||||
project: ['./tsconfig.json'],
|
||||
},
|
||||
|
||||
ignorePatterns: [
|
||||
'node_modules/**',
|
||||
'dist/**',
|
||||
@ -318,11 +312,21 @@ const config = (module.exports = {
|
||||
// eslint-plugin-import
|
||||
// ----------------------------------
|
||||
|
||||
/**
|
||||
* https://github.com/import-js/eslint-plugin-import/blob/main/docs/rules/no-cycle.md
|
||||
*/
|
||||
'import/no-cycle': 'error',
|
||||
|
||||
/**
|
||||
* https://github.com/import-js/eslint-plugin-import/blob/master/docs/rules/no-default-export.md
|
||||
*/
|
||||
'import/no-default-export': 'error',
|
||||
|
||||
/**
|
||||
* https://github.com/import-js/eslint-plugin-import/blob/main/docs/rules/no-unresolved.md
|
||||
*/
|
||||
'import/no-unresolved': 'error',
|
||||
|
||||
/**
|
||||
* https://github.com/import-js/eslint-plugin-import/blob/master/docs/rules/order.md
|
||||
*/
|
||||
|
@ -12,16 +12,6 @@ module.exports = {
|
||||
node: true,
|
||||
},
|
||||
|
||||
parser: 'vue-eslint-parser',
|
||||
parserOptions: {
|
||||
parser: {
|
||||
ts: '@typescript-eslint/parser',
|
||||
js: '@typescript-eslint/parser',
|
||||
vue: 'vue-eslint-parser',
|
||||
template: 'vue-eslint-parser',
|
||||
},
|
||||
},
|
||||
|
||||
ignorePatterns: ['**/*.js', '**/*.d.ts', 'vite.config.ts', '**/*.ts.snap'],
|
||||
|
||||
rules: {
|
||||
|
@ -10,6 +10,7 @@
|
||||
"eslint": "~8.28",
|
||||
"eslint-config-airbnb-typescript": "~17.0",
|
||||
"eslint-config-prettier": "~8.5",
|
||||
"eslint-import-resolver-typescript": "~3.5",
|
||||
"eslint-plugin-diff": "~2.0",
|
||||
"eslint-plugin-import": "~2.26",
|
||||
"eslint-plugin-n8n-local-rules": "~1.0",
|
||||
|
41
packages/@n8n_io/eslint-config/shared.js
Normal file
41
packages/@n8n_io/eslint-config/shared.js
Normal file
@ -0,0 +1,41 @@
|
||||
/**
|
||||
* @type {(dir: string, mode: 'frontend' | undefined) => import('@types/eslint').ESLint.ConfigData}
|
||||
*/
|
||||
exports.sharedOptions = (tsconfigRootDir, mode) => {
|
||||
const isFrontend = mode === 'frontend';
|
||||
const parser = isFrontend ? 'vue-eslint-parser' : '@typescript-eslint/parser';
|
||||
const extraParserOptions = isFrontend
|
||||
? {
|
||||
extraFileExtensions: ['.vue'],
|
||||
parser: {
|
||||
ts: '@typescript-eslint/parser',
|
||||
js: '@typescript-eslint/parser',
|
||||
vue: 'vue-eslint-parser',
|
||||
template: 'vue-eslint-parser',
|
||||
},
|
||||
}
|
||||
: {};
|
||||
|
||||
const settings = {
|
||||
'import/parsers': {
|
||||
'@typescript-eslint/parser': ['.ts'],
|
||||
},
|
||||
|
||||
'import/resolver': {
|
||||
typescript: {
|
||||
tsconfigRootDir,
|
||||
project: './tsconfig.json',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
parser,
|
||||
parserOptions: {
|
||||
tsconfigRootDir,
|
||||
project: ['./tsconfig.json'],
|
||||
...extraParserOptions,
|
||||
},
|
||||
settings,
|
||||
};
|
||||
};
|
@ -1,13 +1,12 @@
|
||||
const { sharedOptions } = require('@n8n_io/eslint-config/shared');
|
||||
|
||||
/**
|
||||
* @type {import('@types/eslint').ESLint.ConfigData}
|
||||
*/
|
||||
module.exports = {
|
||||
extends: ['@n8n_io/eslint-config/node'],
|
||||
|
||||
parserOptions: {
|
||||
project: ['./tsconfig.json'],
|
||||
tsconfigRootDir: __dirname,
|
||||
},
|
||||
...sharedOptions(__dirname),
|
||||
|
||||
ignorePatterns: [
|
||||
'jest.config.js',
|
||||
@ -15,8 +14,10 @@ module.exports = {
|
||||
'src/databases/migrations/**',
|
||||
'src/databases/ormconfig.ts',
|
||||
],
|
||||
|
||||
rules: {
|
||||
// TODO: Remove this
|
||||
'import/no-cycle': 'warn',
|
||||
'import/order': 'off',
|
||||
'import/extensions': 'off',
|
||||
'@typescript-eslint/ban-ts-comment': ['warn', { 'ts-ignore': true }],
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "n8n",
|
||||
"version": "0.208.1",
|
||||
"version": "0.209.0",
|
||||
"description": "n8n Workflow Automation Tool",
|
||||
"license": "SEE LICENSE IN LICENSE.md",
|
||||
"homepage": "https://n8n.io",
|
||||
@ -150,10 +150,10 @@
|
||||
"lodash.split": "^4.4.2",
|
||||
"lodash.unset": "^4.5.2",
|
||||
"mysql2": "~2.3.0",
|
||||
"n8n-core": "~0.148.1",
|
||||
"n8n-editor-ui": "~0.174.1",
|
||||
"n8n-nodes-base": "~0.206.1",
|
||||
"n8n-workflow": "~0.130.0",
|
||||
"n8n-core": "~0.149.0",
|
||||
"n8n-editor-ui": "~0.175.0",
|
||||
"n8n-nodes-base": "~0.207.0",
|
||||
"n8n-workflow": "~0.131.0",
|
||||
"nodemailer": "^6.7.1",
|
||||
"oauth-1.0a": "^2.2.6",
|
||||
"open": "^7.0.0",
|
||||
|
@ -458,7 +458,11 @@ export interface IN8nUISettings {
|
||||
saveManualExecutions: boolean;
|
||||
executionTimeout: number;
|
||||
maxExecutionTimeout: number;
|
||||
workflowCallerPolicyDefaultOption: 'any' | 'none' | 'workflowsFromAList';
|
||||
workflowCallerPolicyDefaultOption:
|
||||
| 'any'
|
||||
| 'none'
|
||||
| 'workflowsFromAList'
|
||||
| 'workflowsFromSameOwner';
|
||||
oauthCallbackUrls: {
|
||||
oauth1: string;
|
||||
oauth2: string;
|
||||
@ -498,7 +502,6 @@ export interface IN8nUISettings {
|
||||
};
|
||||
enterprise: {
|
||||
sharing: boolean;
|
||||
workflowSharing: boolean;
|
||||
};
|
||||
hideUsagePage: boolean;
|
||||
license: {
|
||||
|
@ -17,6 +17,7 @@ import {
|
||||
IExecutionTrackProperties,
|
||||
} from '@/Interfaces';
|
||||
import { Telemetry } from '@/telemetry';
|
||||
import { RoleService } from './role/role.service';
|
||||
|
||||
export class InternalHooksClass implements IInternalHooksClass {
|
||||
private versionCli: string;
|
||||
@ -111,6 +112,14 @@ export class InternalHooksClass implements IInternalHooksClass {
|
||||
(note) => note.overlapping,
|
||||
).length;
|
||||
|
||||
let userRole: 'owner' | 'sharee' | undefined = undefined;
|
||||
if (userId && workflow.id) {
|
||||
const role = await RoleService.getUserRoleForWorkflow(userId, workflow.id.toString());
|
||||
if (role) {
|
||||
userRole = role.name === 'owner' ? 'owner' : 'sharee';
|
||||
}
|
||||
}
|
||||
|
||||
return this.telemetry.track(
|
||||
'User saved workflow',
|
||||
{
|
||||
@ -122,6 +131,7 @@ export class InternalHooksClass implements IInternalHooksClass {
|
||||
version_cli: this.versionCli,
|
||||
num_tags: workflow.tags?.length ?? 0,
|
||||
public_api: publicApi,
|
||||
sharing_role: userRole,
|
||||
},
|
||||
{ withPostHog: true },
|
||||
);
|
||||
@ -196,6 +206,14 @@ export class InternalHooksClass implements IInternalHooksClass {
|
||||
nodeGraphResult = TelemetryHelpers.generateNodesGraph(workflow, this.nodeTypes);
|
||||
}
|
||||
|
||||
let userRole: 'owner' | 'sharee' | undefined = undefined;
|
||||
if (userId) {
|
||||
const role = await RoleService.getUserRoleForWorkflow(userId, workflow.id.toString());
|
||||
if (role) {
|
||||
userRole = role.name === 'owner' ? 'owner' : 'sharee';
|
||||
}
|
||||
}
|
||||
|
||||
const manualExecEventProperties: ITelemetryTrackProperties = {
|
||||
user_id: userId,
|
||||
workflow_id: workflow.id.toString(),
|
||||
@ -205,6 +223,7 @@ export class InternalHooksClass implements IInternalHooksClass {
|
||||
node_graph_string: properties.node_graph_string as string,
|
||||
error_node_id: properties.error_node_id as string,
|
||||
webhook_domain: null,
|
||||
sharing_role: userRole,
|
||||
};
|
||||
|
||||
if (!manualExecEventProperties.node_graph_string) {
|
||||
@ -254,6 +273,16 @@ export class InternalHooksClass implements IInternalHooksClass {
|
||||
]).then(() => {});
|
||||
}
|
||||
|
||||
async onWorkflowSharingUpdate(workflowId: string, userId: string, userList: string[]) {
|
||||
const properties: ITelemetryTrackProperties = {
|
||||
workflow_id: workflowId,
|
||||
user_id_sharer: userId,
|
||||
user_id_list: userList,
|
||||
};
|
||||
|
||||
return this.telemetry.track('User updated workflow sharing', properties, { withPostHog: true });
|
||||
}
|
||||
|
||||
async onN8nStop(): Promise<void> {
|
||||
const timeoutPromise = new Promise<void>((resolve) => {
|
||||
setTimeout(() => {
|
||||
|
@ -10,7 +10,7 @@ import type { ICredentialsDb } from '@/Interfaces';
|
||||
import { CredentialsEntity } from '@db/entities/CredentialsEntity';
|
||||
import { SharedCredentials } from '@db/entities/SharedCredentials';
|
||||
import { User } from '@db/entities/User';
|
||||
import { externalHooks } from '@/Server';
|
||||
import { ExternalHooks } from '@/ExternalHooks';
|
||||
import { IDependency, IJsonSchema } from '../../../types';
|
||||
import { CredentialRequest } from '@/requests';
|
||||
|
||||
@ -74,7 +74,7 @@ export async function saveCredential(
|
||||
scope: 'credential',
|
||||
});
|
||||
|
||||
await externalHooks.run('credentials.create', [encryptedData]);
|
||||
await ExternalHooks().run('credentials.create', [encryptedData]);
|
||||
|
||||
return Db.transaction(async (transactionManager) => {
|
||||
const savedCredential = await transactionManager.save<CredentialsEntity>(credential);
|
||||
@ -96,7 +96,7 @@ export async function saveCredential(
|
||||
}
|
||||
|
||||
export async function removeCredential(credentials: CredentialsEntity): Promise<ICredentialsDb> {
|
||||
await externalHooks.run('credentials.delete', [credentials.id]);
|
||||
await ExternalHooks().run('credentials.delete', [credentials.id]);
|
||||
return Db.collections.Credentials.remove(credentials);
|
||||
}
|
||||
|
||||
|
@ -7,7 +7,7 @@ import * as ActiveWorkflowRunner from '@/ActiveWorkflowRunner';
|
||||
import config from '@/config';
|
||||
import { WorkflowEntity } from '@db/entities/WorkflowEntity';
|
||||
import { InternalHooksManager } from '@/InternalHooksManager';
|
||||
import { externalHooks } from '@/Server';
|
||||
import { ExternalHooks } from '@/ExternalHooks';
|
||||
import { addNodeIds, replaceInvalidCredentials } from '@/WorkflowHelpers';
|
||||
import { WorkflowRequest } from '../../../types';
|
||||
import { authorize, validCursor } from '../../shared/middlewares/global.middleware';
|
||||
@ -49,7 +49,7 @@ export = {
|
||||
|
||||
const createdWorkflow = await createWorkflow(workflow, req.user, role);
|
||||
|
||||
await externalHooks.run('workflow.afterCreate', [createdWorkflow]);
|
||||
await ExternalHooks().run('workflow.afterCreate', [createdWorkflow]);
|
||||
void InternalHooksManager.getInstance().onWorkflowCreated(req.user.id, createdWorkflow, true);
|
||||
|
||||
return res.json(createdWorkflow);
|
||||
@ -76,7 +76,7 @@ export = {
|
||||
await Db.collections.Workflow.delete(id);
|
||||
|
||||
void InternalHooksManager.getInstance().onWorkflowDeleted(req.user.id, id.toString(), true);
|
||||
await externalHooks.run('workflow.afterDelete', [id.toString()]);
|
||||
await ExternalHooks().run('workflow.afterDelete', [id.toString()]);
|
||||
|
||||
return res.json(sharedWorkflow.workflow);
|
||||
},
|
||||
@ -219,7 +219,7 @@ export = {
|
||||
|
||||
const updatedWorkflow = await getWorkflowById(sharedWorkflow.workflowId);
|
||||
|
||||
await externalHooks.run('workflow.afterUpdate', [updateData]);
|
||||
await ExternalHooks().run('workflow.afterUpdate', [updateData]);
|
||||
void InternalHooksManager.getInstance().onWorkflowSaved(req.user.id, updateData, true);
|
||||
|
||||
return res.json(updatedWorkflow);
|
||||
|
@ -165,7 +165,7 @@ require('body-parser-xml')(bodyParser);
|
||||
|
||||
const exec = promisify(callbackExec);
|
||||
|
||||
export const externalHooks: IExternalHooksClass = ExternalHooks();
|
||||
const externalHooks: IExternalHooksClass = ExternalHooks();
|
||||
|
||||
class App {
|
||||
app: express.Application;
|
||||
@ -356,7 +356,6 @@ class App {
|
||||
},
|
||||
enterprise: {
|
||||
sharing: false,
|
||||
workflowSharing: false,
|
||||
},
|
||||
hideUsagePage: config.getEnv('hideUsagePage'),
|
||||
license: {
|
||||
@ -389,7 +388,6 @@ class App {
|
||||
// refresh enterprise status
|
||||
Object.assign(this.frontendSettings.enterprise, {
|
||||
sharing: isSharingEnabled(),
|
||||
workflowSharing: config.getEnv('enterprise.workflowSharingEnabled'),
|
||||
});
|
||||
|
||||
if (config.get('nodes.packagesMissing').length > 0) {
|
||||
@ -1003,7 +1001,7 @@ class App {
|
||||
});
|
||||
|
||||
if (!shared) {
|
||||
LoggerProxy.info('User attempted to access workflow errors without permissions', {
|
||||
LoggerProxy.verbose('User attempted to access workflow errors without permissions', {
|
||||
workflowId,
|
||||
userId: req.user.id,
|
||||
});
|
||||
@ -1515,7 +1513,9 @@ class App {
|
||||
identifier,
|
||||
);
|
||||
if (mimeType) res.setHeader('Content-Type', mimeType);
|
||||
if (fileName) res.setHeader('Content-Disposition', `attachment; filename="${fileName}"`);
|
||||
if (req.query.mode === 'download' && fileName) {
|
||||
res.setHeader('Content-Disposition', `attachment; filename="${fileName}"`);
|
||||
}
|
||||
res.setHeader('Content-Length', fileSize);
|
||||
res.sendFile(binaryPath);
|
||||
},
|
||||
|
@ -1,9 +1,17 @@
|
||||
import { INode, NodeOperationError, Workflow } from 'n8n-workflow';
|
||||
import {
|
||||
INode,
|
||||
NodeOperationError,
|
||||
SubworkflowOperationError,
|
||||
Workflow,
|
||||
WorkflowOperationError,
|
||||
} from 'n8n-workflow';
|
||||
import { FindManyOptions, In, ObjectLiteral } from 'typeorm';
|
||||
import * as Db from '@/Db';
|
||||
import config from '@/config';
|
||||
import type { SharedCredentials } from '@db/entities/SharedCredentials';
|
||||
import { getRole } from './UserManagementHelper';
|
||||
import { getRole, getWorkflowOwner, isSharingEnabled } from './UserManagementHelper';
|
||||
import { WorkflowsService } from '@/workflows/workflows.services';
|
||||
import { UserService } from '@/user/user.service';
|
||||
|
||||
export class PermissionChecker {
|
||||
/**
|
||||
@ -31,7 +39,7 @@ export class PermissionChecker {
|
||||
|
||||
let workflowUserIds = [userId];
|
||||
|
||||
if (workflow.id && config.getEnv('enterprise.workflowSharingEnabled')) {
|
||||
if (workflow.id && isSharingEnabled()) {
|
||||
const workflowSharings = await Db.collections.SharedWorkflow.find({
|
||||
relations: ['workflow'],
|
||||
where: { workflow: { id: Number(workflow.id) } },
|
||||
@ -44,7 +52,7 @@ export class PermissionChecker {
|
||||
where: { user: In(workflowUserIds) },
|
||||
};
|
||||
|
||||
if (!config.getEnv('enterprise.features.sharing')) {
|
||||
if (!isSharingEnabled()) {
|
||||
// If credential sharing is not enabled, get only credentials owned by this user
|
||||
credentialsWhereCondition.where.role = await getRole('credential', 'owner');
|
||||
}
|
||||
@ -53,7 +61,7 @@ export class PermissionChecker {
|
||||
credentialsWhereCondition,
|
||||
);
|
||||
|
||||
const accessibleCredIds = credentialSharings.map((s) => s.credentialId.toString());
|
||||
const accessibleCredIds = credentialSharings.map((s) => s.credentialsId.toString());
|
||||
|
||||
const inaccessibleCredIds = workflowCredIds.filter((id) => !accessibleCredIds.includes(id));
|
||||
|
||||
@ -68,6 +76,72 @@ export class PermissionChecker {
|
||||
});
|
||||
}
|
||||
|
||||
static async checkSubworkflowExecutePolicy(
|
||||
subworkflow: Workflow,
|
||||
userId: string,
|
||||
parentWorkflowId?: string,
|
||||
) {
|
||||
/**
|
||||
* Important considerations: both the current workflow and the parent can have empty IDs.
|
||||
* This happens when a user is executing an unsaved workflow manually running a workflow
|
||||
* loaded from a file or code, for instance.
|
||||
* This is an important topic to keep in mind for all security checks
|
||||
*/
|
||||
if (!subworkflow.id) {
|
||||
// It's a workflow from code and not loaded from DB
|
||||
// No checks are necessary since it doesn't have any sort of settings
|
||||
return;
|
||||
}
|
||||
|
||||
let policy =
|
||||
subworkflow.settings?.callerPolicy ?? config.getEnv('workflows.callerPolicyDefaultOption');
|
||||
|
||||
if (!isSharingEnabled()) {
|
||||
// Community version allows only same owner workflows
|
||||
policy = 'workflowsFromSameOwner';
|
||||
}
|
||||
|
||||
const subworkflowOwner = await getWorkflowOwner(subworkflow.id);
|
||||
|
||||
const errorToThrow = new SubworkflowOperationError(
|
||||
`Target workflow ID ${subworkflow.id ?? ''} may not be called`,
|
||||
subworkflowOwner.id === userId
|
||||
? 'Change the settings of the sub-workflow so it can be called by this one.'
|
||||
: `${subworkflowOwner.firstName} (${subworkflowOwner.email}) can make this change. You may need to tell them the ID of this workflow, which is ${subworkflow.id}`,
|
||||
);
|
||||
|
||||
if (policy === 'none') {
|
||||
throw errorToThrow;
|
||||
}
|
||||
|
||||
if (policy === 'workflowsFromAList') {
|
||||
if (parentWorkflowId === undefined) {
|
||||
throw errorToThrow;
|
||||
}
|
||||
const allowedCallerIds = (subworkflow.settings.callerIds as string | undefined)
|
||||
?.split(',')
|
||||
.map((id) => id.trim())
|
||||
.filter((id) => id !== '');
|
||||
|
||||
if (!allowedCallerIds?.includes(parentWorkflowId)) {
|
||||
throw errorToThrow;
|
||||
}
|
||||
}
|
||||
|
||||
if (policy === 'workflowsFromSameOwner') {
|
||||
const user = await UserService.get({ id: userId });
|
||||
if (!user) {
|
||||
throw new WorkflowOperationError(
|
||||
'Fatal error: user not found. Please contact the system administrator.',
|
||||
);
|
||||
}
|
||||
const sharing = await WorkflowsService.getSharing(user, subworkflow.id, ['role', 'user']);
|
||||
if (!sharing || sharing.role.name !== 'owner') {
|
||||
throw errorToThrow;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static mapCredIdsToNodes(workflow: Workflow) {
|
||||
return Object.values(workflow.nodes).reduce<{ [credentialId: string]: INode[] }>(
|
||||
(map, node) => {
|
||||
|
@ -15,10 +15,13 @@ import config from '@/config';
|
||||
import { getWebhookBaseUrl } from '../WebhookHelpers';
|
||||
import { getLicense } from '@/License';
|
||||
import { WhereClause } from '@/Interfaces';
|
||||
import { RoleService } from '@/role/role.service';
|
||||
|
||||
export async function getWorkflowOwner(workflowId: string | number): Promise<User> {
|
||||
const workflowOwnerRole = await RoleService.get({ name: 'owner', scope: 'workflow' });
|
||||
|
||||
const sharedWorkflow = await Db.collections.SharedWorkflow.findOneOrFail({
|
||||
where: { workflow: { id: workflowId } },
|
||||
where: { workflow: { id: workflowId }, role: workflowOwnerRole },
|
||||
relations: ['user', 'user.globalRole'],
|
||||
});
|
||||
|
||||
|
@ -25,6 +25,7 @@ import {
|
||||
import config from '@/config';
|
||||
import { issueCookie } from '../auth/jwt';
|
||||
import { InternalHooksManager } from '@/InternalHooksManager';
|
||||
import { RoleService } from '@/role/role.service';
|
||||
|
||||
export function usersNamespace(this: N8nApp): void {
|
||||
/**
|
||||
@ -403,33 +404,94 @@ export function usersNamespace(this: N8nApp): void {
|
||||
|
||||
const userToDelete = users.find((user) => user.id === req.params.id) as User;
|
||||
|
||||
const telemetryData: ITelemetryUserDeletionData = {
|
||||
user_id: req.user.id,
|
||||
target_user_old_status: userToDelete.isPending ? 'invited' : 'active',
|
||||
target_user_id: idToDelete,
|
||||
};
|
||||
|
||||
telemetryData.migration_strategy = transferId ? 'transfer_data' : 'delete_data';
|
||||
|
||||
if (transferId) {
|
||||
telemetryData.migration_user_id = transferId;
|
||||
}
|
||||
|
||||
const [workflowOwnerRole, credentialOwnerRole] = await Promise.all([
|
||||
RoleService.get({ name: 'owner', scope: 'workflow' }),
|
||||
RoleService.get({ name: 'owner', scope: 'credential' }),
|
||||
]);
|
||||
|
||||
if (transferId) {
|
||||
const transferee = users.find((user) => user.id === transferId);
|
||||
|
||||
await Db.transaction(async (transactionManager) => {
|
||||
// Get all workflow ids belonging to user to delete
|
||||
const sharedWorkflows = await transactionManager.getRepository(SharedWorkflow).find({
|
||||
where: { user: userToDelete, role: workflowOwnerRole },
|
||||
});
|
||||
|
||||
const sharedWorkflowIds = sharedWorkflows.map((sharedWorkflow) =>
|
||||
sharedWorkflow.workflowId.toString(),
|
||||
);
|
||||
|
||||
// Prevents issues with unique key constraints since user being assigned
|
||||
// workflows and credentials might be a sharee
|
||||
await transactionManager.delete(SharedWorkflow, {
|
||||
user: transferee,
|
||||
workflowId: In(sharedWorkflowIds),
|
||||
});
|
||||
|
||||
// Transfer ownership of owned workflows
|
||||
await transactionManager.update(
|
||||
SharedWorkflow,
|
||||
{ user: userToDelete },
|
||||
{ user: userToDelete, role: workflowOwnerRole },
|
||||
{ user: transferee },
|
||||
);
|
||||
|
||||
// Now do the same for creds
|
||||
|
||||
// Get all workflow ids belonging to user to delete
|
||||
const sharedCredentials = await transactionManager.getRepository(SharedCredentials).find({
|
||||
where: { user: userToDelete, role: credentialOwnerRole },
|
||||
});
|
||||
|
||||
const sharedCredentialIds = sharedCredentials.map((sharedCredential) =>
|
||||
sharedCredential.credentialsId.toString(),
|
||||
);
|
||||
|
||||
// Prevents issues with unique key constraints since user being assigned
|
||||
// workflows and credentials might be a sharee
|
||||
await transactionManager.delete(SharedCredentials, {
|
||||
user: transferee,
|
||||
credentials: In(
|
||||
sharedCredentialIds.map((sharedCredentialId) => ({ id: sharedCredentialId })),
|
||||
),
|
||||
});
|
||||
|
||||
// Transfer ownership of owned credentials
|
||||
await transactionManager.update(
|
||||
SharedCredentials,
|
||||
{ user: userToDelete },
|
||||
{ user: userToDelete, role: credentialOwnerRole },
|
||||
{ user: transferee },
|
||||
);
|
||||
|
||||
// This will remove all shared workflows and credentials not owned
|
||||
await transactionManager.delete(User, { id: userToDelete.id });
|
||||
});
|
||||
|
||||
void InternalHooksManager.getInstance().onUserDeletion(req.user.id, telemetryData, false);
|
||||
await this.externalHooks.run('user.deleted', [sanitizeUser(userToDelete)]);
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
const [ownedSharedWorkflows, ownedSharedCredentials] = await Promise.all([
|
||||
Db.collections.SharedWorkflow.find({
|
||||
relations: ['workflow'],
|
||||
where: { user: userToDelete },
|
||||
where: { user: userToDelete, role: workflowOwnerRole },
|
||||
}),
|
||||
Db.collections.SharedCredentials.find({
|
||||
relations: ['credentials'],
|
||||
where: { user: userToDelete },
|
||||
where: { user: userToDelete, role: credentialOwnerRole },
|
||||
}),
|
||||
]);
|
||||
|
||||
@ -450,22 +512,8 @@ export function usersNamespace(this: N8nApp): void {
|
||||
await transactionManager.delete(User, { id: userToDelete.id });
|
||||
});
|
||||
|
||||
const telemetryData: ITelemetryUserDeletionData = {
|
||||
user_id: req.user.id,
|
||||
target_user_old_status: userToDelete.isPending ? 'invited' : 'active',
|
||||
target_user_id: idToDelete,
|
||||
};
|
||||
|
||||
telemetryData.migration_strategy = transferId ? 'transfer_data' : 'delete_data';
|
||||
|
||||
if (transferId) {
|
||||
telemetryData.migration_user_id = transferId;
|
||||
}
|
||||
|
||||
void InternalHooksManager.getInstance().onUserDeletion(req.user.id, telemetryData, false);
|
||||
|
||||
await this.externalHooks.run('user.deleted', [sanitizeUser(userToDelete)]);
|
||||
|
||||
return { success: true };
|
||||
}),
|
||||
);
|
||||
|
@ -65,6 +65,7 @@ import * as WorkflowHelpers from '@/WorkflowHelpers';
|
||||
import { getUserById, getWorkflowOwner, whereClause } from '@/UserManagement/UserManagementHelper';
|
||||
import { findSubworkflowStart } from '@/utils';
|
||||
import { PermissionChecker } from './UserManagement/PermissionChecker';
|
||||
import { WorkflowsService } from './workflows/workflows.services';
|
||||
|
||||
const ERROR_TRIGGER_TYPE = config.getEnv('nodes.errorTriggerType');
|
||||
|
||||
@ -779,34 +780,6 @@ export async function getRunData(
|
||||
): Promise<IWorkflowExecutionDataProcess> {
|
||||
const mode = 'integrated';
|
||||
|
||||
const policy =
|
||||
workflowData.settings?.callerPolicy ?? config.getEnv('workflows.callerPolicyDefaultOption');
|
||||
|
||||
if (policy === 'none') {
|
||||
throw new SubworkflowOperationError(
|
||||
`Target workflow ID ${workflowData.id} may not be called by other workflows.`,
|
||||
'Please update the settings of the target workflow or ask its owner to do so.',
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
policy === 'workflowsFromAList' &&
|
||||
typeof workflowData.settings?.callerIds === 'string' &&
|
||||
parentWorkflowId !== undefined
|
||||
) {
|
||||
const allowedCallerIds = workflowData.settings.callerIds
|
||||
.split(',')
|
||||
.map((id) => id.trim())
|
||||
.filter((id) => id !== '');
|
||||
|
||||
if (!allowedCallerIds.includes(parentWorkflowId)) {
|
||||
throw new SubworkflowOperationError(
|
||||
`Target workflow ID ${workflowData.id} may only be called by a list of workflows, which does not include current workflow ID ${parentWorkflowId}.`,
|
||||
'Please update the settings of the target workflow or ask its owner to do so.',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const startingNode = findSubworkflowStart(workflowData.nodes);
|
||||
|
||||
// Always start with empty data if no inputData got supplied
|
||||
@ -852,7 +825,6 @@ export async function getRunData(
|
||||
|
||||
export async function getWorkflowData(
|
||||
workflowInfo: IExecuteWorkflowInfo,
|
||||
userId: string,
|
||||
parentWorkflowId?: string,
|
||||
parentWorkflowSettings?: IWorkflowSettings,
|
||||
): Promise<IWorkflowBase> {
|
||||
@ -869,23 +841,15 @@ export async function getWorkflowData(
|
||||
// to get initialized first
|
||||
await Db.init();
|
||||
}
|
||||
const user = await getUserById(userId);
|
||||
let relations = ['workflow', 'workflow.tags'];
|
||||
|
||||
if (config.getEnv('workflowTagsDisabled')) {
|
||||
relations = relations.filter((relation) => relation !== 'workflow.tags');
|
||||
}
|
||||
const relations = config.getEnv('workflowTagsDisabled') ? [] : ['tags'];
|
||||
|
||||
const shared = await Db.collections.SharedWorkflow.findOne({
|
||||
relations,
|
||||
where: whereClause({
|
||||
user,
|
||||
entityType: 'workflow',
|
||||
entityId: workflowInfo.id,
|
||||
}),
|
||||
});
|
||||
|
||||
workflowData = shared?.workflow;
|
||||
workflowData = await WorkflowsService.get(
|
||||
{ id: parseInt(workflowInfo.id, 10) },
|
||||
{
|
||||
relations,
|
||||
},
|
||||
);
|
||||
|
||||
if (workflowData === undefined) {
|
||||
throw new Error(`The workflow with the id "${workflowInfo.id}" does not exist.`);
|
||||
@ -911,7 +875,7 @@ export async function getWorkflowData(
|
||||
async function executeWorkflow(
|
||||
workflowInfo: IExecuteWorkflowInfo,
|
||||
additionalData: IWorkflowExecuteAdditionalData,
|
||||
options?: {
|
||||
options: {
|
||||
parentWorkflowId?: string;
|
||||
inputData?: INodeExecutionData[];
|
||||
parentExecutionId?: string;
|
||||
@ -926,13 +890,8 @@ async function executeWorkflow(
|
||||
const nodeTypes = NodeTypes();
|
||||
|
||||
const workflowData =
|
||||
options?.loadedWorkflowData ??
|
||||
(await getWorkflowData(
|
||||
workflowInfo,
|
||||
additionalData.userId,
|
||||
options?.parentWorkflowId,
|
||||
options?.parentWorkflowSettings,
|
||||
));
|
||||
options.loadedWorkflowData ??
|
||||
(await getWorkflowData(workflowInfo, options.parentWorkflowId, options.parentWorkflowSettings));
|
||||
|
||||
const workflowName = workflowData ? workflowData.name : undefined;
|
||||
const workflow = new Workflow({
|
||||
@ -947,23 +906,28 @@ async function executeWorkflow(
|
||||
});
|
||||
|
||||
const runData =
|
||||
options?.loadedRunData ??
|
||||
(await getRunData(workflowData, additionalData.userId, options?.inputData));
|
||||
options.loadedRunData ??
|
||||
(await getRunData(workflowData, additionalData.userId, options.inputData));
|
||||
|
||||
let executionId;
|
||||
|
||||
if (options?.parentExecutionId !== undefined) {
|
||||
executionId = options?.parentExecutionId;
|
||||
if (options.parentExecutionId !== undefined) {
|
||||
executionId = options.parentExecutionId;
|
||||
} else {
|
||||
executionId =
|
||||
options?.parentExecutionId !== undefined
|
||||
? options?.parentExecutionId
|
||||
options.parentExecutionId !== undefined
|
||||
? options.parentExecutionId
|
||||
: await ActiveExecutions.getInstance().add(runData);
|
||||
}
|
||||
|
||||
let data;
|
||||
try {
|
||||
await PermissionChecker.check(workflow, additionalData.userId);
|
||||
await PermissionChecker.checkSubworkflowExecutePolicy(
|
||||
workflow,
|
||||
additionalData.userId,
|
||||
options.parentWorkflowId,
|
||||
);
|
||||
|
||||
// Create new additionalData to have different workflow loaded and to call
|
||||
// different webhooks
|
||||
@ -1005,7 +969,7 @@ async function executeWorkflow(
|
||||
runData.executionMode,
|
||||
runExecutionData,
|
||||
);
|
||||
if (options?.parentExecutionId !== undefined) {
|
||||
if (options.parentExecutionId !== undefined) {
|
||||
// Must be changed to become typed
|
||||
return {
|
||||
startedAt: new Date(),
|
||||
@ -1049,6 +1013,7 @@ async function executeWorkflow(
|
||||
throw {
|
||||
...error,
|
||||
stack: error.stack,
|
||||
message: error.message,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -24,6 +24,7 @@ import config from '@/config';
|
||||
import { WorkflowEntity } from '@db/entities/WorkflowEntity';
|
||||
import { User } from '@db/entities/User';
|
||||
import { getWorkflowOwner, whereClause } from '@/UserManagement/UserManagementHelper';
|
||||
import omit from 'lodash.omit';
|
||||
|
||||
const ERROR_TRIGGER_TYPE = config.getEnv('nodes.errorTriggerType');
|
||||
|
||||
@ -558,15 +559,16 @@ export function validateWorkflowCredentialUsage(
|
||||
|
||||
nodesWithCredentialsUserDoesNotHaveAccessTo.forEach((node) => {
|
||||
if (isTamperingAttempt(node.id)) {
|
||||
Logger.info('Blocked workflow update due to tampering attempt', {
|
||||
Logger.verbose('Blocked workflow update due to tampering attempt', {
|
||||
nodeType: node.type,
|
||||
nodeName: node.name,
|
||||
nodeId: node.id,
|
||||
nodeCredentials: node.credentials,
|
||||
});
|
||||
// Node is new, so this is probably a tampering attempt. Throw an error
|
||||
throw new Error(
|
||||
'Workflow contains new nodes with credentials the user does not have access to',
|
||||
throw new NodeOperationError(
|
||||
node,
|
||||
`You don't have access to the credentials in the '${node.name}' node. Ask the owner to share them with you.`,
|
||||
);
|
||||
}
|
||||
// Replace the node with the previous version of the node
|
||||
@ -580,9 +582,14 @@ export function validateWorkflowCredentialUsage(
|
||||
nodeName: node.name,
|
||||
nodeId: node.id,
|
||||
});
|
||||
newWorkflowVersion.nodes[nodeIdx] = previousWorkflowVersion.nodes.find(
|
||||
const previousNodeVersion = previousWorkflowVersion.nodes.find(
|
||||
(previousNode) => previousNode.id === node.id,
|
||||
)!;
|
||||
);
|
||||
// Allow changing only name, position and disabled status for read-only nodes
|
||||
Object.assign(
|
||||
newWorkflowVersion.nodes[nodeIdx],
|
||||
omit(previousNodeVersion, ['name', 'position', 'disabled']),
|
||||
);
|
||||
});
|
||||
|
||||
return newWorkflowVersion;
|
||||
|
@ -48,6 +48,7 @@ import { InternalHooksManager } from '@/InternalHooksManager';
|
||||
import { generateFailedExecutionFromError } from '@/WorkflowHelpers';
|
||||
import { initErrorHandling } from '@/ErrorReporting';
|
||||
import { PermissionChecker } from '@/UserManagement/PermissionChecker';
|
||||
import { getLicense } from './License';
|
||||
|
||||
class WorkflowRunnerProcess {
|
||||
data: IWorkflowExecutionDataProcessWithExecution | undefined;
|
||||
@ -118,48 +119,11 @@ class WorkflowRunnerProcess {
|
||||
const binaryDataConfig = config.getEnv('binaryDataManager');
|
||||
await BinaryDataManager.init(binaryDataConfig);
|
||||
|
||||
// Credentials should now be loaded from database.
|
||||
// We check if any node uses credentials. If it does, then
|
||||
// init database.
|
||||
let shouldInitializeDb = false;
|
||||
// eslint-disable-next-line array-callback-return
|
||||
inputData.workflowData.nodes.map((node) => {
|
||||
if (Object.keys(node.credentials === undefined ? {} : node.credentials).length > 0) {
|
||||
shouldInitializeDb = true;
|
||||
}
|
||||
if (node.type === 'n8n-nodes-base.executeWorkflow') {
|
||||
// With UM, child workflows from arbitrary JSON
|
||||
// Should be persisted by the child process,
|
||||
// so DB needs to be initialized
|
||||
shouldInitializeDb = true;
|
||||
}
|
||||
});
|
||||
// Init db since we need to read the license.
|
||||
await Db.init();
|
||||
|
||||
// This code has been split into 4 ifs just to make it easier to understand
|
||||
// Can be made smaller but in the end it will make it impossible to read.
|
||||
if (shouldInitializeDb) {
|
||||
// initialize db as we need to load credentials
|
||||
await Db.init();
|
||||
} else if (
|
||||
inputData.workflowData.settings !== undefined &&
|
||||
inputData.workflowData.settings.saveExecutionProgress === true
|
||||
) {
|
||||
// Workflow settings specifying it should save
|
||||
await Db.init();
|
||||
} else if (
|
||||
inputData.workflowData.settings !== undefined &&
|
||||
inputData.workflowData.settings.saveExecutionProgress !== false &&
|
||||
config.getEnv('executions.saveExecutionProgress')
|
||||
) {
|
||||
// Workflow settings not saying anything about saving but default settings says so
|
||||
await Db.init();
|
||||
} else if (
|
||||
inputData.workflowData.settings === undefined &&
|
||||
config.getEnv('executions.saveExecutionProgress')
|
||||
) {
|
||||
// Workflow settings not saying anything about saving but default settings says so
|
||||
await Db.init();
|
||||
}
|
||||
const license = getLicense();
|
||||
await license.init(instanceId, cli);
|
||||
|
||||
// Start timeout for the execution
|
||||
let workflowTimeout = config.getEnv('executions.timeout'); // initialize with default
|
||||
@ -245,7 +209,6 @@ class WorkflowRunnerProcess {
|
||||
): Promise<Array<INodeExecutionData[] | null> | IRun> => {
|
||||
const workflowData = await WorkflowExecuteAdditionalData.getWorkflowData(
|
||||
workflowInfo,
|
||||
userId,
|
||||
options?.parentWorkflowId,
|
||||
options?.parentWorkflowSettings,
|
||||
);
|
||||
|
@ -2,13 +2,13 @@ import { User } from '@/databases/entities/User';
|
||||
import { whereClause } from '@/UserManagement/UserManagementHelper';
|
||||
import express from 'express';
|
||||
import { LoggerProxy } from 'n8n-workflow';
|
||||
import {
|
||||
Db,
|
||||
import * as Db from '@/Db';
|
||||
import * as ResponseHelper from '@/ResponseHelper';
|
||||
import type {
|
||||
IWorkflowStatisticsCounts,
|
||||
IWorkflowStatisticsDataLoaded,
|
||||
IWorkflowStatisticsTimestamps,
|
||||
ResponseHelper,
|
||||
} from '..';
|
||||
} from '@/Interfaces';
|
||||
import { StatisticsNames } from '../databases/entities/WorkflowStatistics';
|
||||
import { getLogger } from '../Logger';
|
||||
import { ExecutionRequest } from '../requests';
|
||||
@ -28,7 +28,7 @@ async function checkWorkflowId(workflowId: string, user: User): Promise<boolean>
|
||||
});
|
||||
|
||||
if (!shared) {
|
||||
LoggerProxy.info('User attempted to read a workflow without permissions', {
|
||||
LoggerProxy.verbose('User attempted to read a workflow without permissions', {
|
||||
workflowId,
|
||||
userId: user.id,
|
||||
});
|
||||
|
@ -27,7 +27,6 @@ const config = convict(schema);
|
||||
|
||||
if (inE2ETests) {
|
||||
config.set('enterprise.features.sharing', true);
|
||||
config.set('enterprise.workflowSharingEnabled', true);
|
||||
}
|
||||
|
||||
config.getEnv = config.get;
|
||||
|
@ -223,8 +223,8 @@ export const schema = {
|
||||
},
|
||||
callerPolicyDefaultOption: {
|
||||
doc: 'Default option for which workflows may call the current workflow',
|
||||
format: ['any', 'none', 'workflowsFromAList'] as const,
|
||||
default: 'any',
|
||||
format: ['any', 'none', 'workflowsFromAList', 'workflowsFromSameOwner'] as const,
|
||||
default: 'workflowsFromSameOwner',
|
||||
env: 'N8N_WORKFLOW_CALLER_POLICY_DEFAULT_OPTION',
|
||||
},
|
||||
},
|
||||
@ -891,12 +891,6 @@ export const schema = {
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
// This is a temporary flag (acting as feature toggle)
|
||||
// Will be removed when feature goes live
|
||||
workflowSharingEnabled: {
|
||||
format: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
|
||||
hiringBanner: {
|
||||
|
@ -35,7 +35,13 @@ EECredentialsController.get(
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/unbound-method
|
||||
return allCredentials.map(EECredentials.addOwnerAndSharings);
|
||||
return allCredentials
|
||||
.map((credential: CredentialsEntity & CredentialWithSharings) =>
|
||||
EECredentials.addOwnerAndSharings(credential),
|
||||
)
|
||||
.map(
|
||||
(credential): CredentialWithSharings => ({ ...credential, id: credential.id.toString() }),
|
||||
);
|
||||
} catch (error) {
|
||||
LoggerProxy.error('Request to list credentials failed', error as Error);
|
||||
throw error;
|
||||
|
@ -1,14 +1,7 @@
|
||||
/* eslint-disable no-param-reassign */
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
import express from 'express';
|
||||
import {
|
||||
deepCopy,
|
||||
ICredentialType,
|
||||
INodeCredentialTestResult,
|
||||
LoggerProxy,
|
||||
NodeHelpers,
|
||||
} from 'n8n-workflow';
|
||||
import { Credentials } from 'n8n-core';
|
||||
import { deepCopy, INodeCredentialTestResult, LoggerProxy } from 'n8n-workflow';
|
||||
|
||||
import * as GenericHelpers from '@/GenericHelpers';
|
||||
import { InternalHooksManager } from '@/InternalHooksManager';
|
||||
@ -17,7 +10,6 @@ import config from '@/config';
|
||||
import { getLogger } from '@/Logger';
|
||||
import { EECredentialsController } from './credentials.controller.ee';
|
||||
import { CredentialsService } from './credentials.service';
|
||||
import { CredentialTypes } from '@/CredentialTypes';
|
||||
|
||||
import type { ICredentialsResponse } from '@/Interfaces';
|
||||
import type { CredentialRequest } from '@/requests';
|
||||
|
@ -20,7 +20,7 @@ import { CREDENTIAL_BLANKING_VALUE, RESPONSE_ERROR_MESSAGES } from '@/constants'
|
||||
import { CredentialsEntity } from '@db/entities/CredentialsEntity';
|
||||
import { SharedCredentials } from '@db/entities/SharedCredentials';
|
||||
import { validateEntity } from '@/GenericHelpers';
|
||||
import { externalHooks } from '../Server';
|
||||
import { ExternalHooks } from '@/ExternalHooks';
|
||||
|
||||
import type { User } from '@db/entities/User';
|
||||
import type { CredentialRequest } from '@/requests';
|
||||
@ -80,7 +80,7 @@ export class CredentialsService {
|
||||
select: SELECT_FIELDS,
|
||||
relations: options?.relations,
|
||||
where: {
|
||||
id: In(userSharings.map((x) => x.credentialId)),
|
||||
id: In(userSharings.map((x) => x.credentialsId)),
|
||||
},
|
||||
});
|
||||
}
|
||||
@ -234,7 +234,7 @@ export class CredentialsService {
|
||||
credentialId: string,
|
||||
newCredentialData: ICredentialsDb,
|
||||
): Promise<ICredentialsDb | undefined> {
|
||||
await externalHooks.run('credentials.update', [newCredentialData]);
|
||||
await ExternalHooks().run('credentials.update', [newCredentialData]);
|
||||
|
||||
// Update the credentials in DB
|
||||
await Db.collections.Credentials.update(credentialId, newCredentialData);
|
||||
@ -253,7 +253,7 @@ export class CredentialsService {
|
||||
const newCredential = new CredentialsEntity();
|
||||
Object.assign(newCredential, credential, encryptedData);
|
||||
|
||||
await externalHooks.run('credentials.create', [encryptedData]);
|
||||
await ExternalHooks().run('credentials.create', [encryptedData]);
|
||||
|
||||
const role = await Db.collections.Role.findOneOrFail({
|
||||
name: 'owner',
|
||||
@ -285,7 +285,7 @@ export class CredentialsService {
|
||||
}
|
||||
|
||||
static async delete(credentials: CredentialsEntity): Promise<void> {
|
||||
await externalHooks.run('credentials.delete', [credentials.id]);
|
||||
await ExternalHooks().run('credentials.delete', [credentials.id]);
|
||||
|
||||
await Db.collections.Credentials.remove(credentials);
|
||||
}
|
||||
|
@ -27,7 +27,7 @@ import {
|
||||
} from '@/CredentialsHelper';
|
||||
import { getLogger } from '@/Logger';
|
||||
import { OAuthRequest } from '@/requests';
|
||||
import { externalHooks } from '@/Server';
|
||||
import { ExternalHooks } from '@/ExternalHooks';
|
||||
import config from '@/config';
|
||||
import { getInstanceBaseUrl } from '@/UserManagement/UserManagementHelper';
|
||||
|
||||
@ -129,7 +129,7 @@ oauth2CredentialController.get(
|
||||
state: stateEncodedStr,
|
||||
};
|
||||
|
||||
await externalHooks.run('oauth2.authenticate', [oAuthOptions]);
|
||||
await ExternalHooks().run('oauth2.authenticate', [oAuthOptions]);
|
||||
|
||||
const oAuthObj = new ClientOAuth2(oAuthOptions);
|
||||
|
||||
@ -281,7 +281,7 @@ oauth2CredentialController.get(
|
||||
delete oAuth2Parameters.clientSecret;
|
||||
}
|
||||
|
||||
await externalHooks.run('oauth2.callback', [oAuth2Parameters]);
|
||||
await ExternalHooks().run('oauth2.callback', [oAuth2Parameters]);
|
||||
|
||||
const oAuthObj = new ClientOAuth2(oAuth2Parameters);
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
import type { ICredentialNodeAccess } from 'n8n-workflow';
|
||||
import { Column, Entity, Index, OneToMany, PrimaryGeneratedColumn } from 'typeorm';
|
||||
import { IsArray, IsObject, IsString, Length } from 'class-validator';
|
||||
import { SharedCredentials } from './SharedCredentials';
|
||||
import type { SharedCredentials } from './SharedCredentials';
|
||||
import { AbstractEntity, jsonColumnType } from './AbstractEntity';
|
||||
import type { ICredentialsDb } from '@/Interfaces';
|
||||
|
||||
@ -28,7 +28,7 @@ export class CredentialsEntity extends AbstractEntity implements ICredentialsDb
|
||||
})
|
||||
type: string;
|
||||
|
||||
@OneToMany(() => SharedCredentials, (sharedCredentials) => sharedCredentials.credentials)
|
||||
@OneToMany('SharedCredentials', 'credentials')
|
||||
shared: SharedCredentials[];
|
||||
|
||||
@Column(jsonColumnType)
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { Column, Entity, JoinColumn, ManyToOne, PrimaryColumn } from 'typeorm';
|
||||
import { InstalledPackages } from './InstalledPackages';
|
||||
import type { InstalledPackages } from './InstalledPackages';
|
||||
|
||||
@Entity()
|
||||
export class InstalledNodes {
|
||||
@ -12,10 +12,7 @@ export class InstalledNodes {
|
||||
@Column()
|
||||
latestVersion: string;
|
||||
|
||||
@ManyToOne(
|
||||
() => InstalledPackages,
|
||||
(installedPackages: InstalledPackages) => installedPackages.installedNodes,
|
||||
)
|
||||
@ManyToOne('InstalledPackages', 'installedNodes')
|
||||
@JoinColumn({ name: 'package', referencedColumnName: 'packageName' })
|
||||
package: InstalledPackages;
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { Column, Entity, JoinColumn, OneToMany, PrimaryColumn } from 'typeorm';
|
||||
import { InstalledNodes } from './InstalledNodes';
|
||||
import type { InstalledNodes } from './InstalledNodes';
|
||||
import { AbstractEntity } from './AbstractEntity';
|
||||
|
||||
@Entity()
|
||||
@ -16,7 +16,7 @@ export class InstalledPackages extends AbstractEntity {
|
||||
@Column()
|
||||
authorEmail?: string;
|
||||
|
||||
@OneToMany(() => InstalledNodes, (installedNode) => installedNode.package)
|
||||
@OneToMany('InstalledNodes', 'package')
|
||||
@JoinColumn({ referencedColumnName: 'package' })
|
||||
installedNodes: InstalledNodes[];
|
||||
}
|
||||
|
@ -1,9 +1,9 @@
|
||||
import { Column, Entity, OneToMany, PrimaryGeneratedColumn, Unique } from 'typeorm';
|
||||
import { IsString, Length } from 'class-validator';
|
||||
|
||||
import { User } from './User';
|
||||
import { SharedWorkflow } from './SharedWorkflow';
|
||||
import { SharedCredentials } from './SharedCredentials';
|
||||
import type { User } from './User';
|
||||
import type { SharedWorkflow } from './SharedWorkflow';
|
||||
import type { SharedCredentials } from './SharedCredentials';
|
||||
import { AbstractEntity } from './AbstractEntity';
|
||||
|
||||
export type RoleNames = 'owner' | 'member' | 'user' | 'editor';
|
||||
@ -23,12 +23,12 @@ export class Role extends AbstractEntity {
|
||||
@Column()
|
||||
scope: RoleScopes;
|
||||
|
||||
@OneToMany(() => User, (user) => user.globalRole)
|
||||
@OneToMany('User', 'globalRole')
|
||||
globalForUsers: User[];
|
||||
|
||||
@OneToMany(() => SharedWorkflow, (sharedWorkflow) => sharedWorkflow.role)
|
||||
@OneToMany('SharedWorkflow', 'role')
|
||||
sharedWorkflows: SharedWorkflow[];
|
||||
|
||||
@OneToMany(() => SharedCredentials, (sharedCredentials) => sharedCredentials.role)
|
||||
@OneToMany('SharedCredentials', 'role')
|
||||
sharedCredentials: SharedCredentials[];
|
||||
}
|
||||
|
@ -1,26 +1,28 @@
|
||||
import { Entity, ManyToOne, RelationId } from 'typeorm';
|
||||
import { CredentialsEntity } from './CredentialsEntity';
|
||||
import { User } from './User';
|
||||
import { Role } from './Role';
|
||||
import { Entity, ManyToOne, PrimaryColumn, RelationId } from 'typeorm';
|
||||
import type { CredentialsEntity } from './CredentialsEntity';
|
||||
import type { User } from './User';
|
||||
import type { Role } from './Role';
|
||||
import { AbstractEntity } from './AbstractEntity';
|
||||
|
||||
@Entity()
|
||||
export class SharedCredentials extends AbstractEntity {
|
||||
@ManyToOne(() => Role, (role) => role.sharedCredentials, { nullable: false })
|
||||
@ManyToOne('Role', 'sharedCredentials', { nullable: false })
|
||||
role: Role;
|
||||
|
||||
@ManyToOne(() => User, (user) => user.sharedCredentials, { primary: true })
|
||||
@ManyToOne('User', 'sharedCredentials', { primary: true })
|
||||
user: User;
|
||||
|
||||
@PrimaryColumn()
|
||||
@RelationId((sharedCredential: SharedCredentials) => sharedCredential.user)
|
||||
userId: string;
|
||||
|
||||
@ManyToOne(() => CredentialsEntity, (credentials) => credentials.shared, {
|
||||
@ManyToOne('CredentialsEntity', 'shared', {
|
||||
primary: true,
|
||||
onDelete: 'CASCADE',
|
||||
})
|
||||
credentials: CredentialsEntity;
|
||||
|
||||
@PrimaryColumn()
|
||||
@RelationId((sharedCredential: SharedCredentials) => sharedCredential.credentials)
|
||||
credentialId: number;
|
||||
credentialsId: number;
|
||||
}
|
||||
|
@ -1,26 +1,28 @@
|
||||
import { Entity, ManyToOne, RelationId } from 'typeorm';
|
||||
import { WorkflowEntity } from './WorkflowEntity';
|
||||
import { User } from './User';
|
||||
import { Role } from './Role';
|
||||
import { Entity, ManyToOne, PrimaryColumn, RelationId } from 'typeorm';
|
||||
import type { WorkflowEntity } from './WorkflowEntity';
|
||||
import type { User } from './User';
|
||||
import type { Role } from './Role';
|
||||
import { AbstractEntity } from './AbstractEntity';
|
||||
|
||||
@Entity()
|
||||
export class SharedWorkflow extends AbstractEntity {
|
||||
@ManyToOne(() => Role, (role) => role.sharedWorkflows, { nullable: false })
|
||||
@ManyToOne('Role', 'sharedWorkflows', { nullable: false })
|
||||
role: Role;
|
||||
|
||||
@ManyToOne(() => User, (user) => user.sharedWorkflows, { primary: true })
|
||||
@ManyToOne('User', 'sharedWorkflows', { primary: true })
|
||||
user: User;
|
||||
|
||||
@PrimaryColumn()
|
||||
@RelationId((sharedWorkflow: SharedWorkflow) => sharedWorkflow.user)
|
||||
userId: string;
|
||||
|
||||
@ManyToOne(() => WorkflowEntity, (workflow) => workflow.shared, {
|
||||
@ManyToOne('WorkflowEntity', 'shared', {
|
||||
primary: true,
|
||||
onDelete: 'CASCADE',
|
||||
})
|
||||
workflow: WorkflowEntity;
|
||||
|
||||
@PrimaryColumn()
|
||||
@RelationId((sharedWorkflow: SharedWorkflow) => sharedWorkflow.workflow)
|
||||
workflowId: number;
|
||||
}
|
||||
|
@ -1,9 +1,9 @@
|
||||
import { Column, Entity, Generated, Index, ManyToMany, PrimaryColumn } from 'typeorm';
|
||||
import { IsString, Length } from 'class-validator';
|
||||
|
||||
import { ITagDb } from '@/Interfaces';
|
||||
import type { ITagDb } from '@/Interfaces';
|
||||
import { idStringifier } from '../utils/transformers';
|
||||
import { WorkflowEntity } from './WorkflowEntity';
|
||||
import type { WorkflowEntity } from './WorkflowEntity';
|
||||
import { AbstractEntity } from './AbstractEntity';
|
||||
|
||||
@Entity()
|
||||
@ -20,6 +20,6 @@ export class TagEntity extends AbstractEntity implements ITagDb {
|
||||
@Length(1, 24, { message: 'Tag name must be $constraint1 to $constraint2 characters long.' })
|
||||
name: string;
|
||||
|
||||
@ManyToMany(() => WorkflowEntity, (workflow) => workflow.tags)
|
||||
@ManyToMany('WorkflowEntity', 'tags')
|
||||
workflows: WorkflowEntity[];
|
||||
}
|
||||
|
@ -12,9 +12,9 @@ import {
|
||||
} from 'typeorm';
|
||||
import { IsEmail, IsString, Length } from 'class-validator';
|
||||
import type { IUser } from 'n8n-workflow';
|
||||
import { Role } from './Role';
|
||||
import { SharedWorkflow } from './SharedWorkflow';
|
||||
import { SharedCredentials } from './SharedCredentials';
|
||||
import type { Role } from './Role';
|
||||
import type { SharedWorkflow } from './SharedWorkflow';
|
||||
import type { SharedCredentials } from './SharedCredentials';
|
||||
import { NoXss } from '../utils/customValidators';
|
||||
import { objectRetriever, lowerCaser } from '../utils/transformers';
|
||||
import { AbstractEntity, jsonColumnType } from './AbstractEntity';
|
||||
@ -74,16 +74,16 @@ export class User extends AbstractEntity implements IUser {
|
||||
})
|
||||
settings: IUserSettings | null;
|
||||
|
||||
@ManyToOne(() => Role, (role) => role.globalForUsers, {
|
||||
@ManyToOne('Role', 'globalForUsers', {
|
||||
cascade: true,
|
||||
nullable: false,
|
||||
})
|
||||
globalRole: Role;
|
||||
|
||||
@OneToMany(() => SharedWorkflow, (sharedWorkflow) => sharedWorkflow.user)
|
||||
@OneToMany('SharedWorkflow', 'user')
|
||||
sharedWorkflows: SharedWorkflow[];
|
||||
|
||||
@OneToMany(() => SharedCredentials, (sharedCredentials) => sharedCredentials.user)
|
||||
@OneToMany('SharedCredentials', 'user')
|
||||
sharedCredentials: SharedCredentials[];
|
||||
|
||||
@BeforeInsert()
|
||||
|
@ -21,11 +21,11 @@ import {
|
||||
} from 'typeorm';
|
||||
|
||||
import config from '@/config';
|
||||
import { TagEntity } from './TagEntity';
|
||||
import { SharedWorkflow } from './SharedWorkflow';
|
||||
import type { TagEntity } from './TagEntity';
|
||||
import type { SharedWorkflow } from './SharedWorkflow';
|
||||
import type { WorkflowStatistics } from './WorkflowStatistics';
|
||||
import { objectRetriever, sqlite } from '../utils/transformers';
|
||||
import { AbstractEntity, jsonColumnType } from './AbstractEntity';
|
||||
import { WorkflowStatistics } from './WorkflowStatistics';
|
||||
import type { IWorkflowDb } from '@/Interfaces';
|
||||
|
||||
@Entity()
|
||||
@ -63,7 +63,7 @@ export class WorkflowEntity extends AbstractEntity implements IWorkflowDb {
|
||||
})
|
||||
staticData?: IDataObject;
|
||||
|
||||
@ManyToMany(() => TagEntity, (tag) => tag.workflows)
|
||||
@ManyToMany('TagEntity', 'workflows')
|
||||
@JoinTable({
|
||||
name: 'workflows_tags', // table name for the junction table of this relation
|
||||
joinColumn: {
|
||||
@ -77,13 +77,10 @@ export class WorkflowEntity extends AbstractEntity implements IWorkflowDb {
|
||||
})
|
||||
tags?: TagEntity[];
|
||||
|
||||
@OneToMany(() => SharedWorkflow, (sharedWorkflow) => sharedWorkflow.workflow)
|
||||
@OneToMany('SharedWorkflow', 'workflow')
|
||||
shared: SharedWorkflow[];
|
||||
|
||||
@OneToMany(
|
||||
() => WorkflowStatistics,
|
||||
(workflowStatistics: WorkflowStatistics) => workflowStatistics.workflow,
|
||||
)
|
||||
@OneToMany('WorkflowStatistics', 'workflow')
|
||||
@JoinColumn({ referencedColumnName: 'workflow' })
|
||||
statistics: WorkflowStatistics[];
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { Column, Entity, RelationId, ManyToOne, PrimaryColumn } from 'typeorm';
|
||||
import { datetimeColumnType } from './AbstractEntity';
|
||||
import { WorkflowEntity } from './WorkflowEntity';
|
||||
import type { WorkflowEntity } from './WorkflowEntity';
|
||||
|
||||
export enum StatisticsNames {
|
||||
productionSuccess = 'production_success',
|
||||
@ -20,7 +20,7 @@ export class WorkflowStatistics {
|
||||
@PrimaryColumn({ length: 128 })
|
||||
name: StatisticsNames;
|
||||
|
||||
@ManyToOne(() => WorkflowEntity, (workflow) => workflow.shared, {
|
||||
@ManyToOne('WorkflowEntity', 'shared', {
|
||||
primary: true,
|
||||
onDelete: 'CASCADE',
|
||||
})
|
||||
|
@ -1,7 +1,8 @@
|
||||
import { INode, IRun, IWorkflowBase, LoggerProxy } from 'n8n-workflow';
|
||||
import { Db, InternalHooksManager } from '..';
|
||||
import { StatisticsNames } from '../databases/entities/WorkflowStatistics';
|
||||
import { getWorkflowOwner } from '../UserManagement/UserManagementHelper';
|
||||
import * as Db from '@/Db';
|
||||
import { InternalHooksManager } from '@/InternalHooksManager';
|
||||
import { StatisticsNames } from '@/databases/entities/WorkflowStatistics';
|
||||
import { getWorkflowOwner } from '@/UserManagement/UserManagementHelper';
|
||||
|
||||
export async function workflowExecutionCompleted(
|
||||
workflowData: IWorkflowBase,
|
||||
|
@ -1,5 +1,4 @@
|
||||
import express from 'express';
|
||||
import config from '@/config';
|
||||
import {
|
||||
IExecutionFlattedResponse,
|
||||
IExecutionResponse,
|
||||
@ -14,7 +13,7 @@ import { EEExecutionsService } from './executions.service.ee';
|
||||
export const EEExecutionsController = express.Router();
|
||||
|
||||
EEExecutionsController.use((req, res, next) => {
|
||||
if (!isSharingEnabled() || !config.getEnv('enterprise.workflowSharingEnabled')) {
|
||||
if (!isSharingEnabled()) {
|
||||
// skip ee router and use free one
|
||||
next('router');
|
||||
return;
|
||||
|
@ -8,7 +8,6 @@ import { FindOperator, In, IsNull, LessThanOrEqual, Not, Raw } from 'typeorm';
|
||||
import * as ActiveExecutions from '@/ActiveExecutions';
|
||||
import config from '@/config';
|
||||
import { User } from '@/databases/entities/User';
|
||||
import { DEFAULT_EXECUTIONS_GET_ALL_LIMIT } from '@/GenericHelpers';
|
||||
import {
|
||||
IExecutionFlattedResponse,
|
||||
IExecutionResponse,
|
||||
@ -22,7 +21,9 @@ import type { ExecutionRequest } from '@/requests';
|
||||
import * as ResponseHelper from '@/ResponseHelper';
|
||||
import { getSharedWorkflowIds } from '@/WorkflowHelpers';
|
||||
import { WorkflowRunner } from '@/WorkflowRunner';
|
||||
import { DatabaseType, Db, GenericHelpers } from '..';
|
||||
import type { DatabaseType } from '@/Interfaces';
|
||||
import * as Db from '@/Db';
|
||||
import * as GenericHelpers from '@/GenericHelpers';
|
||||
|
||||
interface IGetExecutionsQueryFilter {
|
||||
id?: FindOperator<string>;
|
||||
@ -174,7 +175,7 @@ export class ExecutionsService {
|
||||
|
||||
const limit = req.query.limit
|
||||
? parseInt(req.query.limit, 10)
|
||||
: DEFAULT_EXECUTIONS_GET_ALL_LIMIT;
|
||||
: GenericHelpers.DEFAULT_EXECUTIONS_GET_ALL_LIMIT;
|
||||
|
||||
const executingWorkflowIds: string[] = [];
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { getLicense } from '@/License';
|
||||
import { Db, ILicenseReadResponse } from '..';
|
||||
import type { ILicenseReadResponse } from '@/Interfaces';
|
||||
import * as Db from '@/Db';
|
||||
|
||||
export class LicenseService {
|
||||
static async getActiveTriggerCount(): Promise<number> {
|
||||
|
@ -4,12 +4,9 @@ import express from 'express';
|
||||
import { LoggerProxy } from 'n8n-workflow';
|
||||
|
||||
import { getLogger } from '@/Logger';
|
||||
import {
|
||||
ILicensePostResponse,
|
||||
ILicenseReadResponse,
|
||||
InternalHooksManager,
|
||||
ResponseHelper,
|
||||
} from '..';
|
||||
import * as ResponseHelper from '@/ResponseHelper';
|
||||
import { InternalHooksManager } from '@/InternalHooksManager';
|
||||
import type { ILicensePostResponse, ILicenseReadResponse } from '@/Interfaces';
|
||||
import { LicenseService } from './License.service';
|
||||
import { getLicense } from '@/License';
|
||||
import { AuthenticatedRequest, LicenseRequest } from '@/requests';
|
||||
|
@ -10,4 +10,15 @@ export class RoleService {
|
||||
static async trxGet(transaction: EntityManager, role: Partial<Role>) {
|
||||
return transaction.findOne(Role, role);
|
||||
}
|
||||
|
||||
static async getUserRoleForWorkflow(userId: string, workflowId: string) {
|
||||
const shared = await Db.collections.SharedWorkflow.findOne({
|
||||
where: {
|
||||
workflow: { id: workflowId },
|
||||
user: { id: userId },
|
||||
},
|
||||
relations: ['role'],
|
||||
});
|
||||
return shared?.role;
|
||||
}
|
||||
}
|
||||
|
@ -4,7 +4,9 @@ import { User } from '@db/entities/User';
|
||||
|
||||
export class UserService {
|
||||
static async get(user: Partial<User>): Promise<User | undefined> {
|
||||
return Db.collections.User.findOne(user);
|
||||
return Db.collections.User.findOne(user, {
|
||||
relations: ['globalRole'],
|
||||
});
|
||||
}
|
||||
|
||||
static async getByIds(transaction: EntityManager, ids: string[]) {
|
||||
|
@ -10,7 +10,7 @@ import { validateEntity } from '@/GenericHelpers';
|
||||
import type { WorkflowRequest } from '@/requests';
|
||||
import { isSharingEnabled, rightDiff } from '@/UserManagement/UserManagementHelper';
|
||||
import { EEWorkflowsService as EEWorkflows } from './workflows.services.ee';
|
||||
import { externalHooks } from '../Server';
|
||||
import { ExternalHooks } from '@/ExternalHooks';
|
||||
import { SharedWorkflow } from '@db/entities/SharedWorkflow';
|
||||
import { LoggerProxy } from 'n8n-workflow';
|
||||
import * as TagHelpers from '@/TagHelpers';
|
||||
@ -22,7 +22,7 @@ import * as GenericHelpers from '@/GenericHelpers';
|
||||
export const EEWorkflowController = express.Router();
|
||||
|
||||
EEWorkflowController.use((req, res, next) => {
|
||||
if (!isSharingEnabled() || !config.getEnv('enterprise.workflowSharingEnabled')) {
|
||||
if (!isSharingEnabled()) {
|
||||
// skip ee router and use free one
|
||||
next('router');
|
||||
return;
|
||||
@ -73,6 +73,12 @@ EEWorkflowController.put(
|
||||
await EEWorkflows.share(trx, workflow, newShareeIds);
|
||||
}
|
||||
});
|
||||
|
||||
void InternalHooksManager.getInstance().onWorkflowSharingUpdate(
|
||||
workflowId,
|
||||
req.user.id,
|
||||
shareWithIds,
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
@ -81,10 +87,12 @@ EEWorkflowController.get(
|
||||
ResponseHelper.send(async (req: WorkflowRequest.Get) => {
|
||||
const { id: workflowId } = req.params;
|
||||
|
||||
const workflow = await EEWorkflows.get(
|
||||
{ id: parseInt(workflowId, 10) },
|
||||
{ relations: ['shared', 'shared.user', 'shared.role'] },
|
||||
);
|
||||
const relations = ['shared', 'shared.user', 'shared.role'];
|
||||
if (!config.getEnv('workflowTagsDisabled')) {
|
||||
relations.push('tags');
|
||||
}
|
||||
|
||||
const workflow = await EEWorkflows.get({ id: parseInt(workflowId, 10) }, { relations });
|
||||
|
||||
if (!workflow) {
|
||||
throw new ResponseHelper.NotFoundError(`Workflow with ID "${workflowId}" does not exist`);
|
||||
@ -94,13 +102,13 @@ EEWorkflowController.get(
|
||||
|
||||
if (!userSharing && req.user.globalRole.name !== 'owner') {
|
||||
throw new ResponseHelper.UnauthorizedError(
|
||||
'It looks like you cannot access this workflow. Ask the owner to share it with you.',
|
||||
'You do not have permission to access this workflow. Ask the owner to share it with you',
|
||||
);
|
||||
}
|
||||
|
||||
EEWorkflows.addOwnerAndSharings(workflow);
|
||||
await EEWorkflows.addCredentialsToWorkflow(workflow, req.user);
|
||||
return workflow;
|
||||
return { ...workflow, id: workflow.id.toString() };
|
||||
}),
|
||||
);
|
||||
|
||||
@ -117,7 +125,7 @@ EEWorkflowController.post(
|
||||
|
||||
await validateEntity(newWorkflow);
|
||||
|
||||
await externalHooks.run('workflow.create', [newWorkflow]);
|
||||
await ExternalHooks().run('workflow.create', [newWorkflow]);
|
||||
|
||||
const { tags: tagIds } = req.body;
|
||||
|
||||
@ -178,7 +186,7 @@ EEWorkflowController.post(
|
||||
});
|
||||
}
|
||||
|
||||
await externalHooks.run('workflow.afterCreate', [savedWorkflow]);
|
||||
await ExternalHooks().run('workflow.afterCreate', [savedWorkflow]);
|
||||
void InternalHooksManager.getInstance().onWorkflowCreated(req.user.id, newWorkflow, false);
|
||||
|
||||
const { id, ...rest } = savedWorkflow;
|
||||
@ -206,7 +214,7 @@ EEWorkflowController.get(
|
||||
EEWorkflows.addOwnerAndSharings(workflow);
|
||||
await EEWorkflows.addCredentialsToWorkflow(workflow, req.user);
|
||||
workflow.nodes = [];
|
||||
return workflow;
|
||||
return { ...workflow, id: workflow.id.toString() };
|
||||
}),
|
||||
);
|
||||
}),
|
||||
|
@ -17,7 +17,7 @@ import { SharedWorkflow } from '@db/entities/SharedWorkflow';
|
||||
import { WorkflowEntity } from '@db/entities/WorkflowEntity';
|
||||
import { validateEntity } from '@/GenericHelpers';
|
||||
import { InternalHooksManager } from '@/InternalHooksManager';
|
||||
import { externalHooks } from '@/Server';
|
||||
import { ExternalHooks } from '@/ExternalHooks';
|
||||
import { getLogger } from '@/Logger';
|
||||
import type { WorkflowRequest } from '@/requests';
|
||||
import { isBelowOnboardingThreshold } from '@/WorkflowHelpers';
|
||||
@ -57,7 +57,7 @@ workflowsController.post(
|
||||
|
||||
await validateEntity(newWorkflow);
|
||||
|
||||
await externalHooks.run('workflow.create', [newWorkflow]);
|
||||
await ExternalHooks().run('workflow.create', [newWorkflow]);
|
||||
|
||||
const { tags: tagIds } = req.body;
|
||||
|
||||
@ -103,7 +103,7 @@ workflowsController.post(
|
||||
});
|
||||
}
|
||||
|
||||
await externalHooks.run('workflow.afterCreate', [savedWorkflow]);
|
||||
await ExternalHooks().run('workflow.afterCreate', [savedWorkflow]);
|
||||
void InternalHooksManager.getInstance().onWorkflowCreated(req.user.id, newWorkflow, false);
|
||||
|
||||
const { id, ...rest } = savedWorkflow;
|
||||
@ -213,7 +213,7 @@ workflowsController.get(
|
||||
});
|
||||
|
||||
if (!shared) {
|
||||
LoggerProxy.info('User attempted to access a workflow without permissions', {
|
||||
LoggerProxy.verbose('User attempted to access a workflow without permissions', {
|
||||
workflowId,
|
||||
userId: req.user.id,
|
||||
});
|
||||
@ -273,7 +273,7 @@ workflowsController.delete(
|
||||
ResponseHelper.send(async (req: WorkflowRequest.Delete) => {
|
||||
const { id: workflowId } = req.params;
|
||||
|
||||
await externalHooks.run('workflow.delete', [workflowId]);
|
||||
await ExternalHooks().run('workflow.delete', [workflowId]);
|
||||
|
||||
const shared = await Db.collections.SharedWorkflow.findOne({
|
||||
relations: ['workflow', 'role'],
|
||||
@ -286,7 +286,7 @@ workflowsController.delete(
|
||||
});
|
||||
|
||||
if (!shared) {
|
||||
LoggerProxy.info('User attempted to delete a workflow without permissions', {
|
||||
LoggerProxy.verbose('User attempted to delete a workflow without permissions', {
|
||||
workflowId,
|
||||
userId: req.user.id,
|
||||
});
|
||||
@ -303,7 +303,7 @@ workflowsController.delete(
|
||||
await Db.collections.Workflow.delete(workflowId);
|
||||
|
||||
void InternalHooksManager.getInstance().onWorkflowDeleted(req.user.id, workflowId, false);
|
||||
await externalHooks.run('workflow.afterDelete', [workflowId]);
|
||||
await ExternalHooks().run('workflow.afterDelete', [workflowId]);
|
||||
|
||||
return true;
|
||||
}),
|
||||
|
@ -15,6 +15,7 @@ import type {
|
||||
} from './workflows.types';
|
||||
import { EECredentialsService as EECredentials } from '@/credentials/credentials.service.ee';
|
||||
import { getSharedWorkflowIds } from '@/WorkflowHelpers';
|
||||
import { NodeOperationError } from 'n8n-workflow';
|
||||
|
||||
export class EEWorkflowsService extends WorkflowsService {
|
||||
static async getWorkflowIdsForUser(user: User) {
|
||||
@ -189,6 +190,9 @@ export class EEWorkflowsService extends WorkflowsService {
|
||||
allCredentials,
|
||||
);
|
||||
} catch (error) {
|
||||
if (error instanceof NodeOperationError) {
|
||||
throw new ResponseHelper.BadRequestError(error.message);
|
||||
}
|
||||
throw new ResponseHelper.BadRequestError(
|
||||
'Invalid workflow credentials - make sure you have access to all credentials and try again.',
|
||||
);
|
||||
|
@ -13,7 +13,7 @@ import { SharedWorkflow } from '@db/entities/SharedWorkflow';
|
||||
import { User } from '@db/entities/User';
|
||||
import { WorkflowEntity } from '@db/entities/WorkflowEntity';
|
||||
import { validateEntity } from '@/GenericHelpers';
|
||||
import { externalHooks } from '@/Server';
|
||||
import { ExternalHooks } from '@/ExternalHooks';
|
||||
import * as TagHelpers from '@/TagHelpers';
|
||||
import { WorkflowRequest } from '@/requests';
|
||||
import { IWorkflowDb, IWorkflowExecutionDataProcess } from '@/Interfaces';
|
||||
@ -22,7 +22,7 @@ import { WorkflowRunner } from '@/WorkflowRunner';
|
||||
import * as WorkflowExecuteAdditionalData from '@/WorkflowExecuteAdditionalData';
|
||||
import * as TestWebhooks from '@/TestWebhooks';
|
||||
import { getSharedWorkflowIds } from '@/WorkflowHelpers';
|
||||
import { whereClause } from '@/UserManagement/UserManagementHelper';
|
||||
import { isSharingEnabled, whereClause } from '@/UserManagement/UserManagementHelper';
|
||||
|
||||
export interface IGetWorkflowsQueryFilter {
|
||||
id?: number | string;
|
||||
@ -158,20 +158,26 @@ export class WorkflowsService {
|
||||
return [];
|
||||
}
|
||||
|
||||
const fields: Array<keyof WorkflowEntity> = ['id', 'name', 'active', 'createdAt', 'updatedAt'];
|
||||
const fields: Array<keyof WorkflowEntity> = [
|
||||
'id',
|
||||
'name',
|
||||
'active',
|
||||
'createdAt',
|
||||
'updatedAt',
|
||||
'nodes',
|
||||
];
|
||||
const relations: string[] = [];
|
||||
|
||||
if (!config.getEnv('workflowTagsDisabled')) {
|
||||
relations.push('tags');
|
||||
}
|
||||
|
||||
const isSharingEnabled = config.getEnv('enterprise.features.sharing');
|
||||
if (isSharingEnabled) {
|
||||
if (isSharingEnabled()) {
|
||||
relations.push('shared', 'shared.user', 'shared.role');
|
||||
}
|
||||
|
||||
const query: FindManyOptions<WorkflowEntity> = {
|
||||
select: isSharingEnabled ? [...fields, 'nodes', 'versionId'] : fields,
|
||||
select: isSharingEnabled() ? [...fields, 'versionId'] : fields,
|
||||
relations,
|
||||
where: {
|
||||
id: In(sharedWorkflowIds),
|
||||
@ -210,7 +216,7 @@ export class WorkflowsService {
|
||||
});
|
||||
|
||||
if (!shared) {
|
||||
LoggerProxy.info('User attempted to update a workflow without permissions', {
|
||||
LoggerProxy.verbose('User attempted to update a workflow without permissions', {
|
||||
workflowId,
|
||||
userId: user.id,
|
||||
});
|
||||
@ -246,7 +252,7 @@ export class WorkflowsService {
|
||||
|
||||
WorkflowHelpers.addNodeIds(workflow);
|
||||
|
||||
await externalHooks.run('workflow.update', [workflow]);
|
||||
await ExternalHooks().run('workflow.update', [workflow]);
|
||||
|
||||
if (shared.workflow.active) {
|
||||
// When workflow gets saved always remove it as the triggers could have been
|
||||
@ -332,13 +338,13 @@ export class WorkflowsService {
|
||||
});
|
||||
}
|
||||
|
||||
await externalHooks.run('workflow.afterUpdate', [updatedWorkflow]);
|
||||
await ExternalHooks().run('workflow.afterUpdate', [updatedWorkflow]);
|
||||
void InternalHooksManager.getInstance().onWorkflowSaved(user.id, updatedWorkflow, false);
|
||||
|
||||
if (updatedWorkflow.active) {
|
||||
// When the workflow is supposed to be active add it again
|
||||
try {
|
||||
await externalHooks.run('workflow.activate', [updatedWorkflow]);
|
||||
await ExternalHooks().run('workflow.activate', [updatedWorkflow]);
|
||||
await ActiveWorkflowRunner.getInstance().add(
|
||||
workflowId,
|
||||
shared.workflow.active ? 'update' : 'activate',
|
||||
@ -351,7 +357,7 @@ export class WorkflowsService {
|
||||
updatedWorkflow.active = false;
|
||||
|
||||
// Now return the original error for UI to display
|
||||
throw error;
|
||||
throw new ResponseHelper.BadRequestError((error as Error).message);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -249,6 +249,7 @@ function toTableName(sourceName: CollectionName | MappingName) {
|
||||
Settings: 'settings',
|
||||
InstalledPackages: 'installed_packages',
|
||||
InstalledNodes: 'installed_nodes',
|
||||
WorkflowStatistics: 'workflow_statistics',
|
||||
}[sourceName];
|
||||
}
|
||||
|
||||
|
@ -161,13 +161,13 @@ test('DELETE /users/:id should delete the user', async () => {
|
||||
|
||||
const sharedWorkflow = await Db.collections.SharedWorkflow.findOne({
|
||||
relations: ['user'],
|
||||
where: { user: userToDelete },
|
||||
where: { user: userToDelete, role: workflowOwnerRole },
|
||||
});
|
||||
expect(sharedWorkflow).toBeUndefined(); // deleted
|
||||
|
||||
const sharedCredential = await Db.collections.SharedCredentials.findOne({
|
||||
relations: ['user'],
|
||||
where: { user: userToDelete },
|
||||
where: { user: userToDelete, role: credentialOwnerRole },
|
||||
});
|
||||
expect(sharedCredential).toBeUndefined(); // deleted
|
||||
|
||||
|
@ -47,8 +47,6 @@ beforeAll(async () => {
|
||||
|
||||
isSharingEnabled = jest.spyOn(UserManagementHelpers, 'isSharingEnabled').mockReturnValue(true);
|
||||
|
||||
config.set('enterprise.workflowSharingEnabled', true); // @TODO: Remove once temp flag is removed
|
||||
|
||||
await utils.initNodeTypes();
|
||||
workflowRunner = await utils.initActiveWorkflowRunner();
|
||||
|
||||
@ -666,7 +664,7 @@ describe('PATCH /workflows/:id - validate credential permissions to user', () =>
|
||||
expect(response.statusCode).toBe(400);
|
||||
});
|
||||
|
||||
it('Should succeed but prevent modifying nodes that are read-only for the requester', async () => {
|
||||
it('Should succeed but prevent modifying node attributes other than position, name and disabled', async () => {
|
||||
const member1 = await testDb.createUser({ globalRole: globalMemberRole });
|
||||
const member2 = await testDb.createUser({ globalRole: globalMemberRole });
|
||||
|
||||
@ -676,7 +674,9 @@ describe('PATCH /workflows/:id - validate credential permissions to user', () =>
|
||||
{
|
||||
id: 'uuid-1234',
|
||||
name: 'Start',
|
||||
parameters: {},
|
||||
parameters: {
|
||||
firstParam: 123,
|
||||
},
|
||||
position: [-20, 260],
|
||||
type: 'n8n-nodes-base.start',
|
||||
typeVersion: 1,
|
||||
@ -693,8 +693,10 @@ describe('PATCH /workflows/:id - validate credential permissions to user', () =>
|
||||
{
|
||||
id: 'uuid-1234',
|
||||
name: 'End',
|
||||
parameters: {},
|
||||
position: [-20, 260],
|
||||
parameters: {
|
||||
firstParam: 456,
|
||||
},
|
||||
position: [-20, 555],
|
||||
type: 'n8n-nodes-base.no-op',
|
||||
typeVersion: 1,
|
||||
credentials: {
|
||||
@ -703,6 +705,27 @@ describe('PATCH /workflows/:id - validate credential permissions to user', () =>
|
||||
name: 'fake credential',
|
||||
},
|
||||
},
|
||||
disabled: true,
|
||||
},
|
||||
];
|
||||
|
||||
const expectedNodes: INode[] = [
|
||||
{
|
||||
id: 'uuid-1234',
|
||||
name: 'End',
|
||||
parameters: {
|
||||
firstParam: 123,
|
||||
},
|
||||
position: [-20, 555],
|
||||
type: 'n8n-nodes-base.start',
|
||||
typeVersion: 1,
|
||||
credentials: {
|
||||
default: {
|
||||
id: savedCredential.id.toString(),
|
||||
name: savedCredential.name,
|
||||
},
|
||||
},
|
||||
disabled: true,
|
||||
},
|
||||
];
|
||||
|
||||
@ -726,7 +749,7 @@ describe('PATCH /workflows/:id - validate credential permissions to user', () =>
|
||||
});
|
||||
|
||||
expect(response.statusCode).toBe(200);
|
||||
expect(response.body.data.nodes).toMatchObject(originalNodes);
|
||||
expect(response.body.data.nodes).toMatchObject(expectedNodes);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
import config from '@/config';
|
||||
import { InternalHooksManager } from '../../src';
|
||||
import { nodeFetchedData, workflowExecutionCompleted } from '../../src/events/WorkflowStatistics';
|
||||
import { InternalHooksManager } from '@/InternalHooksManager';
|
||||
import { nodeFetchedData, workflowExecutionCompleted } from '@/events/WorkflowStatistics';
|
||||
import { LoggerProxy, WorkflowExecuteMode } from 'n8n-workflow';
|
||||
import { getLogger } from '@/Logger';
|
||||
|
||||
@ -10,14 +10,14 @@ const mockedFirstProductionWorkflowSuccess = jest.fn((...args) => {});
|
||||
const mockedFirstWorkflowDataLoad = jest.fn((...args) => {});
|
||||
|
||||
jest.spyOn(InternalHooksManager, 'getInstance').mockImplementation((...args) => {
|
||||
const actual = jest.requireActual('../../src/InternalHooks');
|
||||
const actual = jest.requireActual('@/InternalHooks');
|
||||
return {
|
||||
...actual,
|
||||
onFirstProductionWorkflowSuccess: mockedFirstProductionWorkflowSuccess,
|
||||
onFirstWorkflowDataLoad: mockedFirstWorkflowDataLoad,
|
||||
};
|
||||
});
|
||||
jest.mock('../../src/Db', () => {
|
||||
jest.mock('@/Db', () => {
|
||||
return {
|
||||
collections: {
|
||||
Workflow: {
|
||||
@ -36,7 +36,7 @@ jest.mock('../../src/Db', () => {
|
||||
},
|
||||
};
|
||||
});
|
||||
jest.mock('../../src/UserManagement/UserManagementHelper', () => {
|
||||
jest.mock('@/UserManagement/UserManagementHelper', () => {
|
||||
return {
|
||||
getWorkflowOwner: jest.fn((workflowId) => {
|
||||
return { id: FAKE_USER_ID };
|
||||
|
@ -19,7 +19,7 @@ describe('License', () => {
|
||||
config.set('license.autoRenewOffset', MOCK_RENEW_OFFSET);
|
||||
});
|
||||
|
||||
let license;
|
||||
let license: License;
|
||||
|
||||
beforeEach(async () => {
|
||||
license = new License();
|
||||
@ -102,6 +102,6 @@ describe('License', () => {
|
||||
jest.fn(license.getMainPlan).mockReset();
|
||||
|
||||
const mainPlan = license.getMainPlan();
|
||||
expect(mainPlan.id).toBe(MOCK_MAIN_PLAN_ID);
|
||||
expect(mainPlan?.id).toBe(MOCK_MAIN_PLAN_ID);
|
||||
});
|
||||
});
|
||||
|
@ -1,16 +1,16 @@
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { INodeTypeData, INodeTypes, Workflow } from 'n8n-workflow';
|
||||
|
||||
import { Db } from '../../src';
|
||||
import * as Db from '@/Db';
|
||||
import * as testDb from '../integration/shared/testDb';
|
||||
import { NodeTypes as MockNodeTypes } from './Helpers';
|
||||
import { PermissionChecker } from '../../src/UserManagement/PermissionChecker';
|
||||
import { PermissionChecker } from '@/UserManagement/PermissionChecker';
|
||||
import {
|
||||
randomCredentialPayload as randomCred,
|
||||
randomPositiveDigit,
|
||||
} from '../integration/shared/random';
|
||||
|
||||
import type { Role } from '../../src/databases/entities/Role';
|
||||
import type { Role } from '@/databases/entities/Role';
|
||||
import type { SaveCredentialFunction } from '../integration/shared/types';
|
||||
|
||||
let testDbName = '';
|
||||
|
@ -1,13 +1,12 @@
|
||||
const { sharedOptions } = require('@n8n_io/eslint-config/shared');
|
||||
|
||||
/**
|
||||
* @type {import('@types/eslint').ESLint.ConfigData}
|
||||
*/
|
||||
module.exports = {
|
||||
extends: ['@n8n_io/eslint-config/node'],
|
||||
|
||||
parserOptions: {
|
||||
project: ['./tsconfig.json'],
|
||||
tsconfigRootDir: __dirname,
|
||||
},
|
||||
...sharedOptions(__dirname),
|
||||
|
||||
ignorePatterns: ['bin/*.js'],
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "n8n-core",
|
||||
"version": "0.148.1",
|
||||
"version": "0.149.0",
|
||||
"description": "Core functionality of n8n",
|
||||
"license": "SEE LICENSE IN LICENSE.md",
|
||||
"homepage": "https://n8n.io",
|
||||
@ -53,7 +53,7 @@
|
||||
"form-data": "^4.0.0",
|
||||
"lodash.get": "^4.4.2",
|
||||
"mime-types": "^2.1.27",
|
||||
"n8n-workflow": "~0.130.0",
|
||||
"n8n-workflow": "~0.131.0",
|
||||
"oauth-1.0a": "^2.2.6",
|
||||
"p-cancelable": "^2.0.0",
|
||||
"pretty-bytes": "^5.6.0",
|
||||
|
@ -836,7 +836,7 @@ export async function getBinaryDataBuffer(
|
||||
propertyName: string,
|
||||
inputIndex: number,
|
||||
): Promise<Buffer> {
|
||||
const binaryData = inputData.main![inputIndex]![itemIndex]!.binary![propertyName]!;
|
||||
const binaryData = inputData.main[inputIndex]![itemIndex]!.binary![propertyName]!;
|
||||
return BinaryDataManager.getInstance().retrieveBinaryData(binaryData);
|
||||
}
|
||||
|
||||
|
@ -876,8 +876,8 @@ export class WorkflowExecute {
|
||||
// The most nodes just have one but merge node for example has two and data
|
||||
// of both inputs has to be available to be able to process the node.
|
||||
if (
|
||||
executionData.data.main!.length < connectionIndex ||
|
||||
executionData.data.main![connectionIndex] === null
|
||||
executionData.data.main.length < connectionIndex ||
|
||||
executionData.data.main[connectionIndex] === null
|
||||
) {
|
||||
// Does not have the data of the connections so add back to stack
|
||||
this.runExecutionData.executionData!.nodeExecutionStack.push(executionData);
|
||||
|
@ -1,14 +1,12 @@
|
||||
const { sharedOptions } = require('@n8n_io/eslint-config/shared');
|
||||
|
||||
/**
|
||||
* @type {import('@types/eslint').ESLint.ConfigData}
|
||||
*/
|
||||
module.exports = {
|
||||
extends: ['@n8n_io/eslint-config/frontend'],
|
||||
|
||||
parserOptions: {
|
||||
project: ['./tsconfig.json'],
|
||||
tsconfigRootDir: __dirname,
|
||||
extraFileExtensions: ['.vue'],
|
||||
},
|
||||
...sharedOptions(__dirname, 'frontend'),
|
||||
|
||||
rules: {
|
||||
// TODO: Remove these
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "n8n-design-system",
|
||||
"version": "0.48.0",
|
||||
"version": "0.49.0",
|
||||
"license": "SEE LICENSE IN LICENSE.md",
|
||||
"homepage": "https://n8n.io",
|
||||
"author": {
|
||||
|
@ -1,14 +1,12 @@
|
||||
const { sharedOptions } = require('@n8n_io/eslint-config/shared');
|
||||
|
||||
/**
|
||||
* @type {import('@types/eslint').ESLint.ConfigData}
|
||||
*/
|
||||
module.exports = {
|
||||
extends: ['@n8n_io/eslint-config/frontend'],
|
||||
|
||||
parserOptions: {
|
||||
project: ['./tsconfig.json'],
|
||||
tsconfigRootDir: __dirname,
|
||||
extraFileExtensions: ['.vue'],
|
||||
},
|
||||
...sharedOptions(__dirname, 'frontend'),
|
||||
|
||||
ignorePatterns: ['*.d.cts'],
|
||||
|
||||
@ -19,6 +17,7 @@ module.exports = {
|
||||
'import/no-default-export': 'off',
|
||||
'import/no-extraneous-dependencies': 'off',
|
||||
'import/order': 'off',
|
||||
'import/no-cycle': 'warn',
|
||||
indent: 'off',
|
||||
'prettier/prettier': 'off',
|
||||
'@typescript-eslint/ban-types': 'off',
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "n8n-editor-ui",
|
||||
"version": "0.174.1",
|
||||
"version": "0.175.0",
|
||||
"description": "Workflow Editor UI for n8n",
|
||||
"license": "SEE LICENSE IN LICENSE.md",
|
||||
"homepage": "https://n8n.io",
|
||||
@ -57,8 +57,8 @@
|
||||
"lodash.set": "^4.3.2",
|
||||
"luxon": "^2.3.0",
|
||||
"monaco-editor": "^0.33.0",
|
||||
"n8n-design-system": "~0.48.0",
|
||||
"n8n-workflow": "~0.130.0",
|
||||
"n8n-design-system": "~0.49.0",
|
||||
"n8n-workflow": "~0.131.0",
|
||||
"normalize-wheel": "^1.0.1",
|
||||
"pinia": "^2.0.22",
|
||||
"prismjs": "^1.17.1",
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 262 KiB After Width: | Height: | Size: 155 KiB |
Binary file not shown.
Before Width: | Height: | Size: 675 KiB After Width: | Height: | Size: 363 KiB |
@ -41,7 +41,7 @@ import {
|
||||
import { FAKE_DOOR_FEATURES } from './constants';
|
||||
import { BulkCommand, Undoable } from '@/models/history';
|
||||
|
||||
export * from 'n8n-design-system/src/types';
|
||||
export * from 'n8n-design-system/types';
|
||||
|
||||
declare module 'jsplumb' {
|
||||
interface PaintStyle {
|
||||
@ -179,6 +179,7 @@ export interface IUpdateInformation {
|
||||
export interface INodeUpdatePropertiesInformation {
|
||||
name: string; // Node-Name
|
||||
properties: {
|
||||
position: XYPosition;
|
||||
[key: string]: IDataObject | XYPosition;
|
||||
};
|
||||
}
|
||||
@ -234,8 +235,7 @@ export interface IRestApi {
|
||||
deleteExecutions(sendData: IExecutionDeleteFilter): Promise<void>;
|
||||
retryExecution(id: string, loadWorkflow?: boolean): Promise<boolean>;
|
||||
getTimezones(): Promise<IDataObject>;
|
||||
getBinaryBufferString(dataPath: string): Promise<string>;
|
||||
getBinaryUrl(dataPath: string): string;
|
||||
getBinaryUrl(dataPath: string, mode: 'view' | 'download'): string;
|
||||
}
|
||||
|
||||
export interface INodeTranslationHeaders {
|
||||
@ -804,7 +804,7 @@ export interface IN8nUISettings {
|
||||
};
|
||||
enterprise: Record<string, boolean>;
|
||||
deployment?: {
|
||||
type: string;
|
||||
type: string | 'default' | 'n8n-internal' | 'cloud' | 'desktop_mac' | 'desktop_win';
|
||||
};
|
||||
hideUsagePage: boolean;
|
||||
license: {
|
||||
@ -1079,10 +1079,6 @@ export interface IModalState {
|
||||
httpNodeParameters?: string;
|
||||
}
|
||||
|
||||
export interface NestedRecord<T> {
|
||||
[key: string]: T | NestedRecord<T>;
|
||||
}
|
||||
|
||||
export type IRunDataDisplayMode = 'table' | 'json' | 'binary' | 'schema';
|
||||
export type NodePanelType = 'input' | 'output';
|
||||
|
||||
@ -1155,7 +1151,6 @@ export interface UIState {
|
||||
currentView: string;
|
||||
mainPanelPosition: number;
|
||||
fakeDoorFeatures: IFakeDoor[];
|
||||
dynamicTranslations: NestedRecord<string>;
|
||||
draggable: {
|
||||
isDragging: boolean;
|
||||
type: string;
|
||||
|
@ -111,19 +111,5 @@ export default mixins(nodeHelpers, restApi).extend({
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.binary-data {
|
||||
background-color: var(--color-foreground-xlight);
|
||||
|
||||
&.image {
|
||||
max-height: calc(100% - 1em);
|
||||
max-width: calc(100% - 1em);
|
||||
}
|
||||
|
||||
&.other {
|
||||
height: calc(100% - 1em);
|
||||
width: calc(100% - 1em);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -56,7 +56,7 @@ export default mixins(restApi).extend({
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
const binaryUrl = this.restApi().getBinaryUrl(id);
|
||||
const binaryUrl = this.restApi().getBinaryUrl(id, 'view');
|
||||
if (isJSONData) {
|
||||
this.jsonData = await (await fetch(binaryUrl)).json();
|
||||
} else {
|
||||
|
@ -3,7 +3,14 @@
|
||||
<banner
|
||||
v-show="showValidationWarning"
|
||||
theme="danger"
|
||||
:message="$locale.baseText('credentialEdit.credentialConfig.pleaseCheckTheErrorsBelow')"
|
||||
:message="
|
||||
$locale.baseText(
|
||||
`credentialEdit.credentialConfig.pleaseCheckTheErrorsBelow${
|
||||
credentialPermissions.isOwner ? '' : '.sharee'
|
||||
}`,
|
||||
{ interpolate: { owner: credentialOwnerName } },
|
||||
)
|
||||
"
|
||||
/>
|
||||
|
||||
<banner
|
||||
@ -12,7 +19,7 @@
|
||||
:message="
|
||||
$locale.baseText(
|
||||
`credentialEdit.credentialConfig.couldntConnectWithTheseSettings${
|
||||
!credentialPermissions.isOwner ? '.sharee' : ''
|
||||
credentialPermissions.isOwner ? '' : '.sharee'
|
||||
}`,
|
||||
{ interpolate: { owner: credentialOwnerName } },
|
||||
)
|
||||
@ -117,7 +124,7 @@ import OauthButton from './OauthButton.vue';
|
||||
import { restApi } from '@/mixins/restApi';
|
||||
import { addCredentialTranslation } from '@/plugins/i18n';
|
||||
import mixins from 'vue-typed-mixins';
|
||||
import { BUILTIN_CREDENTIALS_DOCS_URL, EnterpriseEditionFeature } from '@/constants';
|
||||
import { BUILTIN_CREDENTIALS_DOCS_URL, DOCS_DOMAIN, EnterpriseEditionFeature } from '@/constants';
|
||||
import { IPermissions } from '@/permissions';
|
||||
import { mapStores } from 'pinia';
|
||||
import { useUIStore } from '@/stores/ui';
|
||||
@ -222,20 +229,29 @@ export default mixins(restApi).extend({
|
||||
const activeNode = this.ndvStore.activeNode;
|
||||
const isCommunityNode = activeNode ? isCommunityPackageName(activeNode.type) : false;
|
||||
|
||||
if (!type || !type.documentationUrl) {
|
||||
const documentationUrl = type && type.documentationUrl;
|
||||
|
||||
if (!documentationUrl) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (
|
||||
type.documentationUrl.startsWith('https://') ||
|
||||
type.documentationUrl.startsWith('http://')
|
||||
) {
|
||||
return type.documentationUrl;
|
||||
let url: URL;
|
||||
if (documentationUrl.startsWith('https://') || documentationUrl.startsWith('http://')) {
|
||||
url = new URL(documentationUrl);
|
||||
if (url.hostname !== DOCS_DOMAIN) return documentationUrl;
|
||||
} else {
|
||||
// Don't show documentation link for community nodes if the URL is not an absolute path
|
||||
if (isCommunityNode) return '';
|
||||
else url = new URL(`${BUILTIN_CREDENTIALS_DOCS_URL}${documentationUrl}/`);
|
||||
}
|
||||
|
||||
return isCommunityNode
|
||||
? '' // Don't show documentation link for community nodes if the URL is not an absolute path
|
||||
: `${BUILTIN_CREDENTIALS_DOCS_URL}${type.documentationUrl}/?utm_source=n8n_app&utm_medium=left_nav_menu&utm_campaign=create_new_credentials_modal`;
|
||||
if (url.hostname === DOCS_DOMAIN) {
|
||||
url.searchParams.set('utm_source', 'n8n_app');
|
||||
url.searchParams.set('utm_medium', 'left_nav_menu');
|
||||
url.searchParams.set('utm_campaign', 'create_new_credentials_modal');
|
||||
}
|
||||
|
||||
return url.href;
|
||||
},
|
||||
isGoogleOAuthType(): boolean {
|
||||
return (
|
||||
|
@ -77,11 +77,7 @@
|
||||
@scrollToTop="scrollToTop"
|
||||
/>
|
||||
</div>
|
||||
<enterprise-edition
|
||||
v-else-if="activeTab === 'sharing' && credentialType"
|
||||
:class="$style.mainContent"
|
||||
:features="[EnterpriseEditionFeature.Sharing]"
|
||||
>
|
||||
<div v-else-if="activeTab === 'sharing' && credentialType" :class="$style.mainContent">
|
||||
<CredentialSharing
|
||||
:credential="currentCredential"
|
||||
:credentialData="credentialData"
|
||||
@ -90,7 +86,7 @@
|
||||
:modalBus="modalBus"
|
||||
@change="onChangeSharedWith"
|
||||
/>
|
||||
</enterprise-edition>
|
||||
</div>
|
||||
<div v-else-if="activeTab === 'details' && credentialType" :class="$style.mainContent">
|
||||
<CredentialInfo
|
||||
:nodeAccess="nodeAccess"
|
||||
@ -111,7 +107,7 @@
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
|
||||
import { ICredentialsResponse, IFakeDoor, IUser } from '@/Interface';
|
||||
import type { ICredentialsResponse, IUser } from '@/Interface';
|
||||
|
||||
import {
|
||||
CredentialInformation,
|
||||
@ -391,9 +387,6 @@ export default mixins(showMessage, nodeHelpers).extend({
|
||||
}
|
||||
return true;
|
||||
},
|
||||
credentialsFakeDoorFeatures(): IFakeDoor[] {
|
||||
return this.uiStore.getFakeDoorByLocation('credentialsModal');
|
||||
},
|
||||
credentialPermissions(): IPermissions {
|
||||
if (this.loading) {
|
||||
return {};
|
||||
@ -405,7 +398,7 @@ export default mixins(showMessage, nodeHelpers).extend({
|
||||
);
|
||||
},
|
||||
sidebarItems(): IMenuItem[] {
|
||||
const items: IMenuItem[] = [
|
||||
return [
|
||||
{
|
||||
id: 'connection',
|
||||
label: this.$locale.baseText('credentialEdit.credentialEdit.connection'),
|
||||
@ -415,26 +408,13 @@ export default mixins(showMessage, nodeHelpers).extend({
|
||||
id: 'sharing',
|
||||
label: this.$locale.baseText('credentialEdit.credentialEdit.sharing'),
|
||||
position: 'top',
|
||||
available: this.credentialType !== null && this.isSharingAvailable,
|
||||
},
|
||||
{
|
||||
id: 'details',
|
||||
label: this.$locale.baseText('credentialEdit.credentialEdit.details'),
|
||||
position: 'top',
|
||||
},
|
||||
];
|
||||
|
||||
if (this.credentialType !== null && !this.isSharingAvailable) {
|
||||
for (const item of this.credentialsFakeDoorFeatures) {
|
||||
items.push({
|
||||
id: `coming-soon/${item.id}`,
|
||||
label: this.$locale.baseText(item.featureName as BaseTextKey),
|
||||
position: 'top',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
items.push({
|
||||
id: 'details',
|
||||
label: this.$locale.baseText('credentialEdit.credentialEdit.details'),
|
||||
position: 'top',
|
||||
});
|
||||
return items;
|
||||
},
|
||||
isSharingAvailable(): boolean {
|
||||
return this.settingsStore.isEnterpriseFeatureEnabled(EnterpriseEditionFeature.Sharing);
|
||||
|
@ -1,7 +1,22 @@
|
||||
<template>
|
||||
<div :class="$style.container">
|
||||
<div v-if="isDefaultUser">
|
||||
<div v-if="!isSharingEnabled">
|
||||
<n8n-action-box
|
||||
:heading="
|
||||
$locale.baseText(contextBasedTranslationKeys.credentials.sharing.unavailable.title)
|
||||
"
|
||||
:description="
|
||||
$locale.baseText(contextBasedTranslationKeys.credentials.sharing.unavailable.description)
|
||||
"
|
||||
:buttonText="
|
||||
$locale.baseText(contextBasedTranslationKeys.credentials.sharing.unavailable.button)
|
||||
"
|
||||
@click="goToUpgrade"
|
||||
/>
|
||||
</div>
|
||||
<div v-else-if="isDefaultUser">
|
||||
<n8n-action-box
|
||||
:heading="$locale.baseText('credentialEdit.credentialSharing.isDefaultUser.title')"
|
||||
:description="
|
||||
$locale.baseText('credentialEdit.credentialSharing.isDefaultUser.description')
|
||||
"
|
||||
@ -23,8 +38,13 @@
|
||||
</template>
|
||||
</n8n-info-tip>
|
||||
<n8n-info-tip
|
||||
v-if="
|
||||
!credentialPermissions.isOwner &&
|
||||
!credentialPermissions.isSharee &&
|
||||
credentialPermissions.isInstanceOwner
|
||||
"
|
||||
class="mb-s"
|
||||
:bold="false"
|
||||
v-if="!credentialPermissions.isOwner && credentialPermissions.isInstanceOwner"
|
||||
>
|
||||
{{ $locale.baseText('credentialEdit.credentialSharing.info.instanceOwner') }}
|
||||
</n8n-info-tip>
|
||||
@ -53,13 +73,16 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { IUser } from '@/Interface';
|
||||
import { IUser, UIState } from '@/Interface';
|
||||
import mixins from 'vue-typed-mixins';
|
||||
import { showMessage } from '@/mixins/showMessage';
|
||||
import { mapStores } from 'pinia';
|
||||
import { useUsersStore } from '@/stores/users';
|
||||
import { useSettingsStore } from '@/stores/settings';
|
||||
import { useUIStore } from '@/stores/ui';
|
||||
import { useCredentialsStore } from '@/stores/credentials';
|
||||
import { VIEWS } from '@/constants';
|
||||
import { useUsageStore } from '@/stores/usage';
|
||||
import { EnterpriseEditionFeature, VIEWS } from '@/constants';
|
||||
|
||||
export default mixins(showMessage).extend({
|
||||
name: 'CredentialSharing',
|
||||
@ -72,10 +95,16 @@ export default mixins(showMessage).extend({
|
||||
'modalBus',
|
||||
],
|
||||
computed: {
|
||||
...mapStores(useCredentialsStore, useUsersStore),
|
||||
...mapStores(useCredentialsStore, useUsersStore, useUsageStore, useUIStore, useSettingsStore),
|
||||
isDefaultUser(): boolean {
|
||||
return this.usersStore.isDefaultUser;
|
||||
},
|
||||
contextBasedTranslationKeys(): UIState['contextBasedTranslationKeys'] {
|
||||
return this.uiStore.contextBasedTranslationKeys;
|
||||
},
|
||||
isSharingEnabled(): boolean {
|
||||
return this.settingsStore.isEnterpriseFeatureEnabled(EnterpriseEditionFeature.Sharing);
|
||||
},
|
||||
usersList(): IUser[] {
|
||||
return this.usersStore.allUsers.filter((user: IUser) => {
|
||||
const isCurrentUser = user.id === this.usersStore.currentUser?.id;
|
||||
@ -138,6 +167,14 @@ export default mixins(showMessage).extend({
|
||||
this.$router.push({ name: VIEWS.USERS_SETTINGS });
|
||||
this.modalBus.$emit('close');
|
||||
},
|
||||
goToUpgrade() {
|
||||
let linkUrl = this.$locale.baseText(this.contextBasedTranslationKeys.upgradeLinkUrl);
|
||||
if (linkUrl.includes('subscription')) {
|
||||
linkUrl = this.usageStore.viewPlansUrl;
|
||||
}
|
||||
|
||||
window.open(linkUrl, '_blank');
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.loadUsers();
|
||||
|
@ -273,7 +273,7 @@ import WorkflowActivator from '@/components/WorkflowActivator.vue';
|
||||
import Modal from '@/components/Modal.vue';
|
||||
|
||||
import { externalHooks } from '@/mixins/externalHooks';
|
||||
import { WAIT_TIME_UNLIMITED, EXECUTIONS_MODAL_KEY, VIEWS } from '@/constants';
|
||||
import { WAIT_TIME_UNLIMITED, EXECUTIONS_MODAL_KEY, VIEWS, PLACEHOLDER_EMPTY_WORKFLOW_ID } from '@/constants';
|
||||
|
||||
import { restApi } from '@/mixins/restApi';
|
||||
import { genericHelpers } from '@/mixins/genericHelpers';
|
||||
@ -434,7 +434,13 @@ export default mixins(externalHooks, genericHelpers, restApi, showMessage).exten
|
||||
this.modalBus.$emit('close');
|
||||
},
|
||||
convertToDisplayDate,
|
||||
displayExecution(execution: IExecutionShortResponse, e: PointerEvent) {
|
||||
displayExecution(execution: IExecutionsSummary, e: PointerEvent) {
|
||||
if (!this.workflowsStore.workflowId || this.workflowsStore.workflowId === PLACEHOLDER_EMPTY_WORKFLOW_ID || execution.workflowId !== this.workflowsStore.workflowId) {
|
||||
const workflowExecutions: IExecutionsSummary[] = this.combinedExecutions.filter(ex => ex.workflowId === execution.workflowId);
|
||||
this.workflowsStore.currentWorkflowExecutions = workflowExecutions;
|
||||
this.workflowsStore.activeWorkflowExecution = execution;
|
||||
}
|
||||
|
||||
if (e.metaKey || e.ctrlKey) {
|
||||
const route = this.$router.resolve({
|
||||
name: VIEWS.EXECUTION_PREVIEW,
|
||||
|
@ -71,6 +71,7 @@
|
||||
v-for="execution in executions"
|
||||
:key="execution.id"
|
||||
:execution="execution"
|
||||
:ref="`execution-${execution.id}`"
|
||||
@refresh="onRefresh"
|
||||
@retryExecution="onRetryExecution"
|
||||
/>
|
||||
@ -95,6 +96,7 @@ import Vue from 'vue';
|
||||
import { PropType } from 'vue';
|
||||
import { mapStores } from 'pinia';
|
||||
import { useUIStore } from '@/stores/ui';
|
||||
import { useWorkflowsStore } from '@/stores/workflows';
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'executions-sidebar',
|
||||
@ -127,7 +129,7 @@ export default Vue.extend({
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapStores(useUIStore),
|
||||
...mapStores(useUIStore, useWorkflowsStore),
|
||||
statusFilterApplied(): boolean {
|
||||
return this.filter.status !== '';
|
||||
},
|
||||
@ -153,6 +155,7 @@ export default Vue.extend({
|
||||
if (this.autoRefresh) {
|
||||
this.autoRefreshInterval = setInterval(() => this.onRefresh(), 4000);
|
||||
}
|
||||
this.scrollToActiveCard();
|
||||
},
|
||||
beforeDestroy() {
|
||||
if (this.autoRefreshInterval) {
|
||||
@ -206,6 +209,18 @@ export default Vue.extend({
|
||||
status: this.filter.status,
|
||||
};
|
||||
},
|
||||
scrollToActiveCard(): void {
|
||||
const executionsList = this.$refs.executionList as HTMLElement;
|
||||
const currentExecutionCard = this.$refs[`execution-${this.workflowsStore.activeWorkflowExecution?.id}`] as Vue[];
|
||||
|
||||
if (executionsList && currentExecutionCard && this.workflowsStore.activeWorkflowExecution) {
|
||||
const cardElement = currentExecutionCard[0].$el as HTMLElement;
|
||||
const cardRect = cardElement.getBoundingClientRect();
|
||||
if (cardRect.top > executionsList.offsetHeight) {
|
||||
executionsList.scrollTo({ top: cardRect.top });
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
@ -174,7 +174,13 @@ export default mixins(
|
||||
const shouldUpdate = workflowUpdated && !onNewWorkflow;
|
||||
await this.initView(shouldUpdate);
|
||||
if (!shouldUpdate) {
|
||||
await this.setExecutions();
|
||||
if (this.workflowsStore.currentWorkflowExecutions.length > 0) {
|
||||
const workflowExecutions = await this.loadExecutions();
|
||||
this.workflowsStore.addToCurrentExecutions(workflowExecutions);
|
||||
this.setActiveExecution();
|
||||
} else {
|
||||
await this.setExecutions();
|
||||
}
|
||||
}
|
||||
this.loading = false;
|
||||
},
|
||||
@ -186,7 +192,9 @@ export default mixins(
|
||||
}
|
||||
await this.openWorkflow(this.$route.params.name);
|
||||
this.uiStore.nodeViewInitialized = false;
|
||||
this.setExecutions();
|
||||
if(this.workflowsStore.currentWorkflowExecutions.length === 0) {
|
||||
this.setExecutions();
|
||||
}
|
||||
if (this.activeExecution) {
|
||||
this.$router
|
||||
.push({
|
||||
|
@ -47,6 +47,7 @@
|
||||
:value="value"
|
||||
:isReadOnly="isReadOnly"
|
||||
@change="valueChanged"
|
||||
@close="closeDialog"
|
||||
ref="inputFieldExpression"
|
||||
data-test-id="expression-modal-input"
|
||||
/>
|
||||
|
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div ref="root" class="ph-no-capture"></div>
|
||||
<div ref="root" class="ph-no-capture" @keydown.stop @keydown.esc="onClose"></div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
@ -82,6 +82,9 @@ export default mixins(expressionManager, workflowHelpers).extend({
|
||||
this.editor?.destroy();
|
||||
},
|
||||
methods: {
|
||||
onClose() {
|
||||
this.$emit('close');
|
||||
},
|
||||
itemSelected({ variable }: IVariableItemSelected) {
|
||||
if (!this.editor || this.isReadOnly) return;
|
||||
|
||||
|
@ -61,7 +61,7 @@
|
||||
<span class="activator">
|
||||
<WorkflowActivator :workflow-active="isWorkflowActive" :workflow-id="currentWorkflowId" />
|
||||
</span>
|
||||
<enterprise-edition :features="[EnterpriseEditionFeature.WorkflowSharing]">
|
||||
<enterprise-edition :features="[EnterpriseEditionFeature.Sharing]">
|
||||
<n8n-button type="secondary" class="mr-2xs" @click="onShareButtonClick">
|
||||
{{ $locale.baseText('workflowDetails.share') }}
|
||||
</n8n-button>
|
||||
@ -72,16 +72,17 @@
|
||||
</n8n-button>
|
||||
<template #content>
|
||||
<i18n
|
||||
:path="dynamicTranslations.workflows.sharing.unavailable.description"
|
||||
:path="
|
||||
contextBasedTranslationKeys.workflows.sharing.unavailable.description.tooltip
|
||||
"
|
||||
tag="span"
|
||||
>
|
||||
<template #action>
|
||||
<a
|
||||
:href="dynamicTranslations.workflows.sharing.unavailable.linkURL"
|
||||
target="_blank"
|
||||
>
|
||||
<a @click="goToUpgrade">
|
||||
{{
|
||||
$locale.baseText(dynamicTranslations.workflows.sharing.unavailable.action)
|
||||
$locale.baseText(
|
||||
contextBasedTranslationKeys.workflows.sharing.unavailable.button,
|
||||
)
|
||||
}}
|
||||
</a>
|
||||
</template>
|
||||
@ -139,13 +140,7 @@ import SaveButton from '@/components/SaveButton.vue';
|
||||
import TagsDropdown from '@/components/TagsDropdown.vue';
|
||||
import InlineTextEdit from '@/components/InlineTextEdit.vue';
|
||||
import BreakpointsObserver from '@/components/BreakpointsObserver.vue';
|
||||
import {
|
||||
IUser,
|
||||
IWorkflowDataUpdate,
|
||||
IWorkflowDb,
|
||||
IWorkflowToShare,
|
||||
NestedRecord,
|
||||
} from '@/Interface';
|
||||
import { IUser, IWorkflowDataUpdate, IWorkflowDb, IWorkflowToShare } from '@/Interface';
|
||||
|
||||
import { saveAs } from 'file-saver';
|
||||
import { titleChange } from '@/mixins/titleChange';
|
||||
@ -158,6 +153,7 @@ import { useRootStore } from '@/stores/n8nRootStore';
|
||||
import { useTagsStore } from '@/stores/tags';
|
||||
import { getWorkflowPermissions, IPermissions } from '@/permissions';
|
||||
import { useUsersStore } from '@/stores/users';
|
||||
import { useUsageStore } from '@/stores/usage';
|
||||
|
||||
const hasChanged = (prev: string[], curr: string[]) => {
|
||||
if (prev.length !== curr.length) {
|
||||
@ -197,14 +193,15 @@ export default mixins(workflowHelpers, titleChange).extend({
|
||||
useRootStore,
|
||||
useSettingsStore,
|
||||
useUIStore,
|
||||
useUsageStore,
|
||||
useWorkflowsStore,
|
||||
useUsersStore,
|
||||
),
|
||||
currentUser(): IUser | null {
|
||||
return this.usersStore.currentUser;
|
||||
},
|
||||
dynamicTranslations(): NestedRecord<string> {
|
||||
return this.uiStore.dynamicTranslations;
|
||||
contextBasedTranslationKeys(): NestedRecord<string> {
|
||||
return this.uiStore.contextBasedTranslationKeys;
|
||||
},
|
||||
isWorkflowActive(): boolean {
|
||||
return this.workflowsStore.isWorkflowActive;
|
||||
@ -528,6 +525,14 @@ export default mixins(workflowHelpers, titleChange).extend({
|
||||
break;
|
||||
}
|
||||
},
|
||||
goToUpgrade() {
|
||||
let linkUrl = this.$locale.baseText(this.contextBasedTranslationKeys.upgradeLinkUrl);
|
||||
if (linkUrl.includes('subscription')) {
|
||||
linkUrl = this.usageStore.viewPlansUrl;
|
||||
}
|
||||
|
||||
window.open(linkUrl, '_blank');
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
currentWorkflowId() {
|
||||
|
@ -377,7 +377,7 @@ export default mixins(
|
||||
let hasForeignCredential = false;
|
||||
if (
|
||||
credentials &&
|
||||
this.settingsStore.isEnterpriseFeatureEnabled(EnterpriseEditionFeature.WorkflowSharing)
|
||||
this.settingsStore.isEnterpriseFeatureEnabled(EnterpriseEditionFeature.Sharing)
|
||||
) {
|
||||
Object.values(credentials).forEach((credential) => {
|
||||
if (
|
||||
|
@ -492,7 +492,7 @@ export default mixins(
|
||||
computed: {
|
||||
...mapStores(useCredentialsStore, useNodeTypesStore, useNDVStore, useWorkflowsStore),
|
||||
expressionDisplayValue(): string {
|
||||
if (this.activeDrop || this.forceShowExpression) {
|
||||
if (this.forceShowExpression) {
|
||||
return '';
|
||||
}
|
||||
|
||||
|
@ -1202,7 +1202,7 @@ export default mixins(externalHooks, genericHelpers, nodeHelpers, pinData).exten
|
||||
const { id, data, fileName, fileExtension, mimeType } = this.binaryData[index][key];
|
||||
|
||||
if (id) {
|
||||
const url = this.restApi().getBinaryUrl(id);
|
||||
const url = this.restApi().getBinaryUrl(id, 'download');
|
||||
saveAs(url, [fileName, fileExtension].join('.'));
|
||||
return;
|
||||
} else {
|
||||
|
@ -229,7 +229,7 @@ export default mixins(externalHooks, nodeBase, nodeHelpers, workflowHelpers).ext
|
||||
const updateInformation: INodeUpdatePropertiesInformation = {
|
||||
name: this.node.name,
|
||||
properties: {
|
||||
position: { position },
|
||||
position,
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -35,7 +35,7 @@
|
||||
</div>
|
||||
<template #append>
|
||||
<div :class="$style.cardActions">
|
||||
<enterprise-edition :features="[EnterpriseEditionFeature.WorkflowSharing]">
|
||||
<enterprise-edition :features="[EnterpriseEditionFeature.Sharing]">
|
||||
<n8n-badge v-if="workflowPermissions.isOwner" class="mr-xs" theme="tertiary" bold>
|
||||
{{ $locale.baseText('workflows.item.owner') }}
|
||||
</n8n-badge>
|
||||
|
@ -42,7 +42,7 @@
|
||||
</n8n-select>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<div v-if="isWorkflowSharingEnabled">
|
||||
<div v-if="isSharingEnabled">
|
||||
<el-row>
|
||||
<el-col :span="10" class="setting-name">
|
||||
{{ $locale.baseText('workflowSettings.callerPolicy') + ':' }}
|
||||
@ -84,6 +84,7 @@
|
||||
</el-col>
|
||||
<el-col :span="14">
|
||||
<n8n-input
|
||||
:placeholder="$locale.baseText('workflowSettings.callerIds.placeholder')"
|
||||
type="text"
|
||||
size="medium"
|
||||
v-model="workflowSettings.callerIds"
|
||||
@ -331,7 +332,9 @@ import { genericHelpers } from '@/mixins/genericHelpers';
|
||||
import { showMessage } from '@/mixins/showMessage';
|
||||
import {
|
||||
ITimeoutHMS,
|
||||
IUser,
|
||||
IWorkflowDataUpdate,
|
||||
IWorkflowDb,
|
||||
IWorkflowSettings,
|
||||
IWorkflowShortResponse,
|
||||
WorkflowCallerPolicyDefaultOption,
|
||||
@ -350,6 +353,8 @@ import { mapStores } from 'pinia';
|
||||
import { useWorkflowsStore } from '@/stores/workflows';
|
||||
import { useSettingsStore } from '@/stores/settings';
|
||||
import { useRootStore } from '@/stores/n8nRootStore';
|
||||
import useWorkflowsEEStore from '@/stores/workflows.ee';
|
||||
import { useUsersStore } from '@/stores/users';
|
||||
|
||||
export default mixins(externalHooks, genericHelpers, restApi, showMessage).extend({
|
||||
name: 'WorkflowSettings',
|
||||
@ -389,7 +394,7 @@ export default mixins(externalHooks, genericHelpers, restApi, showMessage).exten
|
||||
saveDataSuccessExecution: 'all',
|
||||
saveExecutionProgress: false,
|
||||
saveManualExecutions: false,
|
||||
workflowCallerPolicy: '',
|
||||
workflowCallerPolicy: 'workflowsFromSameOwner',
|
||||
},
|
||||
workflowCallerPolicyOptions: [] as Array<{ key: string; value: string }>,
|
||||
saveDataErrorExecutionOptions: [] as Array<{ key: string; value: string }>,
|
||||
@ -408,17 +413,34 @@ export default mixins(externalHooks, genericHelpers, restApi, showMessage).exten
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapStores(useRootStore, useSettingsStore, useWorkflowsStore),
|
||||
...mapStores(
|
||||
useRootStore,
|
||||
useUsersStore,
|
||||
useSettingsStore,
|
||||
useWorkflowsStore,
|
||||
useWorkflowsEEStore,
|
||||
),
|
||||
workflowName(): string {
|
||||
return this.workflowsStore.workflowName;
|
||||
},
|
||||
workflowId(): string {
|
||||
return this.workflowsStore.workflowId;
|
||||
},
|
||||
isWorkflowSharingEnabled(): boolean {
|
||||
return this.settingsStore.isEnterpriseFeatureEnabled(
|
||||
EnterpriseEditionFeature.WorkflowSharing,
|
||||
workflow(): IWorkflowDb {
|
||||
return this.workflowsStore.workflow;
|
||||
},
|
||||
currentUser(): IUser | null {
|
||||
return this.usersStore.currentUser;
|
||||
},
|
||||
isSharingEnabled(): boolean {
|
||||
return this.settingsStore.isEnterpriseFeatureEnabled(EnterpriseEditionFeature.Sharing);
|
||||
},
|
||||
workflowOwnerName(): string {
|
||||
const fallback = this.$locale.baseText(
|
||||
'workflowSettings.callerPolicy.options.workflowsFromSameOwner.fallback',
|
||||
);
|
||||
|
||||
return this.workflowsEEStore.getWorkflowOwnerName(`${this.workflowId}`, fallback);
|
||||
},
|
||||
},
|
||||
async mounted() {
|
||||
@ -518,19 +540,36 @@ export default mixins(externalHooks, genericHelpers, restApi, showMessage).exten
|
||||
};
|
||||
},
|
||||
async loadWorkflowCallerPolicyOptions() {
|
||||
const currentUserIsOwner = this.workflow.ownedBy?.id === this.currentUser?.id;
|
||||
|
||||
this.workflowCallerPolicyOptions = [
|
||||
{
|
||||
key: 'any',
|
||||
value: this.$locale.baseText('workflowSettings.callerPolicy.options.any'),
|
||||
},
|
||||
{
|
||||
key: 'none',
|
||||
value: this.$locale.baseText('workflowSettings.callerPolicy.options.none'),
|
||||
},
|
||||
{
|
||||
key: 'workflowsFromSameOwner',
|
||||
value: this.$locale.baseText(
|
||||
'workflowSettings.callerPolicy.options.workflowsFromSameOwner',
|
||||
{
|
||||
interpolate: {
|
||||
owner: currentUserIsOwner
|
||||
? this.$locale.baseText(
|
||||
'workflowSettings.callerPolicy.options.workflowsFromSameOwner.owner',
|
||||
)
|
||||
: this.workflowOwnerName,
|
||||
},
|
||||
},
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'workflowsFromAList',
|
||||
value: this.$locale.baseText('workflowSettings.callerPolicy.options.workflowsFromAList'),
|
||||
},
|
||||
{
|
||||
key: 'any',
|
||||
value: this.$locale.baseText('workflowSettings.callerPolicy.options.any'),
|
||||
},
|
||||
];
|
||||
},
|
||||
async loadSaveDataErrorExecutionOptions() {
|
||||
|
@ -1,18 +1,23 @@
|
||||
<template>
|
||||
<Modal
|
||||
width="460px"
|
||||
:title="
|
||||
$locale.baseText(dynamicTranslations.workflows.shareModal.title, {
|
||||
interpolate: { name: workflow.name },
|
||||
})
|
||||
"
|
||||
:title="modalTitle"
|
||||
:eventBus="modalBus"
|
||||
:name="WORKFLOW_SHARE_MODAL_KEY"
|
||||
:center="true"
|
||||
:beforeClose="onCloseModal"
|
||||
>
|
||||
<template #content>
|
||||
<div v-if="isDefaultUser" :class="$style.container">
|
||||
<div v-if="!isSharingEnabled" :class="$style.container">
|
||||
<n8n-text>
|
||||
{{
|
||||
$locale.baseText(
|
||||
contextBasedTranslationKeys.workflows.sharing.unavailable.description.modal,
|
||||
)
|
||||
}}
|
||||
</n8n-text>
|
||||
</div>
|
||||
<div v-else-if="isDefaultUser" :class="$style.container">
|
||||
<n8n-text>
|
||||
{{ $locale.baseText('workflows.shareModal.isDefaultUser.description') }}
|
||||
</n8n-text>
|
||||
@ -25,7 +30,7 @@
|
||||
})
|
||||
}}
|
||||
</n8n-info-tip>
|
||||
<enterprise-edition :features="[EnterpriseEditionFeature.WorkflowSharing]">
|
||||
<enterprise-edition :features="[EnterpriseEditionFeature.Sharing]">
|
||||
<n8n-user-select
|
||||
v-if="workflowPermissions.updateSharing"
|
||||
class="mb-s"
|
||||
@ -66,7 +71,7 @@
|
||||
<template #fallback>
|
||||
<n8n-text>
|
||||
<i18n
|
||||
:path="dynamicTranslations.workflows.sharing.unavailable.description"
|
||||
:path="contextBasedTranslationKeys.workflows.sharing.unavailable.description"
|
||||
tag="span"
|
||||
>
|
||||
<template #action />
|
||||
@ -78,14 +83,19 @@
|
||||
</template>
|
||||
|
||||
<template #footer>
|
||||
<div v-if="isDefaultUser" :class="$style.actionButtons">
|
||||
<div v-if="!isSharingEnabled" :class="$style.actionButtons">
|
||||
<n8n-button @click="goToUpgrade">
|
||||
{{ $locale.baseText(contextBasedTranslationKeys.workflows.sharing.unavailable.button) }}
|
||||
</n8n-button>
|
||||
</div>
|
||||
<div v-else-if="isDefaultUser" :class="$style.actionButtons">
|
||||
<n8n-button @click="goToUsersSettings">
|
||||
{{ $locale.baseText('workflows.shareModal.isDefaultUser.button') }}
|
||||
</n8n-button>
|
||||
</div>
|
||||
<enterprise-edition
|
||||
v-else
|
||||
:features="[EnterpriseEditionFeature.WorkflowSharing]"
|
||||
:features="[EnterpriseEditionFeature.Sharing]"
|
||||
:class="$style.actionButtons"
|
||||
>
|
||||
<n8n-text v-show="isDirty" color="text-light" size="small" class="mr-xs">
|
||||
@ -102,9 +112,11 @@
|
||||
</n8n-button>
|
||||
|
||||
<template #fallback>
|
||||
<n8n-link :to="dynamicTranslations.workflows.sharing.unavailable.linkURL">
|
||||
<n8n-link :to="contextBasedTranslationKeys.workflows.sharing.unavailable.linkUrl">
|
||||
<n8n-button :loading="loading" size="medium">
|
||||
{{ $locale.baseText(dynamicTranslations.workflows.sharing.unavailable.button) }}
|
||||
{{
|
||||
$locale.baseText(contextBasedTranslationKeys.workflows.sharing.unavailable.button)
|
||||
}}
|
||||
</n8n-button>
|
||||
</n8n-link>
|
||||
</template>
|
||||
@ -122,7 +134,7 @@ import {
|
||||
VIEWS,
|
||||
WORKFLOW_SHARE_MODAL_KEY,
|
||||
} from '../constants';
|
||||
import { IUser, IWorkflowDb, NestedRecord } from '@/Interface';
|
||||
import { IUser, IWorkflowDb, UIState } from '@/Interface';
|
||||
import { getWorkflowPermissions, IPermissions } from '@/permissions';
|
||||
import mixins from 'vue-typed-mixins';
|
||||
import { showMessage } from '@/mixins/showMessage';
|
||||
@ -134,6 +146,7 @@ import { useUsersStore } from '@/stores/users';
|
||||
import { useWorkflowsStore } from '@/stores/workflows';
|
||||
import { useWorkflowsEEStore } from '@/stores/workflows.ee';
|
||||
import { ITelemetryTrackProperties } from 'n8n-workflow';
|
||||
import { useUsageStore } from '@/stores/usage';
|
||||
|
||||
export default mixins(showMessage).extend({
|
||||
name: 'workflow-share-modal',
|
||||
@ -166,12 +179,26 @@ export default mixins(showMessage).extend({
|
||||
useSettingsStore,
|
||||
useUIStore,
|
||||
useUsersStore,
|
||||
useUsageStore,
|
||||
useWorkflowsStore,
|
||||
useWorkflowsEEStore,
|
||||
),
|
||||
isDefaultUser(): boolean {
|
||||
return this.usersStore.isDefaultUser;
|
||||
},
|
||||
isSharingEnabled(): boolean {
|
||||
return this.settingsStore.isEnterpriseFeatureEnabled(EnterpriseEditionFeature.Sharing);
|
||||
},
|
||||
modalTitle(): string {
|
||||
return this.$locale.baseText(
|
||||
this.isSharingEnabled
|
||||
? this.contextBasedTranslationKeys.workflows.sharing.title
|
||||
: this.contextBasedTranslationKeys.workflows.sharing.unavailable.title,
|
||||
{
|
||||
interpolate: { name: this.workflow.name },
|
||||
},
|
||||
);
|
||||
},
|
||||
usersList(): IUser[] {
|
||||
return this.usersStore.allUsers.filter((user: IUser) => {
|
||||
const isCurrentUser = user.id === this.usersStore.currentUser?.id;
|
||||
@ -208,14 +235,8 @@ export default mixins(showMessage).extend({
|
||||
workflowOwnerName(): string {
|
||||
return this.workflowsEEStore.getWorkflowOwnerName(`${this.workflow.id}`);
|
||||
},
|
||||
isSharingAvailable(): boolean {
|
||||
return (
|
||||
this.settingsStore.isEnterpriseFeatureEnabled(EnterpriseEditionFeature.WorkflowSharing) ===
|
||||
true
|
||||
);
|
||||
},
|
||||
dynamicTranslations(): NestedRecord<string> {
|
||||
return this.uiStore.dynamicTranslations;
|
||||
contextBasedTranslationKeys(): UIState['contextBasedTranslationKeys'] {
|
||||
return this.uiStore.contextBasedTranslationKeys;
|
||||
},
|
||||
isDirty(): boolean {
|
||||
const previousSharedWith = this.workflow.sharedWith || [];
|
||||
@ -240,10 +261,10 @@ export default mixins(showMessage).extend({
|
||||
return new Promise<string>((resolve) => {
|
||||
if (this.workflow.id === PLACEHOLDER_EMPTY_WORKFLOW_ID) {
|
||||
nodeViewEventBus.$emit('saveWorkflow', () => {
|
||||
resolve(this.workflowsStore.workflowId);
|
||||
resolve(this.workflow.id);
|
||||
});
|
||||
} else {
|
||||
resolve(this.workflowsStore.workflowId);
|
||||
resolve(this.workflow.id);
|
||||
}
|
||||
});
|
||||
};
|
||||
@ -411,9 +432,17 @@ export default mixins(showMessage).extend({
|
||||
...data,
|
||||
});
|
||||
},
|
||||
goToUpgrade() {
|
||||
let linkUrl = this.$locale.baseText(this.contextBasedTranslationKeys.upgradeLinkUrl);
|
||||
if (linkUrl.includes('subscription')) {
|
||||
linkUrl = this.usageStore.viewPlansUrl;
|
||||
}
|
||||
|
||||
window.open(linkUrl, '_blank');
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
if (this.isSharingAvailable) {
|
||||
if (this.isSharingEnabled) {
|
||||
this.loadUsers();
|
||||
}
|
||||
},
|
||||
|
@ -57,21 +57,22 @@ export const BREAKPOINT_LG = 1200;
|
||||
export const BREAKPOINT_XL = 1920;
|
||||
|
||||
export const N8N_IO_BASE_URL = `https://api.n8n.io/api/`;
|
||||
export const BUILTIN_NODES_DOCS_URL = `https://docs.n8n.io/integrations/builtin/`;
|
||||
export const BUILTIN_CREDENTIALS_DOCS_URL = `https://docs.n8n.io/integrations/builtin/credentials/`;
|
||||
export const DATA_PINNING_DOCS_URL = 'https://docs.n8n.io/data/data-pinning/';
|
||||
export const DATA_EDITING_DOCS_URL = 'https://docs.n8n.io/data/data-editing/';
|
||||
export const DOCS_DOMAIN = 'docs.n8n.io';
|
||||
export const BUILTIN_NODES_DOCS_URL = `https://${DOCS_DOMAIN}/integrations/builtin/`;
|
||||
export const BUILTIN_CREDENTIALS_DOCS_URL = `https://${DOCS_DOMAIN}/integrations/builtin/credentials/`;
|
||||
export const DATA_PINNING_DOCS_URL = `https://${DOCS_DOMAIN}/data/data-pinning/`;
|
||||
export const DATA_EDITING_DOCS_URL = `https://${DOCS_DOMAIN}/data/data-editing/`;
|
||||
export const NPM_COMMUNITY_NODE_SEARCH_API_URL = `https://api.npms.io/v2/`;
|
||||
export const NPM_PACKAGE_DOCS_BASE_URL = `https://www.npmjs.com/package/`;
|
||||
export const NPM_KEYWORD_SEARCH_URL = `https://www.npmjs.com/search?q=keywords%3An8n-community-node-package`;
|
||||
export const N8N_QUEUE_MODE_DOCS_URL = `https://docs.n8n.io/hosting/scaling/queue-mode/`;
|
||||
export const COMMUNITY_NODES_INSTALLATION_DOCS_URL = `https://docs.n8n.io/integrations/community-nodes/installation/`;
|
||||
export const N8N_QUEUE_MODE_DOCS_URL = `https://${DOCS_DOMAIN}/hosting/scaling/queue-mode/`;
|
||||
export const COMMUNITY_NODES_INSTALLATION_DOCS_URL = `https://${DOCS_DOMAIN}/integrations/community-nodes/installation/`;
|
||||
export const COMMUNITY_NODES_NPM_INSTALLATION_URL =
|
||||
'https://docs.npmjs.com/downloading-and-installing-node-js-and-npm';
|
||||
export const COMMUNITY_NODES_RISKS_DOCS_URL = `https://docs.n8n.io/integrations/community-nodes/risks/`;
|
||||
export const COMMUNITY_NODES_BLOCKLIST_DOCS_URL = `https://docs.n8n.io/integrations/community-nodes/blocklist/`;
|
||||
export const CUSTOM_NODES_DOCS_URL = `https://docs.n8n.io/integrations/creating-nodes/code/create-n8n-nodes-module/`;
|
||||
export const EXPRESSIONS_DOCS_URL = 'https://docs.n8n.io/code-examples/expressions/';
|
||||
export const COMMUNITY_NODES_RISKS_DOCS_URL = `https://${DOCS_DOMAIN}/integrations/community-nodes/risks/`;
|
||||
export const COMMUNITY_NODES_BLOCKLIST_DOCS_URL = `https://${DOCS_DOMAIN}/integrations/community-nodes/blocklist/`;
|
||||
export const CUSTOM_NODES_DOCS_URL = `https://${DOCS_DOMAIN}/integrations/creating-nodes/code/create-n8n-nodes-module/`;
|
||||
export const EXPRESSIONS_DOCS_URL = `https://${DOCS_DOMAIN}/code-examples/expressions/`;
|
||||
|
||||
// node types
|
||||
export const BAMBOO_HR_NODE_TYPE = 'n8n-nodes-base.bambooHr';
|
||||
@ -318,8 +319,6 @@ export enum VIEWS {
|
||||
export enum FAKE_DOOR_FEATURES {
|
||||
ENVIRONMENTS = 'environments',
|
||||
LOGGING = 'logging',
|
||||
CREDENTIALS_SHARING = 'credentialsSharing',
|
||||
WORKFLOWS_SHARING = 'workflowsSharing',
|
||||
}
|
||||
|
||||
export const ONBOARDING_PROMPT_TIMEBOX = 14;
|
||||
@ -374,7 +373,6 @@ export enum WORKFLOW_MENU_ACTIONS {
|
||||
*/
|
||||
export enum EnterpriseEditionFeature {
|
||||
Sharing = 'sharing',
|
||||
WorkflowSharing = 'workflowSharing',
|
||||
}
|
||||
export const MAIN_NODE_PANEL_WIDTH = 360;
|
||||
|
||||
|
@ -119,14 +119,12 @@ export const expressionManager = mixins(workflowHelpers).extend({
|
||||
* _part_ of the result, but displayed when they are the _entire_ result.
|
||||
*
|
||||
* Example:
|
||||
* - Expression `This is a {{ null }} test` is displayed as `This is a test`.
|
||||
* - Expression `{{ null }}` is displayed as `[Object: null]`.
|
||||
* - Expression `This is a {{ [] }} test` is displayed as `This is a test`.
|
||||
* - Expression `{{ [] }}` is displayed as `[Array: []]`.
|
||||
*
|
||||
* Conditionally displayed segments:
|
||||
* - `[Object: null]`
|
||||
* - `[Array: []]`
|
||||
* - `[empty]` (from `''`, not from `undefined`)
|
||||
* - `null` (from `NaN`)
|
||||
*
|
||||
* Exceptionally, for two segments, display differs based on context:
|
||||
* - Date is displayed as
|
||||
@ -157,9 +155,8 @@ export const expressionManager = mixins(workflowHelpers).extend({
|
||||
this.segments.length > 1 &&
|
||||
s.kind === 'resolvable' &&
|
||||
typeof s.resolved === 'string' &&
|
||||
(['[Object: null]', '[Array: []]'].includes(s.resolved) ||
|
||||
s.resolved === this.$locale.baseText('expressionModalInput.empty') ||
|
||||
s.resolved === this.$locale.baseText('expressionModalInput.null'))
|
||||
(s.resolved === '[Array: []]' ||
|
||||
s.resolved === this.$locale.baseText('expressionModalInput.empty'))
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { IExecutionResponse, IExecutionsCurrentSummaryExtended, IPushData } from '../../Interface';
|
||||
import { IExecutionResponse, IExecutionsCurrentSummaryExtended, IPushData } from '@/Interface';
|
||||
|
||||
import { externalHooks } from '@/mixins/externalHooks';
|
||||
import { nodeHelpers } from '@/mixins/nodeHelpers';
|
||||
|
@ -201,13 +201,8 @@ export const restApi = Vue.extend({
|
||||
},
|
||||
|
||||
// Binary data
|
||||
getBinaryBufferString: (dataPath: string): Promise<string> => {
|
||||
return self.restApi().makeRestApiRequest('GET', `/data/${dataPath}`);
|
||||
},
|
||||
|
||||
getBinaryUrl: (dataPath: string): string => {
|
||||
return self.rootStore.getRestApiContext.baseUrl + `/data/${dataPath}`;
|
||||
},
|
||||
getBinaryUrl: (dataPath, mode): string =>
|
||||
self.rootStore.getRestApiContext.baseUrl + `/data/${dataPath}?mode=${mode}`,
|
||||
};
|
||||
},
|
||||
},
|
||||
|
@ -1,6 +1,6 @@
|
||||
import Vue from 'vue';
|
||||
|
||||
import { WorkflowTitleStatus } from '../../Interface';
|
||||
import { WorkflowTitleStatus } from '@/Interface';
|
||||
|
||||
export const titleChange = Vue.extend({
|
||||
methods: {
|
||||
|
@ -66,8 +66,8 @@ import { IWorkflowSettings } from 'n8n-workflow';
|
||||
import { useNDVStore } from '@/stores/ndv';
|
||||
import { useTemplatesStore } from '@/stores/templates';
|
||||
import { useNodeTypesStore } from '@/stores/nodeTypes';
|
||||
import { useUsersStore } from '@/stores/users';
|
||||
import { useWorkflowsEEStore } from '@/stores/workflows.ee';
|
||||
import { useUsersStore } from '@/stores/users';
|
||||
import { getWorkflowPermissions, IPermissions } from '@/permissions';
|
||||
import { ICredentialsResponse } from '@/Interface';
|
||||
|
||||
@ -928,7 +928,7 @@ export const workflowHelpers = mixins(externalHooks, nodeHelpers, restApi, showM
|
||||
this.workflowsStore.setWorkflowVersionId(workflowData.versionId);
|
||||
|
||||
if (
|
||||
this.settingsStore.isEnterpriseFeatureEnabled(EnterpriseEditionFeature.WorkflowSharing) &&
|
||||
this.settingsStore.isEnterpriseFeatureEnabled(EnterpriseEditionFeature.Sharing) &&
|
||||
this.usersStore.currentUser
|
||||
) {
|
||||
this.workflowsEEStore.setWorkflowOwnedBy({
|
||||
|
@ -12,7 +12,7 @@ export enum UserRole {
|
||||
InstanceOwner = 'isInstanceOwner',
|
||||
ResourceOwner = 'isOwner',
|
||||
ResourceEditor = 'isEditor',
|
||||
ResourceReader = 'isReader',
|
||||
ResourceSharee = 'isSharee',
|
||||
}
|
||||
|
||||
export type IPermissions = Record<string, boolean>;
|
||||
@ -65,7 +65,7 @@ export const getCredentialPermissions = (user: IUser | null, credential: ICreden
|
||||
!isSharingEnabled,
|
||||
},
|
||||
{
|
||||
name: UserRole.ResourceReader,
|
||||
name: UserRole.ResourceSharee,
|
||||
test: () =>
|
||||
!!(
|
||||
credential &&
|
||||
@ -75,7 +75,7 @@ export const getCredentialPermissions = (user: IUser | null, credential: ICreden
|
||||
},
|
||||
{
|
||||
name: 'read',
|
||||
test: [UserRole.ResourceOwner, UserRole.InstanceOwner, UserRole.ResourceReader],
|
||||
test: [UserRole.ResourceOwner, UserRole.InstanceOwner, UserRole.ResourceSharee],
|
||||
},
|
||||
{ name: 'save', test: [UserRole.ResourceOwner, UserRole.InstanceOwner] },
|
||||
{ name: 'updateName', test: [UserRole.ResourceOwner, UserRole.InstanceOwner] },
|
||||
@ -83,7 +83,7 @@ export const getCredentialPermissions = (user: IUser | null, credential: ICreden
|
||||
{ name: 'updateSharing', test: [UserRole.ResourceOwner] },
|
||||
{ name: 'updateNodeAccess', test: [UserRole.ResourceOwner] },
|
||||
{ name: 'delete', test: [UserRole.ResourceOwner, UserRole.InstanceOwner] },
|
||||
{ name: 'use', test: [UserRole.ResourceOwner, UserRole.ResourceReader] },
|
||||
{ name: 'use', test: [UserRole.ResourceOwner, UserRole.ResourceSharee] },
|
||||
];
|
||||
|
||||
return parsePermissionsTable(user, table);
|
||||
@ -92,7 +92,7 @@ export const getCredentialPermissions = (user: IUser | null, credential: ICreden
|
||||
export const getWorkflowPermissions = (user: IUser | null, workflow: IWorkflowDb) => {
|
||||
const settingsStore = useSettingsStore();
|
||||
const isSharingEnabled = settingsStore.isEnterpriseFeatureEnabled(
|
||||
EnterpriseEditionFeature.WorkflowSharing,
|
||||
EnterpriseEditionFeature.Sharing,
|
||||
);
|
||||
const isNewWorkflow = workflow.id === PLACEHOLDER_EMPTY_WORKFLOW_ID;
|
||||
|
||||
@ -104,7 +104,7 @@ export const getWorkflowPermissions = (user: IUser | null, workflow: IWorkflowDb
|
||||
!isSharingEnabled,
|
||||
},
|
||||
{
|
||||
name: UserRole.ResourceReader,
|
||||
name: UserRole.ResourceSharee,
|
||||
test: () =>
|
||||
!!(
|
||||
workflow &&
|
||||
@ -114,7 +114,7 @@ export const getWorkflowPermissions = (user: IUser | null, workflow: IWorkflowDb
|
||||
},
|
||||
{
|
||||
name: 'read',
|
||||
test: [UserRole.ResourceOwner, UserRole.InstanceOwner, UserRole.ResourceReader],
|
||||
test: [UserRole.ResourceOwner, UserRole.InstanceOwner, UserRole.ResourceSharee],
|
||||
},
|
||||
{ name: 'save', test: [UserRole.ResourceOwner, UserRole.InstanceOwner] },
|
||||
{ name: 'updateName', test: [UserRole.ResourceOwner, UserRole.InstanceOwner] },
|
||||
@ -124,7 +124,7 @@ export const getWorkflowPermissions = (user: IUser | null, workflow: IWorkflowDb
|
||||
{ name: 'delete', test: [UserRole.ResourceOwner, UserRole.InstanceOwner] },
|
||||
{
|
||||
name: 'use',
|
||||
test: [UserRole.ResourceOwner, UserRole.InstanceOwner, UserRole.ResourceReader],
|
||||
test: [UserRole.ResourceOwner, UserRole.InstanceOwner, UserRole.ResourceSharee],
|
||||
},
|
||||
];
|
||||
|
||||
|
@ -256,6 +256,7 @@
|
||||
"credentialEdit.credentialConfig.oAuthRedirectUrl": "OAuth Redirect URL",
|
||||
"credentialEdit.credentialConfig.openDocs": "Open docs",
|
||||
"credentialEdit.credentialConfig.pleaseCheckTheErrorsBelow": "Please check the errors below",
|
||||
"credentialEdit.credentialConfig.pleaseCheckTheErrorsBelow.sharee": "Problem with connection settings. {owner} may be able to fix this",
|
||||
"credentialEdit.credentialConfig.reconnect": "Reconnect",
|
||||
"credentialEdit.credentialConfig.reconnectOAuth2Credential": "Reconnect OAuth2 Credential",
|
||||
"credentialEdit.credentialConfig.redirectUrlCopiedToClipboard": "Redirect URL copied to clipboard",
|
||||
@ -300,15 +301,16 @@
|
||||
"credentialEdit.oAuthButton.connectMyAccount": "Connect my account",
|
||||
"credentialEdit.oAuthButton.signInWithGoogle": "Sign in with Google",
|
||||
"credentialEdit.credentialSharing.info.owner": "Sharing a credential allows people to use it in their workflows. They cannot access credential details.",
|
||||
"credentialEdit.credentialSharing.info.instanceOwner": "You have access to this credential because you’re the Instance Owner. You can view partial data, update the credential title, or delete the credential.",
|
||||
"credentialEdit.credentialSharing.info.instanceOwner": "You can view this credential because you are the instance owner (and rename or delete it too). To use it in a workflow, ask the credential owner to share it with you.",
|
||||
"credentialEdit.credentialSharing.info.sharee": "Only {credentialOwnerName} can change who this credential is shared with",
|
||||
"credentialEdit.credentialSharing.info.sharee.fallback": "the owner",
|
||||
"credentialEdit.credentialSharing.select.placeholder": "Add people",
|
||||
"credentialEdit.credentialSharing.select.placeholder": "Add users...",
|
||||
"credentialEdit.credentialSharing.list.delete": "Remove",
|
||||
"credentialEdit.credentialSharing.list.delete.confirm.title": "Remove access?",
|
||||
"credentialEdit.credentialSharing.list.delete.confirm.message": "This may break any workflows in which {name} has used this credential",
|
||||
"credentialEdit.credentialSharing.list.delete.confirm.confirmButtonText": "Remove",
|
||||
"credentialEdit.credentialSharing.list.delete.confirm.cancelButtonText": "Cancel",
|
||||
"credentialEdit.credentialSharing.isDefaultUser.title": "Sharing",
|
||||
"credentialEdit.credentialSharing.isDefaultUser.description": "You first need to set up your owner account to enable credential sharing features.",
|
||||
"credentialEdit.credentialSharing.isDefaultUser.button": "Go to settings",
|
||||
"credentialSelectModal.addNewCredential": "Add new credential",
|
||||
@ -495,12 +497,6 @@
|
||||
"expressionModalInput.empty": "[empty]",
|
||||
"expressionModalInput.undefined": "[undefined]",
|
||||
"expressionModalInput.null": "null",
|
||||
"fakeDoor.credentialEdit.sharing.name": "Sharing",
|
||||
"fakeDoor.credentialEdit.sharing.actionBox.title": "Sharing is only available on <a href=\"https://n8n.io/cloud/\" target=\"_blank\">n8n Cloud</a> right now",
|
||||
"fakeDoor.credentialEdit.sharing.actionBox.description": "We’re working on bringing it to this edition of n8n, as a paid feature. If you’d like to be the first to hear when it’s ready, join the list.",
|
||||
"fakeDoor.credentialEdit.sharing.actionBox.title.cloud.upgrade": "Upgrade to share credentials",
|
||||
"fakeDoor.credentialEdit.sharing.actionBox.description.cloud.upgrade": "Power and Pro plan users can create multiple user accounts and collaborate on workflows",
|
||||
"fakeDoor.credentialEdit.sharing.actionBox.button.cloud.upgrade": "Upgrade",
|
||||
"fakeDoor.settings.environments.name": "Environments",
|
||||
"fakeDoor.settings.environments.infoText": "Environments allow you to use different settings and credentials in a workflow when you're building it vs when it's running in production",
|
||||
"fakeDoor.settings.environments.actionBox.title": "We’re working on environments (as a paid feature)",
|
||||
@ -513,8 +509,8 @@
|
||||
"fakeDoor.settings.logging.actionBox.description": "This also includes audit logging. If you'd like to be the first to hear when it's ready, join the list.",
|
||||
"fakeDoor.settings.users.name": "Users",
|
||||
"fakeDoor.settings.users.actionBox.title": "Upgrade to add users",
|
||||
"fakeDoor.settings.users.actionBox.description": "Power and Pro plan users can create multiple user accounts and share credentials. (Sharing workflows is coming soon)",
|
||||
"fakeDoor.settings.users.actionBox.button": "Upgrade",
|
||||
"fakeDoor.settings.users.actionBox.description": "Create multiple users on our higher plans and share workflows and credentials to collaborate",
|
||||
"fakeDoor.settings.users.actionBox.button": "Upgrade now",
|
||||
"fakeDoor.actionBox.button.label": "Join the list",
|
||||
"fixedCollectionParameter.choose": "Choose...",
|
||||
"fixedCollectionParameter.currentlyNoItemsExist": "Currently no items exist",
|
||||
@ -1332,9 +1328,13 @@
|
||||
"workflowRun.showError.title": "Problem running workflow",
|
||||
"workflowRun.showMessage.message": "Please fix them before executing",
|
||||
"workflowRun.showMessage.title": "Workflow has issues",
|
||||
"workflowSettings.callerIds": "Caller IDs",
|
||||
"workflowSettings.callerIds": "IDs of workflows that can call this one",
|
||||
"workflowSettings.callerIds.placeholder": "e.g. 14, 18",
|
||||
"workflowSettings.callerPolicy": "This workflow can be called by",
|
||||
"workflowSettings.callerPolicy.options.any": "Any workflow",
|
||||
"workflowSettings.callerPolicy.options.workflowsFromSameOwner": "Workflows created by {owner}",
|
||||
"workflowSettings.callerPolicy.options.workflowsFromSameOwner.owner": "me",
|
||||
"workflowSettings.callerPolicy.options.workflowsFromSameOwner.fallback": "same owner",
|
||||
"workflowSettings.callerPolicy.options.workflowsFromAList": "Selected workflows",
|
||||
"workflowSettings.callerPolicy.options.none": "No other workflows",
|
||||
"workflowSettings.defaultTimezone": "Default - {defaultTimezoneValue}",
|
||||
@ -1348,7 +1348,7 @@
|
||||
"workflowSettings.helpTexts.saveExecutionProgress": "Whether to save data after each node execution. This allows you to resume from where execution stopped if there is an error, but may increase latency.",
|
||||
"workflowSettings.helpTexts.saveManualExecutions": "Whether to save data of executions that are started manually from the editor",
|
||||
"workflowSettings.helpTexts.timezone": "The timezone in which the workflow should run. Used by 'cron' node, for example.",
|
||||
"workflowSettings.helpTexts.workflowCallerIds": "Comma-delimited list of IDs of workflows that are allowed to call this workflow",
|
||||
"workflowSettings.helpTexts.workflowCallerIds": "The IDs of the workflows that are allowed to execute this one (using an ‘execute workflow’ node). The ID can be found at the end of the workflow’s URL. Separate multiple IDs by commas.",
|
||||
"workflowSettings.helpTexts.workflowCallerPolicy": "Workflows that are allowed to call this workflow using the Execute Workflow node",
|
||||
"workflowSettings.hours": "hours",
|
||||
"workflowSettings.minutes": "minutes",
|
||||
@ -1423,7 +1423,7 @@
|
||||
"workflows.empty.startFromScratch": "Start from scratch",
|
||||
"workflows.empty.browseTemplates": "Browse templates",
|
||||
"workflows.shareModal.title": "Share '{name}'",
|
||||
"workflows.shareModal.select.placeholder": "Add people",
|
||||
"workflows.shareModal.select.placeholder": "Add users...",
|
||||
"workflows.shareModal.list.delete": "Remove access",
|
||||
"workflows.shareModal.list.delete.confirm.title": "Remove {name}'s access?",
|
||||
"workflows.shareModal.list.delete.confirm.lastUserWithAccessToCredentials.message": "If you do this, the workflow will lose access to {name}’s credentials. <strong>Nodes that use those credentials will stop working</strong>.",
|
||||
@ -1457,13 +1457,31 @@
|
||||
"importParameter.showError.invalidProtocol1.title": "Use the {node} node",
|
||||
"importParameter.showError.invalidProtocol2.title": "Invalid Protocol",
|
||||
"importParameter.showError.invalidProtocol.message": "The HTTP node doesn’t support {protocol} requests",
|
||||
"dynamic.workflows.shareModal.title": "Share '{name}'",
|
||||
"dynamic.workflows.shareModal.title.cloud.upgrade": "Upgrade to add users",
|
||||
"dynamic.workflows.sharing.unavailable.description": "You can collaborate with others on workflows when you upgrade your plan. {action}",
|
||||
"dynamic.workflows.sharing.unavailable.description.cloud.upgrade": "Sharing is available for Team and Enterprise plans. {action} to unlock more features.",
|
||||
"dynamic.workflows.sharing.unavailable.action": "See plans",
|
||||
"dynamic.workflows.sharing.unavailable.action.cloud.upgrade": "Upgrade now",
|
||||
"dynamic.workflows.sharing.unavailable.button": "See plans",
|
||||
"dynamic.workflows.sharing.unavailable.button.cloud.upgrade": "Upgrade now",
|
||||
"dynamic.workflows.sharing.unavailable.linkUrl": "https://subscription.n8n.io/"
|
||||
|
||||
"contextual.credentials.sharing.unavailable.title": "Upgrade to collaborate",
|
||||
"contextual.credentials.sharing.unavailable.title.cloud": "Upgrade to collaborate",
|
||||
"contextual.credentials.sharing.unavailable.title.desktop": "Upgrade to n8n Cloud to collaborate",
|
||||
"contextual.credentials.sharing.unavailable.description": "You can share credentials with others when you upgrade your plan.",
|
||||
"contextual.credentials.sharing.unavailable.description.cloud": "You can share credentials with others when you upgrade your plan.",
|
||||
"contextual.credentials.sharing.unavailable.description.desktop": "Sharing features are available on selected Cloud plans",
|
||||
"contextual.credentials.sharing.unavailable.button": "View plans",
|
||||
"contextual.credentials.sharing.unavailable.button.cloud": "Upgrade now",
|
||||
"contextual.credentials.sharing.unavailable.button.desktop": "View plans",
|
||||
|
||||
"contextual.workflows.sharing.title": "Sharing",
|
||||
"contextual.workflows.sharing.unavailable.title": "Sharing",
|
||||
"contextual.workflows.sharing.unavailable.title.cloud": "Upgrade to collaborate",
|
||||
"contextual.workflows.sharing.unavailable.title.desktop": "Upgrade to n8n Cloud to collaborate",
|
||||
"contextual.workflows.sharing.unavailable.description.modal": "You can collaborate with others on workflows when you upgrade your plan.",
|
||||
"contextual.workflows.sharing.unavailable.description.modal.cloud": "You can collaborate with others on workflows when you upgrade your plan.",
|
||||
"contextual.workflows.sharing.unavailable.description.modal.desktop": "Upgrade to n8n Cloud to collaborate on workflows: sharing features are available on selected plans.",
|
||||
"contextual.workflows.sharing.unavailable.description.tooltip": "You can collaborate with others on workflows when you upgrade your plan. {action}",
|
||||
"contextual.workflows.sharing.unavailable.description.tooltip.cloud": "You can collaborate with others on workflows when you upgrade your plan. {action}",
|
||||
"contextual.workflows.sharing.unavailable.description.tooltip.desktop": "Upgrade to n8n Cloud to collaborate on workflows: sharing features are available on selected plans. {action}",
|
||||
"contextual.workflows.sharing.unavailable.button": "View plans",
|
||||
"contextual.workflows.sharing.unavailable.button.cloud": "Upgrade now",
|
||||
"contextual.workflows.sharing.unavailable.button.desktop": "View plans",
|
||||
"contextual.upgradeLinkUrl": "https://subscription.n8n.io/",
|
||||
"contextual.upgradeLinkUrl.cloud": "https://app.n8n.cloud/manage?edition=cloud",
|
||||
"contextual.upgradeLinkUrl.desktop": "https://n8n.io/pricing"
|
||||
}
|
||||
|
@ -53,7 +53,7 @@ function getTemplatesRedirect() {
|
||||
const settingsStore = useSettingsStore();
|
||||
const isTemplatesEnabled: boolean = settingsStore.isTemplatesEnabled;
|
||||
if (!isTemplatesEnabled) {
|
||||
return { name: VIEWS.NOT_FOUND };
|
||||
return {name: VIEWS.NOT_FOUND};
|
||||
}
|
||||
|
||||
return false;
|
||||
@ -75,7 +75,7 @@ const router = new Router({
|
||||
name: VIEWS.HOMEPAGE,
|
||||
meta: {
|
||||
getRedirect() {
|
||||
return { name: VIEWS.WORKFLOWS };
|
||||
return {name: VIEWS.WORKFLOWS};
|
||||
},
|
||||
permissions: {
|
||||
allow: {
|
||||
@ -450,7 +450,7 @@ const router = new Router({
|
||||
deny: {
|
||||
shouldDeny: () => {
|
||||
const settingsStore = useSettingsStore();
|
||||
return settingsStore.settings.hideUsagePage === true;
|
||||
return settingsStore.settings.hideUsagePage === true || settingsStore.settings.deployment?.type === 'cloud';
|
||||
},
|
||||
},
|
||||
},
|
||||
|
2
packages/editor-ui/src/shims-vue.d.ts
vendored
2
packages/editor-ui/src/shims-vue.d.ts
vendored
@ -1,5 +1,5 @@
|
||||
import Vue from 'vue';
|
||||
import 'n8n-design-system/src/shims-element-ui';
|
||||
import 'n8n-design-system/shims-element-ui';
|
||||
|
||||
declare module '*.vue' {
|
||||
import Vue from 'vue';
|
||||
|
@ -1,4 +1,4 @@
|
||||
import startCase from 'lodash.startCase';
|
||||
import { startCase } from 'lodash';
|
||||
import { defineStore } from 'pinia';
|
||||
import {
|
||||
INodePropertyCollection,
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user