refactor(editor): Apply Prettier (no-changelog) (#4920)

*  Adjust `format` script

* 🔥 Remove exemption for `editor-ui`

* 🎨 Prettify

* 👕 Fix lint
This commit is contained in:
Iván Ovejero
2022-12-14 10:04:10 +01:00
committed by GitHub
parent bcde07e032
commit 5ca2148c7e
284 changed files with 19247 additions and 15540 deletions

View File

@@ -1,5 +1,4 @@
coverage coverage
dist dist
packages/editor-ui
package.json package.json
.pnpm-lock.yml .pnpm-lock.yml

View File

@@ -19,7 +19,7 @@ module.exports = {
'import/no-default-export': 'off', 'import/no-default-export': 'off',
'import/no-extraneous-dependencies': 'off', 'import/no-extraneous-dependencies': 'off',
'import/order': 'off', 'import/order': 'off',
'indent': 'off', indent: 'off',
'prettier/prettier': 'off', 'prettier/prettier': 'off',
'@typescript-eslint/ban-types': 'off', '@typescript-eslint/ban-types': 'off',
'@typescript-eslint/dot-notation': 'off', '@typescript-eslint/dot-notation': 'off',

View File

@@ -9,43 +9,50 @@ npm install n8n -g
``` ```
## Project setup ## Project setup
``` ```
pnpm install pnpm install
``` ```
### Compiles and hot-reloads for development ### Compiles and hot-reloads for development
``` ```
pnpm serve pnpm serve
``` ```
### Compiles and minifies for production ### Compiles and minifies for production
``` ```
pnpm build pnpm build
``` ```
### Run your tests ### Run your tests
``` ```
pnpm test pnpm test
``` ```
### Lints and fixes files ### Lints and fixes files
``` ```
pnpm lint pnpm lint
``` ```
### Run your end-to-end tests ### Run your end-to-end tests
``` ```
pnpm test:e2e pnpm test:e2e
``` ```
### Run your unit tests ### Run your unit tests
``` ```
pnpm test:unit pnpm test:unit
``` ```
### Customize configuration ### Customize configuration
See [Configuration Reference](https://cli.vuejs.org/config/).
See [Configuration Reference](https://cli.vuejs.org/config/).
## License ## License

View File

@@ -1,18 +1,23 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width,initial-scale=1.0"> <meta name="viewport" content="width=device-width,initial-scale=1.0" />
<link rel="icon" href="/favicon.ico"> <link rel="icon" href="/favicon.ico" />
<script type="text/javascript">window.BASE_PATH = "/{{BASE_PATH}}/";</script> <script type="text/javascript">
<title>n8n.io - Workflow Automation</title> window.BASE_PATH = '/{{BASE_PATH}}/';
</head> </script>
<body> <title>n8n.io - Workflow Automation</title>
<noscript> </head>
<strong>We're sorry but the n8n Editor-UI doesn't work properly without JavaScript enabled. Please enable it to continue.</strong> <body>
</noscript> <noscript>
<div id="app"></div> <strong
<script type="module" src="/src/main.ts"></script> >We're sorry but the n8n Editor-UI doesn't work properly without JavaScript enabled. Please
</body> enable it to continue.</strong
>
</noscript>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html> </html>

View File

@@ -20,7 +20,7 @@
"dev": "pnpm serve", "dev": "pnpm serve",
"lint": "tslint -p tsconfig.json -c tslint.json && eslint --ext .js,.ts,.vue src", "lint": "tslint -p tsconfig.json -c tslint.json && eslint --ext .js,.ts,.vue src",
"lintfix": "tslint --fix -p tsconfig.json -c tslint.json && eslint --ext .js,.ts,.vue src --fix", "lintfix": "tslint --fix -p tsconfig.json -c tslint.json && eslint --ext .js,.ts,.vue src --fix",
"format": "prettier **/**.{ts,vue} --write", "format": "prettier --write . --ignore-path ../../.prettierignore",
"serve": "cross-env VUE_APP_URL_BASE_API=http://localhost:5678/ vite --host 0.0.0.0 --port 8080 dev", "serve": "cross-env VUE_APP_URL_BASE_API=http://localhost:5678/ vite --host 0.0.0.0 --port 8080 dev",
"test": "vitest run", "test": "vitest run",
"test:ci": "vitest run --coverage", "test:ci": "vitest run --coverage",

View File

@@ -6,7 +6,7 @@
id="app" id="app"
:class="{ :class="{
[$style.container]: true, [$style.container]: true,
[$style.sidebarCollapsed]: uiStore.sidebarMenuCollapsed [$style.sidebarCollapsed]: uiStore.sidebarMenuCollapsed,
}" }"
> >
<div id="header" :class="$style.header"> <div id="header" :class="$style.header">
@@ -47,12 +47,7 @@ import { useTemplatesStore } from './stores/templates';
import { useNodeTypesStore } from './stores/nodeTypes'; import { useNodeTypesStore } from './stores/nodeTypes';
import { historyHelper } from '@/mixins/history'; import { historyHelper } from '@/mixins/history';
export default mixins( export default mixins(showMessage, userHelpers, restApi, historyHelper).extend({
showMessage,
userHelpers,
restApi,
historyHelper,
).extend({
name: 'App', name: 'App',
components: { components: {
LoadingView, LoadingView,
@@ -68,14 +63,14 @@ export default mixins(
}, },
computed: { computed: {
...mapStores( ...mapStores(
useNodeTypesStore, useNodeTypesStore,
useRootStore, useRootStore,
useSettingsStore, useSettingsStore,
useTemplatesStore, useTemplatesStore,
useUIStore, useUIStore,
useUsersStore, useUsersStore,
), ),
defaultLocale (): string { defaultLocale(): string {
return this.rootStore.defaultLocale; return this.rootStore.defaultLocale;
}, },
}, },
@@ -110,8 +105,7 @@ export default mixins(
} }
try { try {
await this.settingsStore.testTemplatesEndpoint(); await this.settingsStore.testTemplatesEndpoint();
} catch (e) { } catch (e) {}
}
}, },
logHiringBanner() { logHiringBanner() {
if (this.settingsStore.isHiringBannerEnabled && this.$route.name !== VIEWS.DEMO) { if (this.settingsStore.isHiringBannerEnabled && this.$route.name !== VIEWS.DEMO) {
@@ -126,8 +120,7 @@ export default mixins(
this.uiStore.currentView = this.$route.name || ''; this.uiStore.currentView = this.$route.name || '';
if (this.$route && this.$route.meta && this.$route.meta.templatesEnabled) { if (this.$route && this.$route.meta && this.$route.meta.templatesEnabled) {
this.templatesStore.setSessionId(); this.templatesStore.setSessionId();
} } else {
else {
this.templatesStore.resetSessionId(); // reset telemetry session id when user leaves template pages this.templatesStore.resetSessionId(); // reset telemetry session id when user leaves template pages
} }
@@ -161,7 +154,8 @@ export default mixins(
// if cannot access page and is logged in, respect signin redirect // if cannot access page and is logged in, respect signin redirect
if (this.$route.name === VIEWS.SIGNIN && typeof this.$route.query.redirect === 'string') { if (this.$route.name === VIEWS.SIGNIN && typeof this.$route.query.redirect === 'string') {
const redirect = decodeURIComponent(this.$route.query.redirect); const redirect = decodeURIComponent(this.$route.query.redirect);
if (redirect.startsWith('/')) { // protect against phishing if (redirect.startsWith('/')) {
// protect against phishing
this.$router.replace(redirect); this.$router.replace(redirect);
return; return;
} }
@@ -171,7 +165,10 @@ export default mixins(
this.$router.replace({ name: VIEWS.HOMEPAGE }); this.$router.replace({ name: VIEWS.HOMEPAGE });
}, },
redirectIfNecessary() { redirectIfNecessary() {
const redirect = this.$route.meta && typeof this.$route.meta.getRedirect === 'function' && this.$route.meta.getRedirect(); const redirect =
this.$route.meta &&
typeof this.$route.meta.getRedirect === 'function' &&
this.$route.meta.getRedirect();
if (redirect) { if (redirect) {
this.$router.replace(redirect); this.$router.replace(redirect);
} }
@@ -221,11 +218,11 @@ export default mixins(
.container { .container {
display: grid; display: grid;
grid-template-areas: grid-template-areas:
"sidebar header" 'sidebar header'
"sidebar content"; 'sidebar content';
grid-auto-columns: fit-content($sidebar-expanded-width) 1fr; grid-auto-columns: fit-content($sidebar-expanded-width) 1fr;
grid-template-rows: fit-content($sidebar-width) 1fr; grid-template-rows: fit-content($sidebar-width) 1fr;
} }
.content { .content {

View File

@@ -9,7 +9,7 @@ import {
EndpointRectangle, EndpointRectangle,
EndpointRectangleOptions, EndpointRectangleOptions,
EndpointSpec, EndpointSpec,
} from "jsplumb"; } from 'jsplumb';
import { import {
GenericValue, GenericValue,
IConnections, IConnections,
@@ -58,10 +58,10 @@ declare module 'jsplumb' {
interface Connection { interface Connection {
__meta?: { __meta?: {
sourceNodeName: string, sourceNodeName: string;
sourceOutputIndex: number, sourceOutputIndex: number;
targetNodeName: string, targetNodeName: string;
targetOutputIndex: number, targetOutputIndex: number;
}; };
canvas?: HTMLElement; canvas?: HTMLElement;
connector?: { connector?: {
@@ -72,7 +72,7 @@ declare module 'jsplumb' {
maxX: number; maxX: number;
minY: number; minY: number;
maxY: number; maxY: number;
} };
}; };
// bind(event: string, (connection: Connection): void;): void; // tslint:disable-line:no-any // bind(event: string, (connection: Connection): void;): void; // tslint:disable-line:no-any
@@ -90,9 +90,9 @@ declare module 'jsplumb' {
endpoint: any; // tslint:disable-line:no-any endpoint: any; // tslint:disable-line:no-any
elementId: string; elementId: string;
__meta?: { __meta?: {
nodeName: string, nodeName: string;
nodeId: string, nodeId: string;
index: number, index: number;
totalEndpoints: number; totalEndpoints: number;
}; };
getUuid(): string; getUuid(): string;
@@ -120,44 +120,57 @@ declare module 'jsplumb' {
// EndpointOptions from jsplumb seems incomplete and wrong so we define an own one // EndpointOptions from jsplumb seems incomplete and wrong so we define an own one
export type IEndpointOptions = Omit<EndpointOptions, 'endpoint' | 'dragProxy'> & { export type IEndpointOptions = Omit<EndpointOptions, 'endpoint' | 'dragProxy'> & {
endpointStyle: EndpointStyle endpointStyle: EndpointStyle;
endpointHoverStyle: EndpointStyle endpointHoverStyle: EndpointStyle;
endpoint?: EndpointSpec | string endpoint?: EndpointSpec | string;
dragAllowedWhenFull?: boolean dragAllowedWhenFull?: boolean;
dropOptions?: DropOptions & { dropOptions?: DropOptions & {
tolerance: string tolerance: string;
}; };
dragProxy?: string | string[] | EndpointSpec | [ EndpointRectangle, EndpointRectangleOptions & { strokeWidth: number } ] dragProxy?:
| string
| string[]
| EndpointSpec
| [EndpointRectangle, EndpointRectangleOptions & { strokeWidth: number }];
}; };
export type EndpointStyle = { export type EndpointStyle = {
width?: number width?: number;
height?: number height?: number;
fill?: string fill?: string;
stroke?: string stroke?: string;
outlineStroke?:string outlineStroke?: string;
lineWidth?: number lineWidth?: number;
hover?: boolean hover?: boolean;
showOutputLabel?: boolean showOutputLabel?: boolean;
size?: string size?: string;
hoverMessage?: string hoverMessage?: string;
}; };
export type IDragOptions = DragOptions & { export type IDragOptions = DragOptions & {
grid: [number, number] grid: [number, number];
filter: string filter: string;
}; };
export type IJsPlumbInstance = Omit<jsPlumbInstance, 'addEndpoint' | 'draggable'> & { export type IJsPlumbInstance = Omit<jsPlumbInstance, 'addEndpoint' | 'draggable'> & {
clearDragSelection: () => void clearDragSelection: () => void;
addEndpoint(el: ElementGroupRef, params?: IEndpointOptions, referenceParams?: IEndpointOptions): Endpoint | Endpoint[] addEndpoint(
draggable(el: {}, options?: IDragOptions): IJsPlumbInstance el: ElementGroupRef,
params?: IEndpointOptions,
referenceParams?: IEndpointOptions,
): Endpoint | Endpoint[];
draggable(el: {}, options?: IDragOptions): IJsPlumbInstance;
}; };
export interface IUpdateInformation { export interface IUpdateInformation {
name: string; name: string;
key?: string; key?: string;
value: string | number | { [key: string]: string | number | boolean } | NodeParameterValueType | INodeParameters; // with null makes problems in NodeSettings.vue value:
| string
| number
| { [key: string]: string | number | boolean }
| NodeParameterValueType
| INodeParameters; // with null makes problems in NodeSettings.vue
node?: string; node?: string;
oldValue?: string | number; oldValue?: string | number;
} }
@@ -197,9 +210,14 @@ export interface IExternalHooks {
*/ */
export interface IRestApi { export interface IRestApi {
getActiveWorkflows(): Promise<string[]>; getActiveWorkflows(): Promise<string[]>;
getActivationError(id: string): Promise<IActivationError | undefined >; getActivationError(id: string): Promise<IActivationError | undefined>;
getCurrentExecutions(filter: object): Promise<IExecutionsCurrentSummaryExtended[]>; getCurrentExecutions(filter: object): Promise<IExecutionsCurrentSummaryExtended[]>;
getPastExecutions(filter: object, limit: number, lastId?: string | number, firstId?: string | number): Promise<IExecutionsListResponse>; getPastExecutions(
filter: object,
limit: number,
lastId?: string | number,
firstId?: string | number,
): Promise<IExecutionsListResponse>;
stopCurrentExecution(executionId: string): Promise<IExecutionsStopData>; stopCurrentExecution(executionId: string): Promise<IExecutionsStopData>;
makeRestApiRequest(method: string, endpoint: string, data?: any): Promise<any>; // tslint:disable-line:no-any makeRestApiRequest(method: string, endpoint: string, data?: any): Promise<any>; // tslint:disable-line:no-any
getCredentialTranslation(credentialType: string): Promise<object>; getCredentialTranslation(credentialType: string): Promise<object>;
@@ -224,7 +242,7 @@ export interface INodeTranslationHeaders {
[key: string]: { [key: string]: {
displayName: string; displayName: string;
description: string; description: string;
}, };
}; };
} }
@@ -239,7 +257,7 @@ export interface IStartRunData {
export interface ITableData { export interface ITableData {
columns: string[]; columns: string[];
data: GenericValue[][]; data: GenericValue[][];
hasJson: {[key: string]: boolean}; hasJson: { [key: string]: boolean };
} }
export interface IVariableItemSelected { export interface IVariableItemSelected {
@@ -336,7 +354,6 @@ export interface IWorkflowsShareResponse {
ownedBy?: Partial<IUser>; ownedBy?: Partial<IUser>;
} }
// Identical or almost identical to cli.Interfaces.ts // Identical or almost identical to cli.Interfaces.ts
export interface IActivationError { export interface IActivationError {
@@ -368,7 +385,7 @@ export interface ICredentialsBase {
updatedAt: number | string; updatedAt: number | string;
} }
export interface ICredentialsDecryptedResponse extends ICredentialsBase, ICredentialsDecrypted{ export interface ICredentialsDecryptedResponse extends ICredentialsBase, ICredentialsDecrypted {
id: string; id: string;
} }
@@ -598,7 +615,10 @@ export type IPersonalizationSurveyAnswersV3 = {
export type IPersonalizationLatestVersion = IPersonalizationSurveyAnswersV3; export type IPersonalizationLatestVersion = IPersonalizationSurveyAnswersV3;
export type IPersonalizationSurveyVersions = IPersonalizationSurveyAnswersV1 | IPersonalizationSurveyAnswersV2 | IPersonalizationSurveyAnswersV3; export type IPersonalizationSurveyVersions =
| IPersonalizationSurveyAnswersV1
| IPersonalizationSurveyAnswersV2
| IPersonalizationSurveyAnswersV3;
export type IRole = 'default' | 'owner' | 'member'; export type IRole = 'default' | 'owner' | 'member';
@@ -679,7 +699,7 @@ export interface ITemplatesCollection {
id: number; id: number;
name: string; name: string;
nodes: ITemplatesNode[]; nodes: ITemplatesNode[];
workflows: Array<{id: number}>; workflows: Array<{ id: number }>;
} }
interface ITemplatesImage { interface ITemplatesImage {
@@ -861,7 +881,11 @@ export interface ActionCreateElement extends CreateElementBase {
properties: IActionItemProps; properties: IActionItemProps;
} }
export type INodeCreateElement = NodeCreateElement | CategoryCreateElement | SubcategoryCreateElement | ActionCreateElement; export type INodeCreateElement =
| NodeCreateElement
| CategoryCreateElement
| SubcategoryCreateElement
| ActionCreateElement;
export interface ICategoriesWithNodes { export interface ICategoriesWithNodes {
[category: string]: { [category: string]: {
@@ -946,7 +970,7 @@ export interface WorkflowsState {
usedCredentials: Record<string, IUsedCredential>; usedCredentials: Record<string, IUsedCredential>;
workflow: IWorkflowDb; workflow: IWorkflowDb;
workflowExecutionData: IExecutionResponse | null; workflowExecutionData: IExecutionResponse | null;
workflowExecutionPairedItemMappings: {[itemId: string]: Set<string>}; workflowExecutionPairedItemMappings: { [itemId: string]: Set<string> };
workflowsById: IWorkflowsMap; workflowsById: IWorkflowsMap;
} }
@@ -998,7 +1022,7 @@ export interface IRootState {
oauthCallbackUrls: object; oauthCallbackUrls: object;
n8nMetadata: object; n8nMetadata: object;
workflowExecutionData: IExecutionResponse | null; workflowExecutionData: IExecutionResponse | null;
workflowExecutionPairedItemMappings: {[itemId: string]: Set<string>}; workflowExecutionPairedItemMappings: { [itemId: string]: Set<string> };
lastSelectedNode: string | null; lastSelectedNode: string | null;
lastSelectedNodeOutputIndex: number | null; lastSelectedNodeOutputIndex: number | null;
nodeViewOffsetPosition: XYPosition; nodeViewOffsetPosition: XYPosition;
@@ -1065,7 +1089,7 @@ export interface TargetItem {
export interface NDVState { export interface NDVState {
activeNodeName: string | null; activeNodeName: string | null;
mainPanelDimensions: {[key: string]: {[key: string]: number}}; mainPanelDimensions: { [key: string]: { [key: string]: number } };
sessionId: string; sessionId: string;
input: { input: {
displayMode: IRunDataDisplayMode; displayMode: IRunDataDisplayMode;
@@ -1074,21 +1098,21 @@ export interface NDVState {
branch?: number; branch?: number;
data: { data: {
isEmpty: boolean; isEmpty: boolean;
} };
}; };
output: { output: {
branch?: number; branch?: number;
displayMode: IRunDataDisplayMode; displayMode: IRunDataDisplayMode;
data: { data: {
isEmpty: boolean; isEmpty: boolean;
} };
editMode: { editMode: {
enabled: boolean; enabled: boolean;
value: string; value: string;
}; };
}; };
focusedMappableInput: string; focusedMappableInput: string;
mappingTelemetry: {[key: string]: string | number | boolean}; mappingTelemetry: { [key: string]: string | number | boolean };
hoveringItem: null | TargetItem; hoveringItem: null | TargetItem;
draggable: { draggable: {
isDragging: boolean; isDragging: boolean;
@@ -1099,7 +1123,6 @@ export interface NDVState {
}; };
} }
export interface IUiState { export interface IUiState {
sidebarMenuCollapsed: boolean; sidebarMenuCollapsed: boolean;
modalStack: string[]; modalStack: string[];
@@ -1149,20 +1172,20 @@ export interface UIState {
export type ILogLevel = 'info' | 'debug' | 'warn' | 'error' | 'verbose'; export type ILogLevel = 'info' | 'debug' | 'warn' | 'error' | 'verbose';
export type IFakeDoor = { export type IFakeDoor = {
id: FAKE_DOOR_FEATURES, id: FAKE_DOOR_FEATURES;
featureName: string, featureName: string;
icon?: string, icon?: string;
infoText?: string, infoText?: string;
actionBoxTitle: string, actionBoxTitle: string;
actionBoxDescription: string, actionBoxDescription: string;
actionBoxButtonLabel?: string, actionBoxButtonLabel?: string;
linkURL: string, linkURL: string;
uiLocations: IFakeDoorLocation[], uiLocations: IFakeDoorLocation[];
}; };
export type IFakeDoorLocation = 'settings' | 'credentialsModal' | 'workflowShareModal'; export type IFakeDoorLocation = 'settings' | 'credentialsModal' | 'workflowShareModal';
export type INodeFilterType = "Regular" | "Trigger" | "All"; export type INodeFilterType = 'Regular' | 'Trigger' | 'All';
export interface INodeCreatorState { export interface INodeCreatorState {
itemsFilter: string; itemsFilter: string;
@@ -1191,25 +1214,25 @@ export interface INodeTypesState {
nodeTypes: { nodeTypes: {
[nodeType: string]: { [nodeType: string]: {
[version: number]: INodeTypeDescription; [version: number]: INodeTypeDescription;
} };
}; };
} }
export interface ITemplateState { export interface ITemplateState {
categories: {[id: string]: ITemplatesCategory}; categories: { [id: string]: ITemplatesCategory };
collections: {[id: string]: ITemplatesCollection}; collections: { [id: string]: ITemplatesCollection };
workflows: {[id: string]: ITemplatesWorkflow}; workflows: { [id: string]: ITemplatesWorkflow };
workflowSearches: { workflowSearches: {
[search: string]: { [search: string]: {
workflowIds: string[]; workflowIds: string[];
totalWorkflows: number; totalWorkflows: number;
loadingMore?: boolean; loadingMore?: boolean;
} };
}; };
collectionSearches: { collectionSearches: {
[search: string]: { [search: string]: {
collectionIds: string[]; collectionIds: string[];
} };
}; };
currentSessionId: string; currentSessionId: string;
previousSessionId: string; previousSessionId: string;
@@ -1223,7 +1246,7 @@ export interface IVersionsState {
export interface IUsersState { export interface IUsersState {
currentUserId: null | string; currentUserId: null | string;
users: {[userId: string]: IUser}; users: { [userId: string]: IUser };
} }
export interface IWorkflowsState { export interface IWorkflowsState {
@@ -1231,7 +1254,7 @@ export interface IWorkflowsState {
activeWorkflowExecution: IExecutionsSummary | null; activeWorkflowExecution: IExecutionsSummary | null;
finishedExecutionsCount: number; finishedExecutionsCount: number;
} }
export interface IWorkflowsMap { export interface IWorkflowsMap {
[name: string]: IWorkflowDb; [name: string]: IWorkflowDb;
} }
@@ -1308,14 +1331,14 @@ export interface IResourceLocatorResultExpanded extends INodeListSearchItems {
} }
export interface CurlToJSONResponse { export interface CurlToJSONResponse {
"parameters.url": string; 'parameters.url': string;
"parameters.authentication": string; 'parameters.authentication': string;
"parameters.method": string; 'parameters.method': string;
"parameters.sendHeaders": boolean; 'parameters.sendHeaders': boolean;
"parameters.headerParameters.parameters.0.name": string; 'parameters.headerParameters.parameters.0.name': string;
"parameters.headerParameters.parameters.0.value": string; 'parameters.headerParameters.parameters.0.value': string;
"parameters.sendQuery": boolean; 'parameters.sendQuery': boolean;
"parameters.sendBody": boolean; 'parameters.sendBody': boolean;
} }
export interface HistoryState { export interface HistoryState {
@@ -1340,4 +1363,4 @@ export type SchemaType =
| 'function' | 'function'
| 'null' | 'null'
| 'undefined'; | 'undefined';
export type Schema = { type: SchemaType, key?: string, value: string | Schema[], path: string }; export type Schema = { type: SchemaType; key?: string; value: string | Schema[]; path: string };

View File

@@ -1,11 +1,11 @@
import { parsePermissionsTable } from '@/permissions'; import { parsePermissionsTable } from '@/permissions';
import { IUser } from "@/Interface"; import { IUser } from '@/Interface';
describe('parsePermissionsTable()', () => { describe('parsePermissionsTable()', () => {
const user: IUser = { const user: IUser = {
id: "1", id: '1',
firstName: "John", firstName: 'John',
lastName: "Doe", lastName: 'Doe',
isDefaultUser: false, isDefaultUser: false,
isOwner: true, isOwner: true,
isPending: false, isPending: false,

View File

@@ -11,4 +11,3 @@ Vue.config.devtools = false;
// [Vue warn]: Failed to mount component: template or render function not defined. // [Vue warn]: Failed to mount component: template or render function not defined.
Vue.component('vue-json-pretty', require('vue-json-pretty').default); Vue.component('vue-json-pretty', require('vue-json-pretty').default);
Vue.use((vue) => I18nPlugin(vue)); Vue.use((vue) => I18nPlugin(vue));

View File

@@ -1,5 +1,5 @@
import {IRestApiContext} from "@/Interface"; import { IRestApiContext } from '@/Interface';
import {makeRestApiRequest} from "@/utils"; import { makeRestApiRequest } from '@/utils';
export function getApiKey(context: IRestApiContext): Promise<{ apiKey: string | null }> { export function getApiKey(context: IRestApiContext): Promise<{ apiKey: string | null }> {
return makeRestApiRequest(context, 'GET', '/me/api-key'); return makeRestApiRequest(context, 'GET', '/me/api-key');

View File

@@ -2,12 +2,17 @@ import { IRestApiContext } from '@/Interface';
import { PublicInstalledPackage } from 'n8n-workflow'; import { PublicInstalledPackage } from 'n8n-workflow';
import { get, post, makeRestApiRequest } from '@/utils'; import { get, post, makeRestApiRequest } from '@/utils';
export async function getInstalledCommunityNodes(context: IRestApiContext): Promise<PublicInstalledPackage[]> { export async function getInstalledCommunityNodes(
context: IRestApiContext,
): Promise<PublicInstalledPackage[]> {
const response = await get(context.baseUrl, '/nodes'); const response = await get(context.baseUrl, '/nodes');
return response.data || []; return response.data || [];
} }
export async function installNewPackage(context: IRestApiContext, name: string): Promise<PublicInstalledPackage> { export async function installNewPackage(
context: IRestApiContext,
name: string,
): Promise<PublicInstalledPackage> {
return await post(context.baseUrl, '/nodes', { name }); return await post(context.baseUrl, '/nodes', { name });
} }
@@ -15,6 +20,9 @@ export async function uninstallPackage(context: IRestApiContext, name: string):
return await makeRestApiRequest(context, 'DELETE', '/nodes', { name }); return await makeRestApiRequest(context, 'DELETE', '/nodes', { name });
} }
export async function updatePackage(context: IRestApiContext, name: string): Promise<PublicInstalledPackage> { export async function updatePackage(
context: IRestApiContext,
name: string,
): Promise<PublicInstalledPackage> {
return await makeRestApiRequest(context, 'PATCH', '/nodes', { name }); return await makeRestApiRequest(context, 'PATCH', '/nodes', { name });
} }

View File

@@ -1,13 +1,16 @@
import { import { ICredentialsResponse, IRestApiContext, IShareCredentialsPayload } from '@/Interface';
ICredentialsResponse,
IRestApiContext,
IShareCredentialsPayload,
} from '@/Interface';
import { makeRestApiRequest } from '@/utils'; import { makeRestApiRequest } from '@/utils';
import { import { IDataObject } from 'n8n-workflow';
IDataObject,
} from 'n8n-workflow';
export async function setCredentialSharedWith(context: IRestApiContext, id: string, data: IShareCredentialsPayload): Promise<ICredentialsResponse> { export async function setCredentialSharedWith(
return makeRestApiRequest(context, 'PUT', `/credentials/${id}/share`, data as unknown as IDataObject); context: IRestApiContext,
id: string,
data: IShareCredentialsPayload,
): Promise<ICredentialsResponse> {
return makeRestApiRequest(
context,
'PUT',
`/credentials/${id}/share`,
data as unknown as IDataObject,
);
} }

View File

@@ -14,7 +14,10 @@ export async function getCredentialTypes(baseUrl: string): Promise<ICredentialTy
return data; return data;
} }
export async function getCredentialsNewName(context: IRestApiContext, name?: string): Promise<{name: string}> { export async function getCredentialsNewName(
context: IRestApiContext,
name?: string,
): Promise<{ name: string }> {
return await makeRestApiRequest(context, 'GET', '/credentials/new', name ? { name } : {}); return await makeRestApiRequest(context, 'GET', '/credentials/new', name ? { name } : {});
} }
@@ -22,7 +25,10 @@ export async function getAllCredentials(context: IRestApiContext): Promise<ICred
return await makeRestApiRequest(context, 'GET', '/credentials'); return await makeRestApiRequest(context, 'GET', '/credentials');
} }
export async function createNewCredential(context: IRestApiContext, data: ICredentialsDecrypted): Promise<ICredentialsResponse> { export async function createNewCredential(
context: IRestApiContext,
data: ICredentialsDecrypted,
): Promise<ICredentialsResponse> {
return makeRestApiRequest(context, 'POST', `/credentials`, data as unknown as IDataObject); return makeRestApiRequest(context, 'POST', `/credentials`, data as unknown as IDataObject);
} }
@@ -30,26 +36,52 @@ export async function deleteCredential(context: IRestApiContext, id: string): Pr
return makeRestApiRequest(context, 'DELETE', `/credentials/${id}`); return makeRestApiRequest(context, 'DELETE', `/credentials/${id}`);
} }
export async function updateCredential(context: IRestApiContext, id: string, data: ICredentialsDecrypted): Promise<ICredentialsResponse> { export async function updateCredential(
context: IRestApiContext,
id: string,
data: ICredentialsDecrypted,
): Promise<ICredentialsResponse> {
return makeRestApiRequest(context, 'PATCH', `/credentials/${id}`, data as unknown as IDataObject); return makeRestApiRequest(context, 'PATCH', `/credentials/${id}`, data as unknown as IDataObject);
} }
export async function getCredentialData(context: IRestApiContext, id: string): Promise<ICredentialsDecryptedResponse | ICredentialsResponse | undefined> { export async function getCredentialData(
context: IRestApiContext,
id: string,
): Promise<ICredentialsDecryptedResponse | ICredentialsResponse | undefined> {
return makeRestApiRequest(context, 'GET', `/credentials/${id}`, { return makeRestApiRequest(context, 'GET', `/credentials/${id}`, {
includeData: true, includeData: true,
}); });
} }
// Get OAuth1 Authorization URL using the stored credentials // Get OAuth1 Authorization URL using the stored credentials
export async function oAuth1CredentialAuthorize(context: IRestApiContext, data: ICredentialsResponse): Promise<string> { export async function oAuth1CredentialAuthorize(
return makeRestApiRequest(context, 'GET', `/oauth1-credential/auth`, data as unknown as IDataObject); context: IRestApiContext,
data: ICredentialsResponse,
): Promise<string> {
return makeRestApiRequest(
context,
'GET',
`/oauth1-credential/auth`,
data as unknown as IDataObject,
);
} }
// Get OAuth2 Authorization URL using the stored credentials // Get OAuth2 Authorization URL using the stored credentials
export async function oAuth2CredentialAuthorize(context: IRestApiContext, data: ICredentialsResponse): Promise<string> { export async function oAuth2CredentialAuthorize(
return makeRestApiRequest(context, 'GET', `/oauth2-credential/auth`, data as unknown as IDataObject); context: IRestApiContext,
data: ICredentialsResponse,
): Promise<string> {
return makeRestApiRequest(
context,
'GET',
`/oauth2-credential/auth`,
data as unknown as IDataObject,
);
} }
export async function testCredential(context: IRestApiContext, data: INodeCredentialTestRequest): Promise<INodeCredentialTestResult> { export async function testCredential(
context: IRestApiContext,
data: INodeCredentialTestRequest,
): Promise<INodeCredentialTestResult> {
return makeRestApiRequest(context, 'POST', '/credentials/test', data as unknown as IDataObject); return makeRestApiRequest(context, 'POST', '/credentials/test', data as unknown as IDataObject);
} }

View File

@@ -1,6 +1,9 @@
import {CurlToJSONResponse, IRestApiContext} from "@/Interface"; import { CurlToJSONResponse, IRestApiContext } from '@/Interface';
import {makeRestApiRequest} from "@/utils"; import { makeRestApiRequest } from '@/utils';
export function getCurlToJson(context: IRestApiContext, curlCommand: string): Promise<CurlToJSONResponse> { export function getCurlToJson(
context: IRestApiContext,
curlCommand: string,
): Promise<CurlToJSONResponse> {
return makeRestApiRequest(context, 'POST', '/curl-to-json', { curlCommand }); return makeRestApiRequest(context, 'POST', '/curl-to-json', { curlCommand });
} }

View File

@@ -37,12 +37,12 @@ export async function getNodesInformation(
export async function getNodeParameterOptions( export async function getNodeParameterOptions(
context: IRestApiContext, context: IRestApiContext,
sendData: { sendData: {
nodeTypeAndVersion: INodeTypeNameVersion, nodeTypeAndVersion: INodeTypeNameVersion;
path: string, path: string;
methodName?: string, methodName?: string;
loadOptions?: ILoadOptions, loadOptions?: ILoadOptions;
currentNodeParameters: INodeParameters, currentNodeParameters: INodeParameters;
credentials?: INodeCredentials, credentials?: INodeCredentials;
}, },
): Promise<INodePropertyOptions[]> { ): Promise<INodePropertyOptions[]> {
return makeRestApiRequest(context, 'GET', '/node-parameter-options', sendData); return makeRestApiRequest(context, 'GET', '/node-parameter-options', sendData);
@@ -52,5 +52,10 @@ export async function getResourceLocatorResults(
context: IRestApiContext, context: IRestApiContext,
sendData: IResourceLocatorReqParams, sendData: IResourceLocatorReqParams,
): Promise<INodeListSearchResult> { ): Promise<INodeListSearchResult> {
return makeRestApiRequest(context, 'GET', '/nodes-list-search', sendData as unknown as IDataObject); return makeRestApiRequest(
context,
'GET',
'/nodes-list-search',
sendData as unknown as IDataObject,
);
} }

View File

@@ -1,25 +1,55 @@
import { IRestApiContext, IN8nPrompts, IN8nValueSurveyData, IN8nUISettings, IN8nPromptResponse } from '../Interface'; import {
IRestApiContext,
IN8nPrompts,
IN8nValueSurveyData,
IN8nUISettings,
IN8nPromptResponse,
} from '../Interface';
import { makeRestApiRequest, get, post } from '@/utils'; import { makeRestApiRequest, get, post } from '@/utils';
import { N8N_IO_BASE_URL, NPM_COMMUNITY_NODE_SEARCH_API_URL } from '@/constants'; import { N8N_IO_BASE_URL, NPM_COMMUNITY_NODE_SEARCH_API_URL } from '@/constants';
export function getSettings(context: IRestApiContext): Promise<IN8nUISettings> { export function getSettings(context: IRestApiContext): Promise<IN8nUISettings> {
return makeRestApiRequest(context, 'GET', '/settings'); return makeRestApiRequest(context, 'GET', '/settings');
} }
export async function getPromptsData(instanceId: string, userId: string): Promise<IN8nPrompts> { export async function getPromptsData(instanceId: string, userId: string): Promise<IN8nPrompts> {
return await get(N8N_IO_BASE_URL, '/prompts', {}, {'n8n-instance-id': instanceId, 'n8n-user-id': userId}); return await get(
N8N_IO_BASE_URL,
'/prompts',
{},
{ 'n8n-instance-id': instanceId, 'n8n-user-id': userId },
);
} }
export async function submitContactInfo(instanceId: string, userId: string, email: string): Promise<IN8nPromptResponse> { export async function submitContactInfo(
return await post(N8N_IO_BASE_URL, '/prompt', { email }, {'n8n-instance-id': instanceId, 'n8n-user-id': userId}); instanceId: string,
userId: string,
email: string,
): Promise<IN8nPromptResponse> {
return await post(
N8N_IO_BASE_URL,
'/prompt',
{ email },
{ 'n8n-instance-id': instanceId, 'n8n-user-id': userId },
);
} }
export async function submitValueSurvey(instanceId: string, userId: string, params: IN8nValueSurveyData): Promise<IN8nPromptResponse> { export async function submitValueSurvey(
return await post(N8N_IO_BASE_URL, '/value-survey', params, {'n8n-instance-id': instanceId, 'n8n-user-id': userId}); instanceId: string,
userId: string,
params: IN8nValueSurveyData,
): Promise<IN8nPromptResponse> {
return await post(N8N_IO_BASE_URL, '/value-survey', params, {
'n8n-instance-id': instanceId,
'n8n-user-id': userId,
});
} }
export async function getAvailableCommunityPackageCount(): Promise<number> { export async function getAvailableCommunityPackageCount(): Promise<number> {
const response = await get(NPM_COMMUNITY_NODE_SEARCH_API_URL, 'search?q=keywords:n8n-community-node-package'); const response = await get(
NPM_COMMUNITY_NODE_SEARCH_API_URL,
'search?q=keywords:n8n-community-node-package',
);
return response.total || 0; return response.total || 0;
} }

View File

@@ -9,7 +9,11 @@ export async function createTag(context: IRestApiContext, params: { name: string
return await makeRestApiRequest(context, 'POST', '/tags', params); return await makeRestApiRequest(context, 'POST', '/tags', params);
} }
export async function updateTag(context: IRestApiContext, id: string, params: { name: string }): Promise<ITag> { export async function updateTag(
context: IRestApiContext,
id: string,
params: { name: string },
): Promise<ITag> {
return await makeRestApiRequest(context, 'PATCH', `/tags/${id}`, params); return await makeRestApiRequest(context, 'PATCH', `/tags/${id}`, params);
} }

View File

@@ -1,4 +1,12 @@
import { ITemplatesCategory, ITemplatesCollection, ITemplatesQuery, ITemplatesWorkflow, ITemplatesCollectionResponse, ITemplatesWorkflowResponse, IWorkflowTemplate } from '@/Interface'; import {
ITemplatesCategory,
ITemplatesCollection,
ITemplatesQuery,
ITemplatesWorkflow,
ITemplatesCollectionResponse,
ITemplatesWorkflowResponse,
IWorkflowTemplate,
} from '@/Interface';
import { IDataObject } from 'n8n-workflow'; import { IDataObject } from 'n8n-workflow';
import { get } from '@/utils'; import { get } from '@/utils';
@@ -10,30 +18,64 @@ export function testHealthEndpoint(apiEndpoint: string) {
return get(apiEndpoint, '/health'); return get(apiEndpoint, '/health');
} }
export function getCategories(apiEndpoint: string, headers?: IDataObject): Promise<{categories: ITemplatesCategory[]}> { export function getCategories(
apiEndpoint: string,
headers?: IDataObject,
): Promise<{ categories: ITemplatesCategory[] }> {
return get(apiEndpoint, '/templates/categories', undefined, headers); return get(apiEndpoint, '/templates/categories', undefined, headers);
} }
export async function getCollections(apiEndpoint: string, query: ITemplatesQuery, headers?: IDataObject): Promise<{collections: ITemplatesCollection[]}> { export async function getCollections(
return await get(apiEndpoint, '/templates/collections', {category: stringifyArray(query.categories || []), search: query.search}, headers); apiEndpoint: string,
query: ITemplatesQuery,
headers?: IDataObject,
): Promise<{ collections: ITemplatesCollection[] }> {
return await get(
apiEndpoint,
'/templates/collections',
{ category: stringifyArray(query.categories || []), search: query.search },
headers,
);
} }
export async function getWorkflows( export async function getWorkflows(
apiEndpoint: string, apiEndpoint: string,
query: {skip: number, limit: number, categories: number[], search: string}, query: { skip: number; limit: number; categories: number[]; search: string },
headers?: IDataObject, headers?: IDataObject,
): Promise<{totalWorkflows: number, workflows: ITemplatesWorkflow[]}> { ): Promise<{ totalWorkflows: number; workflows: ITemplatesWorkflow[] }> {
return get(apiEndpoint, '/templates/workflows', {skip: query.skip, rows: query.limit, category: stringifyArray(query.categories), search: query.search}, headers); return get(
apiEndpoint,
'/templates/workflows',
{
skip: query.skip,
rows: query.limit,
category: stringifyArray(query.categories),
search: query.search,
},
headers,
);
} }
export async function getCollectionById(apiEndpoint: string, collectionId: string, headers?: IDataObject): Promise<{collection: ITemplatesCollectionResponse}> { export async function getCollectionById(
apiEndpoint: string,
collectionId: string,
headers?: IDataObject,
): Promise<{ collection: ITemplatesCollectionResponse }> {
return await get(apiEndpoint, `/templates/collections/${collectionId}`, undefined, headers); return await get(apiEndpoint, `/templates/collections/${collectionId}`, undefined, headers);
} }
export async function getTemplateById(apiEndpoint: string, templateId: string, headers?: IDataObject): Promise<{workflow: ITemplatesWorkflowResponse}> { export async function getTemplateById(
apiEndpoint: string,
templateId: string,
headers?: IDataObject,
): Promise<{ workflow: ITemplatesWorkflowResponse }> {
return await get(apiEndpoint, `/templates/workflows/${templateId}`, undefined, headers); return await get(apiEndpoint, `/templates/workflows/${templateId}`, undefined, headers);
} }
export async function getWorkflowTemplate(apiEndpoint: string, templateId: string, headers?: IDataObject): Promise<IWorkflowTemplate> { export async function getWorkflowTemplate(
apiEndpoint: string,
templateId: string,
headers?: IDataObject,
): Promise<IWorkflowTemplate> {
return await get(apiEndpoint, `/workflows/templates/${templateId}`, undefined, headers); return await get(apiEndpoint, `/workflows/templates/${templateId}`, undefined, headers);
} }

View File

@@ -1,4 +1,9 @@
import { IInviteResponse, IPersonalizationLatestVersion, IRestApiContext, IUserResponse } from '@/Interface'; import {
IInviteResponse,
IPersonalizationLatestVersion,
IRestApiContext,
IUserResponse,
} from '@/Interface';
import { IDataObject } from 'n8n-workflow'; import { IDataObject } from 'n8n-workflow';
import { makeRestApiRequest } from '@/utils'; import { makeRestApiRequest } from '@/utils';
@@ -10,7 +15,10 @@ export function getCurrentUser(context: IRestApiContext): Promise<IUserResponse
return makeRestApiRequest(context, 'GET', '/me'); return makeRestApiRequest(context, 'GET', '/me');
} }
export function login(context: IRestApiContext, params: {email: string, password: string}): Promise<IUserResponse> { export function login(
context: IRestApiContext,
params: { email: string; password: string },
): Promise<IUserResponse> {
return makeRestApiRequest(context, 'POST', '/login', params); return makeRestApiRequest(context, 'POST', '/login', params);
} }
@@ -18,7 +26,10 @@ export async function logout(context: IRestApiContext): Promise<void> {
await makeRestApiRequest(context, 'POST', '/logout'); await makeRestApiRequest(context, 'POST', '/logout');
} }
export function setupOwner(context: IRestApiContext, params: { firstName: string; lastName: string; email: string; password: string;}): Promise<IUserResponse> { export function setupOwner(
context: IRestApiContext,
params: { firstName: string; lastName: string; email: string; password: string },
): Promise<IUserResponse> {
return makeRestApiRequest(context, 'POST', '/owner', params as unknown as IDataObject); return makeRestApiRequest(context, 'POST', '/owner', params as unknown as IDataObject);
} }
@@ -26,36 +37,71 @@ export function skipOwnerSetup(context: IRestApiContext): Promise<void> {
return makeRestApiRequest(context, 'POST', '/owner/skip-setup'); return makeRestApiRequest(context, 'POST', '/owner/skip-setup');
} }
export function validateSignupToken(context: IRestApiContext, params: {inviterId: string; inviteeId: string}): Promise<{inviter: {firstName: string, lastName: string}}> { export function validateSignupToken(
context: IRestApiContext,
params: { inviterId: string; inviteeId: string },
): Promise<{ inviter: { firstName: string; lastName: string } }> {
return makeRestApiRequest(context, 'GET', '/resolve-signup-token', params); return makeRestApiRequest(context, 'GET', '/resolve-signup-token', params);
} }
export function signup(context: IRestApiContext, params: {inviterId: string; inviteeId: string; firstName: string; lastName: string; password: string}): Promise<IUserResponse> { export function signup(
context: IRestApiContext,
params: {
inviterId: string;
inviteeId: string;
firstName: string;
lastName: string;
password: string;
},
): Promise<IUserResponse> {
const { inviteeId, ...props } = params; const { inviteeId, ...props } = params;
return makeRestApiRequest(context, 'POST', `/users/${params.inviteeId}`, props as unknown as IDataObject); return makeRestApiRequest(
context,
'POST',
`/users/${params.inviteeId}`,
props as unknown as IDataObject,
);
} }
export async function sendForgotPasswordEmail(context: IRestApiContext, params: {email: string}): Promise<void> { export async function sendForgotPasswordEmail(
context: IRestApiContext,
params: { email: string },
): Promise<void> {
await makeRestApiRequest(context, 'POST', '/forgot-password', params); await makeRestApiRequest(context, 'POST', '/forgot-password', params);
} }
export async function validatePasswordToken(context: IRestApiContext, params: {token: string, userId: string}): Promise<void> { export async function validatePasswordToken(
context: IRestApiContext,
params: { token: string; userId: string },
): Promise<void> {
await makeRestApiRequest(context, 'GET', '/resolve-password-token', params); await makeRestApiRequest(context, 'GET', '/resolve-password-token', params);
} }
export async function changePassword(context: IRestApiContext, params: {token: string, password: string, userId: string}): Promise<void> { export async function changePassword(
context: IRestApiContext,
params: { token: string; password: string; userId: string },
): Promise<void> {
await makeRestApiRequest(context, 'POST', '/change-password', params); await makeRestApiRequest(context, 'POST', '/change-password', params);
} }
export function updateCurrentUser(context: IRestApiContext, params: {id: string, firstName: string, lastName: string, email: string}): Promise<IUserResponse> { export function updateCurrentUser(
context: IRestApiContext,
params: { id: string; firstName: string; lastName: string; email: string },
): Promise<IUserResponse> {
return makeRestApiRequest(context, 'PATCH', `/me`, params as unknown as IDataObject); return makeRestApiRequest(context, 'PATCH', `/me`, params as unknown as IDataObject);
} }
export function updateCurrentUserPassword(context: IRestApiContext, params: {newPassword: string, currentPassword: string}): Promise<void> { export function updateCurrentUserPassword(
context: IRestApiContext,
params: { newPassword: string; currentPassword: string },
): Promise<void> {
return makeRestApiRequest(context, 'PATCH', `/me/password`, params); return makeRestApiRequest(context, 'PATCH', `/me/password`, params);
} }
export async function deleteUser(context: IRestApiContext, {id, transferId}: {id: string, transferId?: string}): Promise<void> { export async function deleteUser(
context: IRestApiContext,
{ id, transferId }: { id: string; transferId?: string },
): Promise<void> {
await makeRestApiRequest(context, 'DELETE', `/users/${id}`, transferId ? { transferId } : {}); await makeRestApiRequest(context, 'DELETE', `/users/${id}`, transferId ? { transferId } : {});
} }
@@ -63,14 +109,20 @@ export function getUsers(context: IRestApiContext): Promise<IUserResponse[]> {
return makeRestApiRequest(context, 'GET', '/users'); return makeRestApiRequest(context, 'GET', '/users');
} }
export function inviteUsers(context: IRestApiContext, params: Array<{email: string}>): Promise<IInviteResponse[]> { export function inviteUsers(
context: IRestApiContext,
params: Array<{ email: string }>,
): Promise<IInviteResponse[]> {
return makeRestApiRequest(context, 'POST', '/users', params as unknown as IDataObject); return makeRestApiRequest(context, 'POST', '/users', params as unknown as IDataObject);
} }
export async function reinvite(context: IRestApiContext, {id}: {id: string}): Promise<void> { export async function reinvite(context: IRestApiContext, { id }: { id: string }): Promise<void> {
await makeRestApiRequest(context, 'POST', `/users/${id}/reinvite`); await makeRestApiRequest(context, 'POST', `/users/${id}/reinvite`);
} }
export async function submitPersonalizationSurvey(context: IRestApiContext, params: IPersonalizationLatestVersion): Promise<void> { export async function submitPersonalizationSurvey(
context: IRestApiContext,
params: IPersonalizationLatestVersion,
): Promise<void> {
await makeRestApiRequest(context, 'POST', '/me/survey', params as unknown as IDataObject); await makeRestApiRequest(context, 'POST', '/me/survey', params as unknown as IDataObject);
} }

View File

@@ -2,7 +2,11 @@ import { IVersion } from '@/Interface';
import { INSTANCE_ID_HEADER } from '@/constants'; import { INSTANCE_ID_HEADER } from '@/constants';
import { get } from '@/utils'; import { get } from '@/utils';
export async function getNextVersions(endpoint: string, version: string, instanceId: string): Promise<IVersion[]> { export async function getNextVersions(
const headers = {[INSTANCE_ID_HEADER as string] : instanceId}; endpoint: string,
version: string,
instanceId: string,
): Promise<IVersion[]> {
const headers = { [INSTANCE_ID_HEADER as string]: instanceId };
return await get(endpoint, version, {}, headers); return await get(endpoint, version, {}, headers);
} }

View File

@@ -1,49 +1,49 @@
import { IOnboardingCallPrompt, IOnboardingCallPromptResponse, IUser } from "@/Interface"; import { IOnboardingCallPrompt, IOnboardingCallPromptResponse, IUser } from '@/Interface';
import { get, post } from "@/utils"; import { get, post } from '@/utils';
const N8N_API_BASE_URL = 'https://api.n8n.io/api'; const N8N_API_BASE_URL = 'https://api.n8n.io/api';
const ONBOARDING_PROMPTS_ENDPOINT = '/prompts/onboarding'; const ONBOARDING_PROMPTS_ENDPOINT = '/prompts/onboarding';
const CONTACT_EMAIL_SUBMISSION_ENDPOINT = '/accounts/onboarding'; const CONTACT_EMAIL_SUBMISSION_ENDPOINT = '/accounts/onboarding';
export async function fetchNextOnboardingPrompt(instanceId: string, currentUer: IUser): Promise<IOnboardingCallPrompt> { export async function fetchNextOnboardingPrompt(
return await get( instanceId: string,
N8N_API_BASE_URL, currentUer: IUser,
ONBOARDING_PROMPTS_ENDPOINT, ): Promise<IOnboardingCallPrompt> {
{ return await get(N8N_API_BASE_URL, ONBOARDING_PROMPTS_ENDPOINT, {
instance_id: instanceId, instance_id: instanceId,
user_id: `${instanceId}#${currentUer.id}`, user_id: `${instanceId}#${currentUer.id}`,
is_owner: currentUer.isOwner, is_owner: currentUer.isOwner,
survey_results: currentUer.personalizationAnswers, survey_results: currentUer.personalizationAnswers,
}, });
);
} }
export async function applyForOnboardingCall(instanceId: string, currentUer: IUser, email: string): Promise<string> { export async function applyForOnboardingCall(
instanceId: string,
currentUer: IUser,
email: string,
): Promise<string> {
try { try {
const response = await post( const response = await post(N8N_API_BASE_URL, ONBOARDING_PROMPTS_ENDPOINT, {
N8N_API_BASE_URL, instance_id: instanceId,
ONBOARDING_PROMPTS_ENDPOINT, user_id: `${instanceId}#${currentUer.id}`,
{ email,
instance_id: instanceId, });
user_id: `${instanceId}#${currentUer.id}`,
email,
},
);
return response; return response;
} catch (e) { } catch (e) {
throw e; throw e;
} }
} }
export async function submitEmailOnSignup(instanceId: string, currentUer: IUser, email: string | undefined, agree: boolean): Promise<string> { export async function submitEmailOnSignup(
return await post( instanceId: string,
N8N_API_BASE_URL, currentUer: IUser,
CONTACT_EMAIL_SUBMISSION_ENDPOINT, email: string | undefined,
{ agree: boolean,
instance_id: instanceId, ): Promise<string> {
user_id: `${instanceId}#${currentUer.id}`, return await post(N8N_API_BASE_URL, CONTACT_EMAIL_SUBMISSION_ENDPOINT, {
email, instance_id: instanceId,
agree, user_id: `${instanceId}#${currentUer.id}`,
}, email,
); agree,
});
} }

View File

@@ -1,13 +1,16 @@
import { import { IRestApiContext, IShareWorkflowsPayload, IWorkflowsShareResponse } from '@/Interface';
IRestApiContext,
IShareWorkflowsPayload,
IWorkflowsShareResponse,
} from '@/Interface';
import { makeRestApiRequest } from '@/utils'; import { makeRestApiRequest } from '@/utils';
import { import { IDataObject } from 'n8n-workflow';
IDataObject,
} from 'n8n-workflow';
export async function setWorkflowSharedWith(context: IRestApiContext, id: string, data: IShareWorkflowsPayload): Promise<IWorkflowsShareResponse> { export async function setWorkflowSharedWith(
return makeRestApiRequest(context, 'PUT', `/workflows/${id}/share`, data as unknown as IDataObject); context: IRestApiContext,
id: string,
data: IShareWorkflowsPayload,
): Promise<IWorkflowsShareResponse> {
return makeRestApiRequest(
context,
'PUT',
`/workflows/${id}/share`,
data as unknown as IDataObject,
);
} }

View File

@@ -73,10 +73,7 @@ export default Vue.extend({
}; };
}, },
computed: { computed: {
...mapStores( ...mapStores(useRootStore, useSettingsStore),
useRootStore,
useSettingsStore,
),
}, },
methods: { methods: {
closeDialog() { closeDialog() {

View File

@@ -23,10 +23,11 @@
</div> </div>
</template> </template>
<template #footer="{ close }"> <template #footer="{ close }">
<div :class="$style.footer"> <div :class="$style.footer">
<el-checkbox :value="checked" @change="handleCheckboxChange">{{ $locale.baseText('activationModal.dontShowAgain') }}</el-checkbox> <el-checkbox :value="checked" @change="handleCheckboxChange">{{
$locale.baseText('activationModal.dontShowAgain')
}}</el-checkbox>
<n8n-button @click="close" :label="$locale.baseText('activationModal.gotIt')" /> <n8n-button @click="close" :label="$locale.baseText('activationModal.gotIt')" />
</div> </div>
</template> </template>
@@ -37,7 +38,12 @@
import Vue from 'vue'; import Vue from 'vue';
import Modal from '@/components/Modal.vue'; import Modal from '@/components/Modal.vue';
import { WORKFLOW_ACTIVE_MODAL_KEY, WORKFLOW_SETTINGS_MODAL_KEY, LOCAL_STORAGE_ACTIVATION_FLAG, VIEWS } from '../constants'; import {
WORKFLOW_ACTIVE_MODAL_KEY,
WORKFLOW_SETTINGS_MODAL_KEY,
LOCAL_STORAGE_ACTIVATION_FLAG,
VIEWS,
} from '../constants';
import { getActivatableTriggerNodes, getTriggerNodeServiceName } from '@/utils'; import { getActivatableTriggerNodes, getTriggerNodeServiceName } from '@/utils';
import { mapStores } from 'pinia'; import { mapStores } from 'pinia';
import { useUIStore } from '@/stores/ui'; import { useUIStore } from '@/stores/ui';
@@ -49,10 +55,8 @@ export default Vue.extend({
components: { components: {
Modal, Modal,
}, },
props: [ props: ['modalName'],
'modalName', data() {
],
data () {
return { return {
WORKFLOW_ACTIVE_MODAL_KEY, WORKFLOW_ACTIVE_MODAL_KEY,
checked: false, checked: false,
@@ -60,35 +64,35 @@ export default Vue.extend({
}; };
}, },
methods: { methods: {
async showExecutionsList () { async showExecutionsList() {
const activeExecution = this.workflowsStore.activeWorkflowExecution; const activeExecution = this.workflowsStore.activeWorkflowExecution;
const currentWorkflow = this.workflowsStore.workflowId; const currentWorkflow = this.workflowsStore.workflowId;
if (activeExecution) { if (activeExecution) {
this.$router.push({ this.$router
name: VIEWS.EXECUTION_PREVIEW, .push({
params: { name: currentWorkflow, executionId: activeExecution.id }, name: VIEWS.EXECUTION_PREVIEW,
}).catch(()=>{});; params: { name: currentWorkflow, executionId: activeExecution.id },
})
.catch(() => {});
} else { } else {
this.$router.push({ name: VIEWS.EXECUTION_HOME, params: { name: currentWorkflow } }).catch(() => {}); this.$router
.push({ name: VIEWS.EXECUTION_HOME, params: { name: currentWorkflow } })
.catch(() => {});
} }
this.uiStore.closeModal(WORKFLOW_ACTIVE_MODAL_KEY); this.uiStore.closeModal(WORKFLOW_ACTIVE_MODAL_KEY);
}, },
async showSettings() { async showSettings() {
this.uiStore.openModal(WORKFLOW_SETTINGS_MODAL_KEY); this.uiStore.openModal(WORKFLOW_SETTINGS_MODAL_KEY);
}, },
handleCheckboxChange (checkboxValue: boolean) { handleCheckboxChange(checkboxValue: boolean) {
this.checked = checkboxValue; this.checked = checkboxValue;
window.localStorage.setItem(LOCAL_STORAGE_ACTIVATION_FLAG, checkboxValue.toString()); window.localStorage.setItem(LOCAL_STORAGE_ACTIVATION_FLAG, checkboxValue.toString());
}, },
}, },
computed: { computed: {
...mapStores( ...mapStores(useNodeTypesStore, useUIStore, useWorkflowsStore),
useNodeTypesStore, triggerContent(): string {
useUIStore,
useWorkflowsStore,
),
triggerContent (): string {
const foundTriggers = getActivatableTriggerNodes(this.workflowsStore.workflowTriggerNodes); const foundTriggers = getActivatableTriggerNodes(this.workflowsStore.workflowTriggerNodes);
if (!foundTriggers.length) { if (!foundTriggers.length) {
return ''; return '';
@@ -101,10 +105,10 @@ export default Vue.extend({
const trigger = foundTriggers[0]; const trigger = foundTriggers[0];
const triggerNodeType = this.nodeTypesStore.getNodeType(trigger.type, trigger.typeVersion); const triggerNodeType = this.nodeTypesStore.getNodeType(trigger.type, trigger.typeVersion);
if (triggerNodeType) { if (triggerNodeType) {
if (triggerNodeType.activationMessage) { if (triggerNodeType.activationMessage) {
return triggerNodeType.activationMessage; return triggerNodeType.activationMessage;
} }
const serviceName = getTriggerNodeServiceName(triggerNodeType); const serviceName = getTriggerNodeServiceName(triggerNodeType);
if (trigger.webhookId) { if (trigger.webhookId) {
@@ -139,5 +143,4 @@ export default Vue.extend({
margin-left: var(--spacing-s); margin-left: var(--spacing-s);
} }
} }
</style> </style>

View File

@@ -1,51 +1,42 @@
<template> <template>
<fragment> <fragment>
<el-tag <el-tag v-if="type === 'danger'" type="danger" size="small" :class="$style['danger']">
v-if="type === 'danger'" {{ text }}
type="danger" </el-tag>
size="small" <el-tag v-else-if="type === 'warning'" size="small" :class="$style['warning']">
:class="$style['danger']" {{ text }}
> </el-tag>
{{ text }} </fragment>
</el-tag>
<el-tag
v-else-if="type === 'warning'"
size="small"
:class="$style['warning']"
>
{{ text }}
</el-tag>
</fragment>
</template> </template>
<script lang="ts"> <script lang="ts">
export default { export default {
props: ["text", "type"], props: ['text', 'type'],
}; };
</script> </script>
<style lang="scss" module> <style lang="scss" module>
.badge { .badge {
font-size: 11px; font-size: 11px;
line-height: 18px; line-height: 18px;
max-height: 18px; max-height: 18px;
font-weight: 400; font-weight: 400;
display: flex; display: flex;
align-items: center; align-items: center;
padding: 2px 4px; padding: 2px 4px;
} }
.danger { .danger {
composes: badge; composes: badge;
color: $badge-danger-color; color: $badge-danger-color;
background-color: $badge-danger-background-color; background-color: $badge-danger-background-color;
border-color: $badge-danger-border-color; border-color: $badge-danger-border-color;
} }
.warning { .warning {
composes: badge; composes: badge;
background-color: $badge-warning-background-color; background-color: $badge-warning-background-color;
color: $badge-warning-color; color: $badge-warning-color;
border: none; border: none;
} }
</style> </style>

View File

@@ -1,30 +1,16 @@
<template> <template>
<el-tag <el-tag :type="theme" size="medium" :disable-transitions="true" :class="$style.container">
:type="theme"
size="medium"
:disable-transitions="true"
:class="$style.container"
>
<font-awesome-icon <font-awesome-icon
:icon="theme === 'success' ? 'check-circle' : 'exclamation-triangle'" :icon="theme === 'success' ? 'check-circle' : 'exclamation-triangle'"
:class="theme === 'success' ? $style.icon : $style.dangerIcon" :class="theme === 'success' ? $style.icon : $style.dangerIcon"
/> />
<div <div :class="$style.banner">
:class="$style.banner"
>
<div :class="$style.content"> <div :class="$style.content">
<div> <div>
<span <span :class="theme === 'success' ? $style.message : $style.dangerMessage">
:class="theme === 'success' ? $style.message : $style.dangerMessage"
>
{{ message }}&nbsp; {{ message }}&nbsp;
</span> </span>
<n8n-link <n8n-link v-if="details && !expanded" :bold="true" size="small" @click="expand">
v-if="details && !expanded"
:bold="true"
size="small"
@click="expand"
>
<span :class="$style.moreDetails">More details</span> <span :class="$style.moreDetails">More details</span>
</n8n-link> </n8n-link>
</div> </div>
@@ -43,7 +29,7 @@
</div> </div>
<div v-if="expanded" :class="$style.details"> <div v-if="expanded" :class="$style.details">
{{details}} {{ details }}
</div> </div>
</el-tag> </el-tag>
</template> </template>
@@ -61,8 +47,7 @@ export default Vue.extend({
props: { props: {
theme: { theme: {
type: String, type: String,
validator: (value: string): boolean => validator: (value: string): boolean => ['success', 'danger'].indexOf(value) !== -1,
['success', 'danger'].indexOf(value) !== -1,
}, },
message: { message: {
type: String, type: String,

View File

@@ -13,9 +13,8 @@
<div v-if="!binaryData"> <div v-if="!binaryData">
{{ $locale.baseText('binaryDataDisplay.noDataFoundToDisplay') }} {{ $locale.baseText('binaryDataDisplay.noDataFoundToDisplay') }}
</div> </div>
<BinaryDataDisplayEmbed v-else :binaryData="binaryData"/> <BinaryDataDisplayEmbed v-else :binaryData="binaryData" />
</div> </div>
</div> </div>
</template> </template>
@@ -31,62 +30,62 @@ import { restApi } from '@/mixins/restApi';
import { mapStores } from 'pinia'; import { mapStores } from 'pinia';
import { useWorkflowsStore } from '@/stores/workflows'; import { useWorkflowsStore } from '@/stores/workflows';
export default mixins( export default mixins(nodeHelpers, restApi).extend({
nodeHelpers, name: 'BinaryDataDisplay',
restApi, components: {
) BinaryDataDisplayEmbed,
.extend({ },
name: 'BinaryDataDisplay', props: [
components: { 'displayData', // IBinaryData
BinaryDataDisplayEmbed, 'windowVisible', // boolean
],
computed: {
...mapStores(useWorkflowsStore),
binaryData(): IBinaryData | null {
const binaryData = this.getBinaryData(
this.workflowRunData,
this.displayData.node,
this.displayData.runIndex,
this.displayData.outputIndex,
);
if (binaryData.length === 0) {
return null;
}
if (
this.displayData.index >= binaryData.length ||
binaryData[this.displayData.index][this.displayData.key] === undefined
) {
return null;
}
const binaryDataItem: IBinaryData = binaryData[this.displayData.index][this.displayData.key];
return binaryDataItem;
}, },
props: [
'displayData', // IBinaryData
'windowVisible', // boolean
],
computed: {
...mapStores(
useWorkflowsStore,
),
binaryData (): IBinaryData | null {
const binaryData = this.getBinaryData(this.workflowRunData, this.displayData.node, this.displayData.runIndex, this.displayData.outputIndex);
if (binaryData.length === 0) {
return null;
}
if (this.displayData.index >= binaryData.length || binaryData[this.displayData.index][this.displayData.key] === undefined) {
return null;
}
const binaryDataItem: IBinaryData = binaryData[this.displayData.index][this.displayData.key];
return binaryDataItem;
},
workflowRunData (): IRunData | null {
const workflowExecution = this.workflowsStore.getWorkflowExecution;
if (workflowExecution === null) {
return null;
}
const executionData = workflowExecution.data;
return executionData? executionData.resultData.runData : null;
},
workflowRunData(): IRunData | null {
const workflowExecution = this.workflowsStore.getWorkflowExecution;
if (workflowExecution === null) {
return null;
}
const executionData = workflowExecution.data;
return executionData ? executionData.resultData.runData : null;
}, },
methods: { },
closeWindow () { methods: {
// Handle the close externally as the visible parameter is an external prop closeWindow() {
// and is so not allowed to be changed here. // Handle the close externally as the visible parameter is an external prop
this.$emit('close'); // and is so not allowed to be changed here.
return false; this.$emit('close');
}, return false;
}, },
}); },
});
</script> </script>
<style lang="scss"> <style lang="scss">
.binary-data-window { .binary-data-window {
position: absolute; position: absolute;
top: 50px; top: 50px;
@@ -103,7 +102,7 @@ export default mixins(
} }
.binary-data-window-wrapper { .binary-data-window-wrapper {
margin-top: .5em; margin-top: 0.5em;
padding: 0 1em; padding: 0 1em;
height: calc(100% - 50px); height: calc(100% - 50px);
@@ -126,7 +125,5 @@ export default mixins(
width: calc(100% - 1em); width: calc(100% - 1em);
} }
} }
} }
</style> </style>

View File

@@ -1,14 +1,10 @@
<template> <template>
<span> <span>
<div v-if="isLoading"> <div v-if="isLoading">Loading binary data...</div>
Loading binary data... <div v-else-if="error">Error loading binary data</div>
</div>
<div v-else-if="error">
Error loading binary data
</div>
<span v-else> <span v-else>
<video v-if="binaryData.fileType === 'video'" controls autoplay> <video v-if="binaryData.fileType === 'video'" controls autoplay>
<source :src="embedSource" :type="binaryData.mimeType"> <source :src="embedSource" :type="binaryData.mimeType" />
{{ $locale.baseText('binaryDataDisplay.yourBrowserDoesNotSupport') }} {{ $locale.baseText('binaryDataDisplay.yourBrowserDoesNotSupport') }}
</video> </video>
<vue-json-pretty <vue-json-pretty
@@ -17,7 +13,7 @@
:deep="3" :deep="3"
:showLength="true" :showLength="true"
/> />
<embed v-else :src="embedSource" class="binary-data" :class="embedClass()"/> <embed v-else :src="embedSource" class="binary-data" :class="embedClass()" />
</span> </span>
</span> </span>
</template> </template>
@@ -29,76 +25,71 @@ import { IBinaryData, jsonParse } from 'n8n-workflow';
import type { PropType } from 'vue'; import type { PropType } from 'vue';
import VueJsonPretty from 'vue-json-pretty'; import VueJsonPretty from 'vue-json-pretty';
export default mixins( export default mixins(restApi).extend({
restApi, name: 'BinaryDataDisplayEmbed',
) components: {
.extend({ VueJsonPretty,
name: 'BinaryDataDisplayEmbed', },
components: { props: {
VueJsonPretty, binaryData: {
type: Object as PropType<IBinaryData>,
required: true,
}, },
props: { },
binaryData: { data() {
type: Object as PropType<IBinaryData>, return {
required: true, isLoading: true,
}, embedSource: '',
}, error: false,
data() { jsonData: '',
return { };
isLoading: true, },
embedSource: '', async mounted() {
error: false, const id = this.binaryData?.id;
jsonData: '', const isJSONData = this.binaryData.fileType === 'json';
};
},
async mounted() {
const id = this.binaryData?.id;
const isJSONData = this.binaryData.fileType === 'json';
if(!id) { if (!id) {
if (isJSONData) { if (isJSONData) {
this.jsonData = jsonParse(atob(this.binaryData.data)); this.jsonData = jsonParse(atob(this.binaryData.data));
} else {
this.embedSource = 'data:' + this.binaryData.mimeType + ';base64,' + this.binaryData.data;
}
} else { } else {
try { this.embedSource = 'data:' + this.binaryData.mimeType + ';base64,' + this.binaryData.data;
const binaryUrl = this.restApi().getBinaryUrl(id);
if (isJSONData) {
this.jsonData = await (await fetch(binaryUrl)).json();
} else {
this.embedSource = binaryUrl;
}
} catch (e) {
this.error = true;
}
} }
} else {
try {
const binaryUrl = this.restApi().getBinaryUrl(id);
if (isJSONData) {
this.jsonData = await (await fetch(binaryUrl)).json();
} else {
this.embedSource = binaryUrl;
}
} catch (e) {
this.error = true;
}
}
this.isLoading = false; this.isLoading = false;
},
methods: {
embedClass(): string[] {
const { fileType } = (this.binaryData || {}) as IBinaryData;
return [fileType ?? 'other'];
}, },
methods: { },
embedClass(): string[] { });
const { fileType } = (this.binaryData || {}) as IBinaryData;
return [fileType ?? 'other'];
},
},
});
</script> </script>
<style lang="scss"> <style lang="scss">
.binary-data { .binary-data {
background-color: var(--color-foreground-xlight); background-color: var(--color-foreground-xlight);
&.image { &.image {
max-height: calc(100% - 1em); max-height: calc(100% - 1em);
max-width: calc(100% - 1em); max-width: calc(100% - 1em);
} }
&.other { &.other {
height: calc(100% - 1em); height: calc(100% - 1em);
width: calc(100% - 1em); width: calc(100% - 1em);
} }
} }
</style> </style>

View File

@@ -5,12 +5,7 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { import { BREAKPOINT_SM, BREAKPOINT_MD, BREAKPOINT_LG, BREAKPOINT_XL } from '@/constants';
BREAKPOINT_SM,
BREAKPOINT_MD,
BREAKPOINT_LG,
BREAKPOINT_XL,
} from "@/constants";
/** /**
* matching element.io https://element.eleme.io/#/en-US/component/layout#col-attributes * matching element.io https://element.eleme.io/#/en-US/component/layout#col-attributes
@@ -21,34 +16,27 @@ import {
* xl >= 1920 * xl >= 1920
*/ */
import mixins from "vue-typed-mixins"; import mixins from 'vue-typed-mixins';
import { genericHelpers } from "@/mixins/genericHelpers"; import { genericHelpers } from '@/mixins/genericHelpers';
import { debounceHelper } from "@/mixins/debounce"; import { debounceHelper } from '@/mixins/debounce';
export default mixins(genericHelpers, debounceHelper).extend({ export default mixins(genericHelpers, debounceHelper).extend({
name: "BreakpointsObserver", name: 'BreakpointsObserver',
props: [ props: ['valueXS', 'valueXL', 'valueLG', 'valueMD', 'valueSM', 'valueDefault'],
"valueXS",
"valueXL",
"valueLG",
"valueMD",
"valueSM",
"valueDefault",
],
data() { data() {
return { return {
width: window.innerWidth, width: window.innerWidth,
}; };
}, },
created() { created() {
window.addEventListener("resize", this.onResize); window.addEventListener('resize', this.onResize);
}, },
beforeDestroy() { beforeDestroy() {
window.removeEventListener("resize", this.onResize); window.removeEventListener('resize', this.onResize);
}, },
methods: { methods: {
onResize() { onResize() {
this.callDebounced("onResizeEnd", { debounceTime: 50 }); this.callDebounced('onResizeEnd', { debounceTime: 50 });
}, },
onResizeEnd() { onResizeEnd() {
this.$data.width = window.innerWidth; this.$data.width = window.innerWidth;
@@ -57,24 +45,25 @@ export default mixins(genericHelpers, debounceHelper).extend({
computed: { computed: {
bp(): string { bp(): string {
if (this.$data.width < BREAKPOINT_SM) { if (this.$data.width < BREAKPOINT_SM) {
return "XS"; return 'XS';
} }
if (this.$data.width >= BREAKPOINT_XL) { if (this.$data.width >= BREAKPOINT_XL) {
return "XL"; return 'XL';
} }
if (this.$data.width >= BREAKPOINT_LG) { if (this.$data.width >= BREAKPOINT_LG) {
return "LG"; return 'LG';
} }
if (this.$data.width >= BREAKPOINT_MD) { if (this.$data.width >= BREAKPOINT_MD) {
return "MD"; return 'MD';
} }
return "SM"; return 'SM';
}, },
value(): any | undefined { // tslint:disable-line:no-any value(): any | undefined {
// tslint:disable-line:no-any
if (this.$props.valueXS !== undefined && this.$data.width < BREAKPOINT_SM) { if (this.$props.valueXS !== undefined && this.$data.width < BREAKPOINT_SM) {
return this.$props.valueXS; return this.$props.valueXS;
} }

View File

@@ -1,14 +1,41 @@
<template> <template>
<div :class="{ [$style.zoomMenu]: true, [$style.regularZoomMenu]: !isDemo, [$style.demoZoomMenu]: isDemo }"> <div
<n8n-icon-button @click="zoomToFit" type="tertiary" size="large" :title="$locale.baseText('nodeView.zoomToFit')" :class="{
icon="expand" [$style.zoomMenu]: true,
data-test-id="zoom-to-fit" /> [$style.regularZoomMenu]: !isDemo,
<n8n-icon-button @click="zoomIn" type="tertiary" size="large" :title="$locale.baseText('nodeView.zoomIn')" [$style.demoZoomMenu]: isDemo,
icon="search-plus" /> }"
<n8n-icon-button @click="zoomOut" type="tertiary" size="large" :title="$locale.baseText('nodeView.zoomOut')" >
icon="search-minus" /> <n8n-icon-button
<n8n-icon-button v-if="nodeViewScale !== 1 && !isDemo" @click="resetZoom" type="tertiary" size="large" @click="zoomToFit"
:title="$locale.baseText('nodeView.resetZoom')" icon="undo" /> type="tertiary"
size="large"
:title="$locale.baseText('nodeView.zoomToFit')"
icon="expand"
data-test-id="zoom-to-fit"
/>
<n8n-icon-button
@click="zoomIn"
type="tertiary"
size="large"
:title="$locale.baseText('nodeView.zoomIn')"
icon="search-plus"
/>
<n8n-icon-button
@click="zoomOut"
type="tertiary"
size="large"
:title="$locale.baseText('nodeView.zoomOut')"
icon="search-minus"
/>
<n8n-icon-button
v-if="nodeViewScale !== 1 && !isDemo"
@click="resetZoom"
type="tertiary"
size="large"
:title="$locale.baseText('nodeView.resetZoom')"
icon="undo"
/>
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
@@ -17,7 +44,7 @@ import { storeToRefs } from 'pinia';
import { useCanvasStore } from '@/stores/canvas'; import { useCanvasStore } from '@/stores/canvas';
const canvasStore = useCanvasStore(); const canvasStore = useCanvasStore();
const { zoomToFit, zoomIn, zoomOut, resetZoom } = canvasStore; const { zoomToFit, zoomIn, zoomOut, resetZoom } = canvasStore;
const { nodeViewScale, isDemo } = storeToRefs(canvasStore); const { nodeViewScale, isDemo } = storeToRefs(canvasStore);
const keyDown = (e: KeyboardEvent) => { const keyDown = (e: KeyboardEvent) => {
@@ -26,9 +53,9 @@ const keyDown = (e: KeyboardEvent) => {
zoomIn(); zoomIn();
} else if ((e.key === '_' || e.key === '-') && !isCtrlKeyPressed) { } else if ((e.key === '_' || e.key === '-') && !isCtrlKeyPressed) {
zoomOut(); zoomOut();
} else if ((e.key === '0') && !isCtrlKeyPressed) { } else if (e.key === '0' && !isCtrlKeyPressed) {
resetZoom(); resetZoom();
} else if ((e.key === '1') && !isCtrlKeyPressed) { } else if (e.key === '1' && !isCtrlKeyPressed) {
zoomToFit(); zoomToFit();
} }
}; };
@@ -40,7 +67,6 @@ onBeforeMount(() => {
onBeforeUnmount(() => { onBeforeUnmount(() => {
document.removeEventListener('keydown', keyDown); document.removeEventListener('keydown', keyDown);
}); });
</script> </script>
<style lang="scss" module> <style lang="scss" module>
@@ -57,8 +83,8 @@ onBeforeUnmount(() => {
border: var(--border-base); border: var(--border-base);
} }
>* { > * {
+* { + * {
margin-left: var(--spacing-3xs); margin-left: var(--spacing-3xs);
} }

View File

@@ -17,26 +17,30 @@
/> />
</template> </template>
<template #footer> <template #footer>
<n8n-button :loading="loading" :label="$locale.baseText('auth.changePassword')" @click="onSubmitClick" float="right" /> <n8n-button
:loading="loading"
:label="$locale.baseText('auth.changePassword')"
@click="onSubmitClick"
float="right"
/>
</template> </template>
</Modal> </Modal>
</template> </template>
<script lang="ts"> <script lang="ts">
import mixins from "vue-typed-mixins"; import mixins from 'vue-typed-mixins';
import { showMessage } from "@/mixins/showMessage"; import { showMessage } from '@/mixins/showMessage';
import Modal from "./Modal.vue"; import Modal from './Modal.vue';
import Vue from "vue"; import Vue from 'vue';
import { IFormInputs } from "@/Interface"; import { IFormInputs } from '@/Interface';
import { CHANGE_PASSWORD_MODAL_KEY } from '../constants'; import { CHANGE_PASSWORD_MODAL_KEY } from '../constants';
import { mapStores } from "pinia"; import { mapStores } from 'pinia';
import { useUsersStore } from "@/stores/users"; import { useUsersStore } from '@/stores/users';
export default mixins(showMessage).extend({ export default mixins(showMessage).extend({
components: { Modal }, components: { Modal },
name: "ChangePasswordModal", name: 'ChangePasswordModal',
props: { props: {
modalName: { modalName: {
type: String, type: String,
@@ -74,7 +78,7 @@ export default mixins(showMessage).extend({
label: this.$locale.baseText('auth.newPassword'), label: this.$locale.baseText('auth.newPassword'),
type: 'password', type: 'password',
required: true, required: true,
validationRules: [{name: 'DEFAULT_PASSWORD_RULES'}], validationRules: [{ name: 'DEFAULT_PASSWORD_RULES' }],
infoText: this.$locale.baseText('auth.defaultPasswordRequirements'), infoText: this.$locale.baseText('auth.defaultPasswordRequirements'),
autocomplete: 'new-password', autocomplete: 'new-password',
capitalize: true, capitalize: true,
@@ -91,7 +95,7 @@ export default mixins(showMessage).extend({
validate: this.passwordsMatch, validate: this.passwordsMatch,
}, },
}, },
validationRules: [{name: 'TWO_PASSWORDS_MATCH'}], validationRules: [{ name: 'TWO_PASSWORDS_MATCH' }],
autocomplete: 'new-password', autocomplete: 'new-password',
capitalize: true, capitalize: true,
}, },
@@ -112,12 +116,12 @@ export default mixins(showMessage).extend({
return false; return false;
}, },
onInput(e: {name: string, value: string}) { onInput(e: { name: string; value: string }) {
if (e.name === 'password') { if (e.name === 'password') {
this.password = e.value; this.password = e.value;
} }
}, },
async onSubmit(values: {[key: string]: string}) { async onSubmit(values: { [key: string]: string }) {
try { try {
this.loading = true; this.loading = true;
await this.usersStore.updateCurrentUserPassword(values); await this.usersStore.updateCurrentUserPassword(values);
@@ -129,7 +133,6 @@ export default mixins(showMessage).extend({
}); });
this.modalBus.$emit('close'); this.modalBus.$emit('close');
} catch (error) { } catch (error) {
this.$showError(error, this.$locale.baseText('auth.changePassword.error')); this.$showError(error, this.$locale.baseText('auth.changePassword.error'));
} }
@@ -140,5 +143,4 @@ export default mixins(showMessage).extend({
}, },
}, },
}); });
</script> </script>

View File

@@ -4,11 +4,18 @@
append-to-body append-to-body
:close-on-click-modal="false" :close-on-click-modal="false"
width="80%" width="80%"
:title="`${$locale.baseText('codeEdit.edit')} ${$locale.nodeText().inputLabelDisplayName(parameter, path)}`" :title="`${$locale.baseText('codeEdit.edit')} ${$locale
.nodeText()
.inputLabelDisplayName(parameter, path)}`"
:before-close="closeDialog" :before-close="closeDialog"
> >
<div class="text-editor-wrapper ignore-key-press"> <div class="text-editor-wrapper ignore-key-press">
<code-editor :value="value" :autocomplete="loadAutocompleteData" :readonly="readonly" @input="$emit('valueChanged', $event)" /> <code-editor
:value="value"
:autocomplete="loadAutocompleteData"
:readonly="readonly"
@input="$emit('valueChanged', $event)"
/>
</div> </div>
</el-dialog> </el-dialog>
</template> </template>
@@ -28,30 +35,21 @@ import {
WorkflowDataProxy, WorkflowDataProxy,
} from 'n8n-workflow'; } from 'n8n-workflow';
import { import { PLACEHOLDER_FILLED_AT_EXECUTION_TIME } from '@/constants';
PLACEHOLDER_FILLED_AT_EXECUTION_TIME,
} from '@/constants';
import { CodeEditor } from './forms'; import { CodeEditor } from './forms';
import { mapStores } from 'pinia'; import { mapStores } from 'pinia';
import { useWorkflowsStore } from '@/stores/workflows'; import { useWorkflowsStore } from '@/stores/workflows';
import { useRootStore } from '@/stores/n8nRootStore'; import { useRootStore } from '@/stores/n8nRootStore';
import { useNDVStore } from '@/stores/ndv'; import { useNDVStore } from '@/stores/ndv';
export default mixins( export default mixins(genericHelpers, workflowHelpers).extend({
genericHelpers,
workflowHelpers,
).extend({
name: 'CodeEdit', name: 'CodeEdit',
components: { components: {
CodeEditor, CodeEditor,
}, },
props: ['codeAutocomplete', 'parameter', 'path', 'type', 'value', 'readonly'], props: ['codeAutocomplete', 'parameter', 'path', 'type', 'value', 'readonly'],
computed: { computed: {
...mapStores( ...mapStores(useNDVStore, useRootStore, useWorkflowsStore),
useNDVStore,
useRootStore,
useWorkflowsStore,
),
}, },
methods: { methods: {
loadAutocompleteData(): string[] { loadAutocompleteData(): string[] {
@@ -65,7 +63,10 @@ export default mixins(
const workflow = this.getCurrentWorkflow(); const workflow = this.getCurrentWorkflow();
const activeNode: INodeUi | null = this.ndvStore.activeNode; const activeNode: INodeUi | null = this.ndvStore.activeNode;
const parentNode = workflow.getParentNodes(activeNode!.name, inputName, 1); const parentNode = workflow.getParentNodes(activeNode!.name, inputName, 1);
const nodeConnection = workflow.getNodeConnectionIndexes(activeNode!.name, parentNode[0]) || { const nodeConnection = workflow.getNodeConnectionIndexes(
activeNode!.name,
parentNode[0],
) || {
sourceIndex: 0, sourceIndex: 0,
destinationIndex: 0, destinationIndex: 0,
}; };
@@ -86,7 +87,13 @@ export default mixins(
} }
} }
const connectionInputData = this.connectionInputData(parentNode, activeNode!.name, inputName, runIndex, nodeConnection); const connectionInputData = this.connectionInputData(
parentNode,
activeNode!.name,
inputName,
runIndex,
nodeConnection,
);
const additionalProxyKeys: IWorkflowDataProxyAdditionalKeys = { const additionalProxyKeys: IWorkflowDataProxyAdditionalKeys = {
$execution: { $execution: {
@@ -100,7 +107,18 @@ export default mixins(
$resumeWebhookUrl: PLACEHOLDER_FILLED_AT_EXECUTION_TIME, $resumeWebhookUrl: PLACEHOLDER_FILLED_AT_EXECUTION_TIME,
}; };
const dataProxy = new WorkflowDataProxy(workflow, runExecutionData, runIndex, itemIndex, activeNode!.name, connectionInputData || [], {}, mode, this.rootStore.timezone, additionalProxyKeys); const dataProxy = new WorkflowDataProxy(
workflow,
runExecutionData,
runIndex,
itemIndex,
activeNode!.name,
connectionInputData || [],
{},
mode,
this.rootStore.timezone,
additionalProxyKeys,
);
const proxy = dataProxy.getDataProxy(); const proxy = dataProxy.getDataProxy();
const autoCompleteItems = [ const autoCompleteItems = [
@@ -126,13 +144,7 @@ export default mixins(
'Interval', 'Interval',
]; ];
const functionItemKeys = [ const functionItemKeys = ['$json', '$binary', '$position', '$thisItem', '$thisItemIndex'];
'$json',
'$binary',
'$position',
'$thisItem',
'$thisItemIndex',
];
const additionalKeys: string[] = []; const additionalKeys: string[] = [];
if (this.codeAutocomplete === 'functionItem') { if (this.codeAutocomplete === 'functionItem') {
@@ -142,13 +154,15 @@ export default mixins(
if (executedWorkflow && connectionInputData && connectionInputData.length) { if (executedWorkflow && connectionInputData && connectionInputData.length) {
baseKeys.push(...additionalKeys); baseKeys.push(...additionalKeys);
} else { } else {
additionalKeys.forEach(key => { additionalKeys.forEach((key) => {
autoCompleteItems.push(`const ${key} = {}`); autoCompleteItems.push(`const ${key} = {}`);
}); });
} }
for (const key of baseKeys) { for (const key of baseKeys) {
autoCompleteItems.push(`const ${key} = ${JSON.stringify(this.createSimpleRepresentation(proxy[key]))}`); autoCompleteItems.push(
`const ${key} = ${JSON.stringify(this.createSimpleRepresentation(proxy[key]))}`,
);
} }
// Add the nodes and their simplified data // Add the nodes and their simplified data
@@ -159,24 +173,36 @@ export default mixins(
// To not load to much data create a simple representation. // To not load to much data create a simple representation.
nodes[nodeName] = { nodes[nodeName] = {
json: {} as IDataObject, json: {} as IDataObject,
parameter: this.createSimpleRepresentation(proxy.$node[nodeName].parameter) as IDataObject, parameter: this.createSimpleRepresentation(
proxy.$node[nodeName].parameter,
) as IDataObject,
}; };
try { try {
nodes[nodeName]!.json = this.createSimpleRepresentation(proxy.$node[nodeName].json) as IDataObject; nodes[nodeName]!.json = this.createSimpleRepresentation(
nodes[nodeName]!.context = this.createSimpleRepresentation(proxy.$node[nodeName].context) as IDataObject; proxy.$node[nodeName].json,
) as IDataObject;
nodes[nodeName]!.context = this.createSimpleRepresentation(
proxy.$node[nodeName].context,
) as IDataObject;
nodes[nodeName]!.runIndex = proxy.$node[nodeName].runIndex; nodes[nodeName]!.runIndex = proxy.$node[nodeName].runIndex;
if (Object.keys(proxy.$node[nodeName].binary).length) { if (Object.keys(proxy.$node[nodeName].binary).length) {
nodes[nodeName]!.binary = this.createSimpleRepresentation(proxy.$node[nodeName].binary) as IBinaryKeyData; nodes[nodeName]!.binary = this.createSimpleRepresentation(
proxy.$node[nodeName].binary,
) as IBinaryKeyData;
} }
} catch(error) {} } catch (error) {}
} }
autoCompleteItems.push(`const $node = ${JSON.stringify(nodes)}`); autoCompleteItems.push(`const $node = ${JSON.stringify(nodes)}`);
autoCompleteItems.push(`function $jmespath(jsonDoc: object, query: string): {};`); autoCompleteItems.push(`function $jmespath(jsonDoc: object, query: string): {};`);
if (this.codeAutocomplete === 'function') { if (this.codeAutocomplete === 'function') {
if (connectionInputData) { if (connectionInputData) {
autoCompleteItems.push(`const items = ${JSON.stringify(this.createSimpleRepresentation(connectionInputData))}`); autoCompleteItems.push(
`const items = ${JSON.stringify(
this.createSimpleRepresentation(connectionInputData),
)}`,
);
} else { } else {
autoCompleteItems.push(`const items: {json: {[key: string]: any}}[] = []`); autoCompleteItems.push(`const items: {json: {[key: string]: any}}[] = []`);
} }
@@ -200,7 +226,29 @@ export default mixins(
return false; return false;
}, },
createSimpleRepresentation(inputData: object | null | undefined | boolean | string | number | boolean[] | string[] | number[] | object[]): object | null | undefined | boolean | string | number | boolean[] | string[] | number[] | object[] { createSimpleRepresentation(
inputData:
| object
| null
| undefined
| boolean
| string
| number
| boolean[]
| string[]
| number[]
| object[],
):
| object
| null
| undefined
| boolean
| string
| number
| boolean[]
| string[]
| number[]
| object[] {
if (inputData === null || inputData === undefined) { if (inputData === null || inputData === undefined) {
return inputData; return inputData;
} else if (typeof inputData === 'string') { } else if (typeof inputData === 'string') {
@@ -210,10 +258,10 @@ export default mixins(
} else if (typeof inputData === 'number') { } else if (typeof inputData === 'number') {
return 1; return 1;
} else if (Array.isArray(inputData)) { } else if (Array.isArray(inputData)) {
return inputData.map(value => this.createSimpleRepresentation(value)); return inputData.map((value) => this.createSimpleRepresentation(value));
} else if (typeof inputData === 'object') { } else if (typeof inputData === 'object') {
const returnData: { [key: string]: object } = {}; const returnData: { [key: string]: object } = {};
Object.keys(inputData).forEach(key => { Object.keys(inputData).forEach((key) => {
// @ts-ignore // @ts-ignore
returnData[key] = this.createSimpleRepresentation(inputData[key]); returnData[key] = this.createSimpleRepresentation(inputData[key]);
}); });

View File

@@ -49,9 +49,7 @@ export default mixins(linterExtension, completerExtension, workflowHelpers).exte
}, },
}, },
computed: { computed: {
...mapStores( ...mapStores(useRootStore),
useRootStore,
),
content(): string { content(): string {
if (!this.editor) return ''; if (!this.editor) return '';

View File

@@ -9,7 +9,12 @@ import {
} from '@codemirror/view'; } from '@codemirror/view';
import { bracketMatching, foldGutter, indentOnInput } from '@codemirror/language'; import { bracketMatching, foldGutter, indentOnInput } from '@codemirror/language';
import { acceptCompletion, closeBrackets } from '@codemirror/autocomplete'; import { acceptCompletion, closeBrackets } from '@codemirror/autocomplete';
import { history, indentWithTab, insertNewlineAndIndent, toggleComment } from '@codemirror/commands'; import {
history,
indentWithTab,
insertNewlineAndIndent,
toggleComment,
} from '@codemirror/commands';
import { lintGutter } from '@codemirror/lint'; import { lintGutter } from '@codemirror/lint';
import type { Extension } from '@codemirror/state'; import type { Extension } from '@codemirror/state';

View File

@@ -1,5 +1,5 @@
import { snippets } from '@codemirror/lang-javascript'; import { snippets } from '@codemirror/lang-javascript';
import { completeFromList, snippetCompletion } from "@codemirror/autocomplete"; import { completeFromList, snippetCompletion } from '@codemirror/autocomplete';
/** /**
* https://github.com/codemirror/lang-javascript/blob/main/src/snippets.ts * https://github.com/codemirror/lang-javascript/blob/main/src/snippets.ts

View File

@@ -9,10 +9,7 @@ import { useNDVStore } from '@/stores/ndv';
export const jsonFieldCompletions = (Vue as CodeNodeEditorMixin).extend({ export const jsonFieldCompletions = (Vue as CodeNodeEditorMixin).extend({
computed: { computed: {
...mapStores( ...mapStores(useNDVStore, useWorkflowsStore),
useNDVStore,
useWorkflowsStore,
),
}, },
methods: { methods: {
/** /**

View File

@@ -23,13 +23,15 @@ export const luxonCompletions = (Vue as CodeNodeEditorMixin).extend({
}; };
}); });
options.push(...this.luxonInstanceMethods().map(([method, description]) => { options.push(
return { ...this.luxonInstanceMethods().map(([method, description]) => {
label: `${matcher}.${method}()`, return {
type: 'function', label: `${matcher}.${method}()`,
info: description, type: 'function',
}; info: description,
})); };
}),
);
return { return {
from: preCursor.from, from: preCursor.from,
@@ -55,13 +57,15 @@ export const luxonCompletions = (Vue as CodeNodeEditorMixin).extend({
}; };
}); });
options.push(...this.luxonInstanceMethods().map(([method, description]) => { options.push(
return { ...this.luxonInstanceMethods().map(([method, description]) => {
label: `${matcher}.${method}()`, return {
type: 'function', label: `${matcher}.${method}()`,
info: description, type: 'function',
}; info: description,
})); };
}),
);
return { return {
from: preCursor.from, from: preCursor.from,

View File

@@ -75,34 +75,47 @@ export const CODE_NODE_EDITOR_THEME = [
cursor: BASE_STYLING.diagnosticButton.cursor, cursor: BASE_STYLING.diagnosticButton.cursor,
}, },
}), }),
syntaxHighlighting(HighlightStyle.define([ syntaxHighlighting(
{ HighlightStyle.define([
tag: tags.comment, {
color: 'var(--color-code-tags-comment)', tag: tags.comment,
}, color: 'var(--color-code-tags-comment)',
{ },
tag: [tags.string, tags.special(tags.brace)], {
color: 'var(--color-code-tags-string)', tag: [tags.string, tags.special(tags.brace)],
}, color: 'var(--color-code-tags-string)',
{ },
tag: [tags.number, tags.self, tags.bool, tags.null], {
color: 'var(--color-code-tags-primitive)', tag: [tags.number, tags.self, tags.bool, tags.null],
}, color: 'var(--color-code-tags-primitive)',
{ },
tag: tags.keyword, {
color: 'var(--color-code-tags-keyword)', tag: tags.keyword,
}, color: 'var(--color-code-tags-keyword)',
{ },
tag: tags.operator, {
color: 'var(--color-code-tags-operator)', tag: tags.operator,
}, color: 'var(--color-code-tags-operator)',
{ },
tag: [tags.variableName, tags.propertyName, tags.attributeName, tags.regexp, tags.className, tags.typeName], {
color: 'var(--color-code-tags-variable)', tag: [
}, tags.variableName,
{ tags.propertyName,
tag: [tags.definition(tags.typeName), tags.definition(tags.propertyName), tags.function(tags.variableName)], tags.attributeName,
color: 'var(--color-code-tags-definition)', tags.regexp,
}, tags.className,
])), tags.typeName,
],
color: 'var(--color-code-tags-variable)',
},
{
tag: [
tags.definition(tags.typeName),
tags.definition(tags.propertyName),
tags.function(tags.variableName),
],
color: 'var(--color-code-tags-definition)',
},
]),
),
]; ];

View File

@@ -1,9 +1,5 @@
<template> <template>
<Card <Card :loading="loading" :title="collection.name" @click="onClick">
:loading="loading"
:title="collection.name"
@click="onClick"
>
<template #footer> <template #footer>
<n8n-text size="small" color="text-light"> <n8n-text size="small" color="text-light">
{{ collection.workflows.length }} {{ collection.workflows.length }}
@@ -42,6 +38,4 @@ export default mixins(genericHelpers).extend({
}); });
</script> </script>
<style lang="scss" module> <style lang="scss" module></style>
</style>

View File

@@ -5,7 +5,15 @@
<n8n-text size="small">{{ $locale.baseText('collectionParameter.noProperties') }}</n8n-text> <n8n-text size="small">{{ $locale.baseText('collectionParameter.noProperties') }}</n8n-text>
</div> </div>
<parameter-input-list :parameters="getProperties" :nodeValues="nodeValues" :path="path" :hideDelete="hideDelete" :indent="true" :isReadOnly="isReadOnly" @valueChanged="valueChanged" /> <parameter-input-list
:parameters="getProperties"
:nodeValues="nodeValues"
:path="path"
:hideDelete="hideDelete"
:indent="true"
:isReadOnly="isReadOnly"
@valueChanged="valueChanged"
/>
<div v-if="parameterOptions.length > 0 && !isReadOnly" class="param-options"> <div v-if="parameterOptions.length > 0 && !isReadOnly" class="param-options">
<n8n-button <n8n-button
@@ -16,183 +24,180 @@
:label="getPlaceholderText" :label="getPlaceholderText"
/> />
<div v-else class="add-option"> <div v-else class="add-option">
<n8n-select v-model="selectedOption" :placeholder="getPlaceholderText" size="small" @change="optionSelected" filterable> <n8n-select
v-model="selectedOption"
:placeholder="getPlaceholderText"
size="small"
@change="optionSelected"
filterable
>
<n8n-option <n8n-option
v-for="item in parameterOptions" v-for="item in parameterOptions"
:key="item.name" :key="item.name"
:label="$locale.nodeText().collectionOptionDisplayName(parameter, item, path)" :label="$locale.nodeText().collectionOptionDisplayName(parameter, item, path)"
:value="item.name"> :value="item.name"
>
</n8n-option> </n8n-option>
</n8n-select> </n8n-select>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts">
import { import { INodeUi, IUpdateInformation } from '@/Interface';
INodeUi,
IUpdateInformation,
} from '@/Interface';
import { import { deepCopy, INodeProperties, INodePropertyOptions } from 'n8n-workflow';
deepCopy,
INodeProperties,
INodePropertyOptions,
} from 'n8n-workflow';
import { nodeHelpers } from '@/mixins/nodeHelpers'; import { nodeHelpers } from '@/mixins/nodeHelpers';
import { get } from 'lodash'; import { get } from 'lodash';
import mixins from 'vue-typed-mixins'; import mixins from 'vue-typed-mixins';
import {Component} from "vue"; import { Component } from 'vue';
import { mapStores } from 'pinia'; import { mapStores } from 'pinia';
import { useNDVStore } from '@/stores/ndv'; import { useNDVStore } from '@/stores/ndv';
export default mixins( export default mixins(nodeHelpers).extend({
nodeHelpers, name: 'CollectionParameter',
) props: [
.extend({ 'hideDelete', // boolean
name: 'CollectionParameter', 'nodeValues', // NodeParameters
props: [ 'parameter', // INodeProperties
'hideDelete', // boolean 'path', // string
'nodeValues', // NodeParameters 'values', // NodeParameters
'parameter', // INodeProperties 'isReadOnly', // boolean
'path', // string ],
'values', // NodeParameters components: {
'isReadOnly', // boolean ParameterInputList: () => import('./ParameterInputList.vue') as Promise<Component>,
], },
components: { data() {
ParameterInputList: () => import('./ParameterInputList.vue') as Promise<Component>, return {
selectedOption: undefined,
};
},
computed: {
...mapStores(useNDVStore),
getPlaceholderText(): string {
const placeholder = this.$locale.nodeText().placeholder(this.parameter, this.path);
return placeholder ? placeholder : this.$locale.baseText('collectionParameter.choose');
}, },
data () { getProperties(): INodeProperties[] {
return { const returnProperties = [];
selectedOption: undefined, let tempProperties;
}; for (const name of this.propertyNames) {
}, tempProperties = this.getOptionProperties(name);
computed: { if (tempProperties !== undefined) {
...mapStores( returnProperties.push(...tempProperties);
useNDVStore,
),
getPlaceholderText (): string {
const placeholder = this.$locale.nodeText().placeholder(this.parameter, this.path);
return placeholder ? placeholder : this.$locale.baseText('collectionParameter.choose');
},
getProperties (): INodeProperties[] {
const returnProperties = [];
let tempProperties;
for (const name of this.propertyNames) {
tempProperties = this.getOptionProperties(name);
if (tempProperties !== undefined) {
returnProperties.push(...tempProperties);
}
} }
return returnProperties; }
}, return returnProperties;
// Returns all the options which should be displayed },
filteredOptions (): Array<INodePropertyOptions | INodeProperties> { // Returns all the options which should be displayed
return (this.parameter.options as Array<INodePropertyOptions | INodeProperties>).filter((option) => { filteredOptions(): Array<INodePropertyOptions | INodeProperties> {
return (this.parameter.options as Array<INodePropertyOptions | INodeProperties>).filter(
(option) => {
return this.displayNodeParameter(option as INodeProperties); return this.displayNodeParameter(option as INodeProperties);
}); },
}, );
node (): INodeUi | null { },
return this.ndvStore.activeNode; node(): INodeUi | null {
}, return this.ndvStore.activeNode;
// Returns all the options which did not get added already },
parameterOptions (): Array<INodePropertyOptions | INodeProperties> { // Returns all the options which did not get added already
return (this.filteredOptions as Array<INodePropertyOptions | INodeProperties>).filter((option) => { parameterOptions(): Array<INodePropertyOptions | INodeProperties> {
return (this.filteredOptions as Array<INodePropertyOptions | INodeProperties>).filter(
(option) => {
return !this.propertyNames.includes(option.name); return !this.propertyNames.includes(option.name);
}); },
}, );
propertyNames (): string[] {
if (this.values) {
return Object.keys(this.values);
}
return [];
},
}, },
methods: { propertyNames(): string[] {
getArgument (argumentName: string): string | number | boolean | undefined { if (this.values) {
if (this.parameter.typeOptions === undefined) { return Object.keys(this.values);
return undefined; }
return [];
},
},
methods: {
getArgument(argumentName: string): string | number | boolean | undefined {
if (this.parameter.typeOptions === undefined) {
return undefined;
}
if (this.parameter.typeOptions[argumentName] === undefined) {
return undefined;
}
return this.parameter.typeOptions[argumentName];
},
getOptionProperties(optionName: string): INodeProperties[] {
const properties: INodeProperties[] = [];
for (const option of this.parameter.options) {
if (option.name === optionName) {
properties.push(option);
} }
}
if (this.parameter.typeOptions[argumentName] === undefined) { return properties;
return undefined; },
} displayNodeParameter(parameter: INodeProperties) {
if (parameter.displayOptions === undefined) {
// If it is not defined no need to do a proper check
return true;
}
return this.displayParameter(this.nodeValues, parameter, this.path, this.node);
},
optionSelected(optionName: string) {
const options = this.getOptionProperties(optionName);
if (options.length === 0) {
return;
}
return this.parameter.typeOptions[argumentName]; const option = options[0];
}, const name = `${this.path}.${option.name}`;
getOptionProperties (optionName: string): INodeProperties[] {
const properties: INodeProperties[] = [];
for (const option of this.parameter.options) {
if (option.name === optionName) {
properties.push(option);
}
}
return properties; let parameterData;
},
displayNodeParameter (parameter: INodeProperties) {
if (parameter.displayOptions === undefined) {
// If it is not defined no need to do a proper check
return true;
}
return this.displayParameter(this.nodeValues, parameter, this.path, this.node);
},
optionSelected (optionName: string) {
const options = this.getOptionProperties(optionName);
if (options.length === 0) {
return;
}
const option = options[0]; if (option.typeOptions !== undefined && option.typeOptions.multipleValues === true) {
const name = `${this.path}.${option.name}`; // Multiple values are allowed
let parameterData; let newValue;
if (option.type === 'fixedCollection') {
if (option.typeOptions !== undefined && option.typeOptions.multipleValues === true) { // The "fixedCollection" entries are different as they save values
// Multiple values are allowed // in an object and then underneath there is an array. So initialize
// them differently.
let newValue; newValue = get(this.nodeValues, `${this.path}.${optionName}`, {});
if (option.type === 'fixedCollection') {
// The "fixedCollection" entries are different as they save values
// in an object and then underneath there is an array. So initialize
// them differently.
newValue = get(this.nodeValues, `${this.path}.${optionName}`, {});
} else {
// Everything else saves them directly as an array.
newValue = get(this.nodeValues, `${this.path}.${optionName}`, []);
newValue.push(deepCopy(option.default));
}
parameterData = {
name,
value: newValue,
};
} else { } else {
// Add a new option // Everything else saves them directly as an array.
parameterData = { newValue = get(this.nodeValues, `${this.path}.${optionName}`, []);
name, newValue.push(deepCopy(option.default));
value: deepCopy(option.default),
};
} }
this.$emit('valueChanged', parameterData); parameterData = {
this.selectedOption = undefined; name,
}, value: newValue,
valueChanged (parameterData: IUpdateInformation) { };
this.$emit('valueChanged', parameterData); } else {
}, // Add a new option
parameterData = {
name,
value: deepCopy(option.default),
};
}
this.$emit('valueChanged', parameterData);
this.selectedOption = undefined;
}, },
}); valueChanged(parameterData: IUpdateInformation) {
this.$emit('valueChanged', parameterData);
},
},
});
</script> </script>
<style lang="scss"> <style lang="scss">
.collection-parameter { .collection-parameter {
padding-left: var(--spacing-s); padding-left: var(--spacing-s);
@@ -212,5 +217,4 @@ export default mixins(
padding: 0.25em 0 0.25em 1em; padding: 0.25em 0 0.25em 1em;
} }
} }
</style> </style>

View File

@@ -1,13 +1,7 @@
<template> <template>
<n8n-card <n8n-card :class="$style.card" v-on="$listeners">
:class="$style.card"
v-on="$listeners"
>
<template #header v-if="!loading"> <template #header v-if="!loading">
<span <span v-text="title" :class="$style.title" />
v-text="title"
:class="$style.title"
/>
</template> </template>
<n8n-loading :loading="loading" :rows="3" variant="p" /> <n8n-loading :loading="loading" :rows="3" variant="p" />
<template #footer v-if="!loading"> <template #footer v-if="!loading">
@@ -45,7 +39,7 @@ export default mixins(genericHelpers).extend({
} }
&:hover { &:hover {
box-shadow: 0 2px 4px rgba(68,28,23,0.07); box-shadow: 0 2px 4px rgba(68, 28, 23, 0.07);
} }
> div { > div {

View File

@@ -1,9 +1,16 @@
<template> <template>
<div :class="$style.container" v-show="loading || collections.length"> <div :class="$style.container" v-show="loading || collections.length">
<agile ref="slider" :dots="false" :navButtons="false" :infinite="false" :slides-to-show="4" @after-change="updateCarouselScroll"> <agile
<Card v-for="n in (loading ? 4: 0)" :key="`loading-${n}`" :loading="loading" /> ref="slider"
:dots="false"
:navButtons="false"
:infinite="false"
:slides-to-show="4"
@after-change="updateCarouselScroll"
>
<Card v-for="n in loading ? 4 : 0" :key="`loading-${n}`" :loading="loading" />
<CollectionCard <CollectionCard
v-for="collection in (loading? []: collections)" v-for="collection in loading ? [] : collections"
:key="collection.id" :key="collection.id"
:collection="collection" :collection="collection"
@click="(e) => onCardClick(e, collection.id)" @click="(e) => onCardClick(e, collection.id)"
@@ -19,8 +26,8 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { PropType } from "vue"; import { PropType } from 'vue';
import { ITemplatesCollection } from "@/Interface"; import { ITemplatesCollection } from '@/Interface';
import Card from '@/components/CollectionWorkflowCard.vue'; import Card from '@/components/CollectionWorkflowCard.vue';
import CollectionCard from '@/components/CollectionCard.vue'; import CollectionCard from '@/components/CollectionCard.vue';
import VueAgile from 'vue-agile'; import VueAgile from 'vue-agile';
@@ -75,7 +82,7 @@ export default mixins(genericHelpers).extend({
} }
}, },
onCardClick(event: MouseEvent, id: string) { onCardClick(event: MouseEvent, id: string) {
this.$emit('openCollection', {event, id}); this.$emit('openCollection', { event, id });
}, },
scrollLeft() { scrollLeft() {
if (this.listElement) { if (this.listElement) {
@@ -144,9 +151,21 @@ export default mixins(genericHelpers).extend({
&:after { &:after {
left: 27px; left: 27px;
background: linear-gradient(270deg, background: linear-gradient(
hsla(var(--color-background-light-h), var(--color-background-light-s), var(--color-background-light-l), 50%), 270deg,
hsla(var(--color-background-light-h), var(--color-background-light-s), var(--color-background-light-l), 100%)); hsla(
var(--color-background-light-h),
var(--color-background-light-s),
var(--color-background-light-l),
50%
),
hsla(
var(--color-background-light-h),
var(--color-background-light-s),
var(--color-background-light-l),
100%
)
);
} }
} }
@@ -155,9 +174,21 @@ export default mixins(genericHelpers).extend({
right: -30px; right: -30px;
&:after { &:after {
right: 27px; right: 27px;
background: linear-gradient(90deg, background: linear-gradient(
hsla(var(--color-background-light-h), var(--color-background-light-s), var(--color-background-light-l), 50%), 90deg,
hsla(var(--color-background-light-h), var(--color-background-light-s), var(--color-background-light-l), 100%)); hsla(
var(--color-background-light-h),
var(--color-background-light-s),
var(--color-background-light-l),
50%
),
hsla(
var(--color-background-light-h),
var(--color-background-light-s),
var(--color-background-light-l),
100%
)
);
} }
} }
</style> </style>

View File

@@ -19,7 +19,8 @@
</n8n-text> </n8n-text>
<n8n-text size="small" color="text-light"> <n8n-text size="small" color="text-light">
<span v-for="(node, index) in communityPackage.installedNodes" :key="node.name"> <span v-for="(node, index) in communityPackage.installedNodes" :key="node.name">
{{ node.name }}<span v-if="index != communityPackage.installedNodes.length - 1">,</span> {{ node.name
}}<span v-if="index != communityPackage.installedNodes.length - 1">,</span>
</span> </span>
</n8n-text> </n8n-text>
</div> </div>
@@ -42,7 +43,7 @@
{{ $locale.baseText('settings.communityNodes.updateAvailable.tooltip') }} {{ $locale.baseText('settings.communityNodes.updateAvailable.tooltip') }}
</div> </div>
</template> </template>
<n8n-button type="outline" label="Update" @click="onUpdateClick"/> <n8n-button type="outline" label="Update" @click="onUpdateClick" />
</n8n-tooltip> </n8n-tooltip>
<n8n-tooltip v-else placement="top"> <n8n-tooltip v-else placement="top">
<template #content> <template #content>
@@ -65,15 +66,10 @@ import { useUIStore } from '@/stores/ui';
import { PublicInstalledPackage } from 'n8n-workflow'; import { PublicInstalledPackage } from 'n8n-workflow';
import { mapStores } from 'pinia'; import { mapStores } from 'pinia';
import mixins from 'vue-typed-mixins'; import mixins from 'vue-typed-mixins';
import { import { NPM_PACKAGE_DOCS_BASE_URL, COMMUNITY_PACKAGE_MANAGE_ACTIONS } from '../constants';
NPM_PACKAGE_DOCS_BASE_URL,
COMMUNITY_PACKAGE_MANAGE_ACTIONS,
} from '../constants';
import { showMessage } from '@/mixins/showMessage'; import { showMessage } from '@/mixins/showMessage';
export default mixins( export default mixins(showMessage).extend({
showMessage,
).extend({
name: 'CommunityPackageCard', name: 'CommunityPackageCard',
props: { props: {
communityPackage: { communityPackage: {
@@ -135,7 +131,8 @@ export default mixins(
background-color: var(--color-background-xlight); background-color: var(--color-background-xlight);
} }
.packageCard, .cardSkeleton { .packageCard,
.cardSkeleton {
display: flex; display: flex;
flex-basis: 100%; flex-basis: 100%;
justify-content: space-between; justify-content: space-between;

View File

@@ -13,11 +13,9 @@
<div> <div>
<n8n-text> <n8n-text>
{{ $locale.baseText('settings.communityNodes.installModal.description') }} {{ $locale.baseText('settings.communityNodes.installModal.description') }}
</n8n-text> <n8n-link </n8n-text>
:to="COMMUNITY_NODES_INSTALLATION_DOCS_URL" <n8n-link :to="COMMUNITY_NODES_INSTALLATION_DOCS_URL" @click="onMoreInfoTopClick">
@click="onMoreInfoTopClick" {{ $locale.baseText('_reusableDynamicText.moreInfo') }}
>
{{ $locale.baseText('_reusableDynamicText.moreInfo') }}
</n8n-link> </n8n-link>
</div> </div>
<n8n-button <n8n-button
@@ -31,16 +29,20 @@
<n8n-input-label <n8n-input-label
:class="$style.labelTooltip" :class="$style.labelTooltip"
:label="$locale.baseText('settings.communityNodes.installModal.packageName.label')" :label="$locale.baseText('settings.communityNodes.installModal.packageName.label')"
:tooltipText="$locale.baseText('settings.communityNodes.installModal.packageName.tooltip', :tooltipText="
{ interpolate: { npmURL: NPM_KEYWORD_SEARCH_URL } } $locale.baseText('settings.communityNodes.installModal.packageName.tooltip', {
)" interpolate: { npmURL: NPM_KEYWORD_SEARCH_URL },
})
"
> >
<n8n-input <n8n-input
name="packageNameInput" name="packageNameInput"
v-model="packageName" v-model="packageName"
type="text" type="text"
:maxlength="214" :maxlength="214"
:placeholder="$locale.baseText('settings.communityNodes.installModal.packageName.placeholder')" :placeholder="
$locale.baseText('settings.communityNodes.installModal.packageName.placeholder')
"
:required="true" :required="true"
:disabled="loading" :disabled="loading"
@blur="onInputBlur" @blur="onInputBlur"
@@ -60,9 +62,11 @@
@change="onCheckboxChecked" @change="onCheckboxChecked"
> >
<n8n-text> <n8n-text>
{{ $locale.baseText('settings.communityNodes.installModal.checkbox.label') }} {{ $locale.baseText('settings.communityNodes.installModal.checkbox.label') }} </n8n-text
</n8n-text><br /> ><br />
<n8n-link :to="COMMUNITY_NODES_RISKS_DOCS_URL" @click="onLearnMoreLinkClick">{{ $locale.baseText('_reusableDynamicText.moreInfo') }}</n8n-link> <n8n-link :to="COMMUNITY_NODES_RISKS_DOCS_URL" @click="onLearnMoreLinkClick">{{
$locale.baseText('_reusableDynamicText.moreInfo')
}}</n8n-link>
</el-checkbox> </el-checkbox>
</div> </div>
</template> </template>
@@ -70,9 +74,11 @@
<n8n-button <n8n-button
:loading="loading" :loading="loading"
:disabled="packageName === '' || loading" :disabled="packageName === '' || loading"
:label="loading ? :label="
$locale.baseText('settings.communityNodes.installModal.installButton.label.loading') : loading
$locale.baseText('settings.communityNodes.installModal.installButton.label')" ? $locale.baseText('settings.communityNodes.installModal.installButton.label.loading')
: $locale.baseText('settings.communityNodes.installModal.installButton.label')
"
size="large" size="large"
float="right" float="right"
@click="onInstallClick" @click="onInstallClick"
@@ -95,9 +101,7 @@ import { showMessage } from '@/mixins/showMessage';
import { mapStores } from 'pinia'; import { mapStores } from 'pinia';
import { useCommunityNodesStore } from '@/stores/communityNodes'; import { useCommunityNodesStore } from '@/stores/communityNodes';
export default mixins( export default mixins(showMessage).extend({
showMessage,
).extend({
name: 'CommunityPackageInstallModal', name: 'CommunityPackageInstallModal',
components: { components: {
Modal, Modal,
@@ -129,7 +133,10 @@ export default mixins(
this.checkboxWarning = true; this.checkboxWarning = true;
} else { } else {
try { try {
this.$telemetry.track('user started cnr package install', { input_string: this.packageName, source: 'cnr settings page' }); this.$telemetry.track('user started cnr package install', {
input_string: this.packageName,
source: 'cnr settings page',
});
this.infoTextErrorMessage = ''; this.infoTextErrorMessage = '';
this.loading = true; this.loading = true;
await this.communityNodesStore.installPackage(this.packageName); await this.communityNodesStore.installPackage(this.packageName);
@@ -141,8 +148,8 @@ export default mixins(
title: this.$locale.baseText('settings.communityNodes.messages.install.success'), title: this.$locale.baseText('settings.communityNodes.messages.install.success'),
type: 'success', type: 'success',
}); });
} catch(error) { } catch (error) {
if(error.httpStatusCode && error.httpStatusCode === 400) { if (error.httpStatusCode && error.httpStatusCode === 400) {
this.infoTextErrorMessage = error.message; this.infoTextErrorMessage = error.message;
} else { } else {
this.$showError( this.$showError(
@@ -168,7 +175,9 @@ export default mixins(
this.$telemetry.track('user clicked cnr docs link', { source: 'install package modal top' }); this.$telemetry.track('user clicked cnr docs link', { source: 'install package modal top' });
}, },
onLearnMoreLinkClick() { onLearnMoreLinkClick() {
this.$telemetry.track('user clicked cnr docs link', { source: 'install package modal bottom' }); this.$telemetry.track('user clicked cnr docs link', {
source: 'install package modal bottom',
});
}, },
}, },
}); });
@@ -193,7 +202,6 @@ export default mixins(
} }
} }
.formContainer { .formContainer {
font-size: var(--font-size-2xs); font-size: var(--font-size-2xs);
font-weight: var(--font-weight-regular); font-weight: var(--font-weight-regular);

View File

@@ -10,7 +10,10 @@
> >
<template #content> <template #content>
<n8n-text>{{ getModalContent.message }}</n8n-text> <n8n-text>{{ getModalContent.message }}</n8n-text>
<div :class="$style.descriptionContainer" v-if="mode === COMMUNITY_PACKAGE_MANAGE_ACTIONS.UPDATE"> <div
:class="$style.descriptionContainer"
v-if="mode === COMMUNITY_PACKAGE_MANAGE_ACTIONS.UPDATE"
>
<n8n-info-tip theme="info" type="note" :bold="false"> <n8n-info-tip theme="info" type="note" :bold="false">
<template> <template>
<span v-text="getModalContent.description"></span> <span v-text="getModalContent.description"></span>
@@ -35,7 +38,10 @@
import Vue from 'vue'; import Vue from 'vue';
import mixins from 'vue-typed-mixins'; import mixins from 'vue-typed-mixins';
import Modal from './Modal.vue'; import Modal from './Modal.vue';
import { COMMUNITY_PACKAGE_CONFIRM_MODAL_KEY, COMMUNITY_PACKAGE_MANAGE_ACTIONS } from '../constants'; import {
COMMUNITY_PACKAGE_CONFIRM_MODAL_KEY,
COMMUNITY_PACKAGE_MANAGE_ACTIONS,
} from '../constants';
import { showMessage } from '@/mixins/showMessage'; import { showMessage } from '@/mixins/showMessage';
import { mapStores } from 'pinia'; import { mapStores } from 'pinia';
import { useCommunityNodesStore } from '@/stores/communityNodes'; import { useCommunityNodesStore } from '@/stores/communityNodes';
@@ -80,8 +86,12 @@ export default mixins(showMessage).extend({
packageName: this.activePackageName, packageName: this.activePackageName,
}, },
}), }),
buttonLabel: this.$locale.baseText('settings.communityNodes.confirmModal.uninstall.buttonLabel'), buttonLabel: this.$locale.baseText(
buttonLoadingLabel: this.$locale.baseText('settings.communityNodes.confirmModal.uninstall.buttonLoadingLabel'), 'settings.communityNodes.confirmModal.uninstall.buttonLabel',
),
buttonLoadingLabel: this.$locale.baseText(
'settings.communityNodes.confirmModal.uninstall.buttonLoadingLabel',
),
}; };
} }
return { return {
@@ -90,15 +100,21 @@ export default mixins(showMessage).extend({
packageName: this.activePackageName, packageName: this.activePackageName,
}, },
}), }),
description: this.$locale.baseText('settings.communityNodes.confirmModal.update.description'), description: this.$locale.baseText(
'settings.communityNodes.confirmModal.update.description',
),
message: this.$locale.baseText('settings.communityNodes.confirmModal.update.message', { message: this.$locale.baseText('settings.communityNodes.confirmModal.update.message', {
interpolate: { interpolate: {
packageName: this.activePackageName, packageName: this.activePackageName,
version: this.activePackage.updateAvailable, version: this.activePackage.updateAvailable,
}, },
}), }),
buttonLabel: this.$locale.baseText('settings.communityNodes.confirmModal.update.buttonLabel'), buttonLabel: this.$locale.baseText(
buttonLoadingLabel: this.$locale.baseText('settings.communityNodes.confirmModal.update.buttonLoadingLabel'), 'settings.communityNodes.confirmModal.update.buttonLabel',
),
buttonLoadingLabel: this.$locale.baseText(
'settings.communityNodes.confirmModal.update.buttonLoadingLabel',
),
}; };
}, },
}, },
@@ -117,7 +133,7 @@ export default mixins(showMessage).extend({
try { try {
this.$telemetry.track('user started cnr package deletion', { this.$telemetry.track('user started cnr package deletion', {
package_name: this.activePackage.packageName, package_name: this.activePackage.packageName,
package_node_names: this.activePackage.installedNodes.map(node => node.name), package_node_names: this.activePackage.installedNodes.map((node) => node.name),
package_version: this.activePackage.installedVersion, package_version: this.activePackage.installedVersion,
package_author: this.activePackage.authorName, package_author: this.activePackage.authorName,
package_author_email: this.activePackage.authorEmail, package_author_email: this.activePackage.authorEmail,
@@ -129,7 +145,10 @@ export default mixins(showMessage).extend({
type: 'success', type: 'success',
}); });
} catch (error) { } catch (error) {
this.$showError(error, this.$locale.baseText('settings.communityNodes.messages.uninstall.error')); this.$showError(
error,
this.$locale.baseText('settings.communityNodes.messages.uninstall.error'),
);
} finally { } finally {
this.loading = false; this.loading = false;
this.modalBus.$emit('close'); this.modalBus.$emit('close');
@@ -139,7 +158,7 @@ export default mixins(showMessage).extend({
try { try {
this.$telemetry.track('user started cnr package update', { this.$telemetry.track('user started cnr package update', {
package_name: this.activePackage.packageName, package_name: this.activePackage.packageName,
package_node_names: this.activePackage.installedNodes.map(node => node.name), package_node_names: this.activePackage.installedNodes.map((node) => node.name),
package_version_current: this.activePackage.installedVersion, package_version_current: this.activePackage.installedVersion,
package_version_new: this.activePackage.updateAvailable, package_version_new: this.activePackage.updateAvailable,
package_author: this.activePackage.authorName, package_author: this.activePackage.authorName,
@@ -150,16 +169,22 @@ export default mixins(showMessage).extend({
await this.communityNodesStore.updatePackage(this.activePackageName); await this.communityNodesStore.updatePackage(this.activePackageName);
this.$showMessage({ this.$showMessage({
title: this.$locale.baseText('settings.communityNodes.messages.update.success.title'), title: this.$locale.baseText('settings.communityNodes.messages.update.success.title'),
message: this.$locale.baseText('settings.communityNodes.messages.update.success.message', { message: this.$locale.baseText(
interpolate: { 'settings.communityNodes.messages.update.success.message',
packageName: this.activePackageName, {
version: updatedVersion, interpolate: {
packageName: this.activePackageName,
version: updatedVersion,
},
}, },
}), ),
type: 'success', type: 'success',
}); });
} catch (error) { } catch (error) {
this.$showError(error, this.$locale.baseText('settings.communityNodes.messages.update.error.title')); this.$showError(
error,
this.$locale.baseText('settings.communityNodes.messages.update.error.title'),
);
} finally { } finally {
this.loading = false; this.loading = false;
this.modalBus.$emit('close'); this.modalBus.$emit('close');
@@ -170,17 +195,17 @@ export default mixins(showMessage).extend({
</script> </script>
<style module lang="scss"> <style module lang="scss">
.descriptionContainer { .descriptionContainer {
display: flex; display: flex;
margin: var(--spacing-s) 0; margin: var(--spacing-s) 0;
} }
.descriptionIcon { .descriptionIcon {
align-self: center; align-self: center;
color: var(--color-text-lighter); color: var(--color-text-lighter);
} }
.descriptionText { .descriptionText {
padding: 0 var(--spacing-xs); padding: 0 var(--spacing-xs);
} }
</style> </style>

View File

@@ -55,10 +55,7 @@ export default mixins(workflowHelpers).extend({
}; };
}, },
computed: { computed: {
...mapStores( ...mapStores(useRootStore, useSettingsStore),
useRootStore,
useSettingsStore,
),
title(): string { title(): string {
if (this.settingsStore.promptsData && this.settingsStore.promptsData.title) { if (this.settingsStore.promptsData && this.settingsStore.promptsData.title) {
return this.settingsStore.promptsData.title; return this.settingsStore.promptsData.title;
@@ -88,7 +85,9 @@ export default mixins(workflowHelpers).extend({
}, },
async send() { async send() {
if (this.isEmailValid) { if (this.isEmailValid) {
const response = await this.settingsStore.submitContactInfo(this.email) as IN8nPromptResponse; const response = (await this.settingsStore.submitContactInfo(
this.email,
)) as IN8nPromptResponse;
if (response.updated) { if (response.updated) {
this.$telemetry.track('User closed email modal', { this.$telemetry.track('User closed email modal', {

View File

@@ -1,9 +1,15 @@
<template> <template>
<div> <div>
<n8n-input-label :label="label"> <n8n-input-label :label="label">
<div :class="{[$style.copyText]: true, [$style[size]]: true, [$style.collapsed]: collapse}" @click="copy" data-test-id="copy-input"> <div
:class="{ [$style.copyText]: true, [$style[size]]: true, [$style.collapsed]: collapse }"
@click="copy"
data-test-id="copy-input"
>
<span ref="copyInputValue">{{ value }}</span> <span ref="copyInputValue">{{ value }}</span>
<div :class="$style.copyButton"><span>{{ copyButtonText }}</span></div> <div :class="$style.copyButton">
<span>{{ copyButtonText }}</span>
</div>
</div> </div>
</n8n-input-label> </n8n-input-label>
<div v-if="hint" :class="$style.hint">{{ hint }}</div> <div v-if="hint" :class="$style.hint">{{ hint }}</div>
@@ -103,7 +109,7 @@ export default mixins(copyPaste, showMessage).extend({
.collapsed { .collapsed {
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
} }
.copyButton { .copyButton {
@@ -129,5 +135,4 @@ export default mixins(copyPaste, showMessage).extend({
font-weight: var(--font-weight-regular); font-weight: var(--font-weight-regular);
word-break: normal; word-break: normal;
} }
</style> </style>

View File

@@ -1,52 +1,44 @@
<template> <template>
<n8n-card <n8n-card :class="$style['card-link']" @click="onClick">
:class="$style['card-link']" <template #prepend>
@click="onClick" <credential-icon :credential-type-name="credentialType ? credentialType.name : ''" />
> </template>
<template #prepend> <template #header>
<credential-icon :credential-type-name="credentialType ? credentialType.name : ''" /> <n8n-heading tag="h2" bold class="ph-no-capture" :class="$style['card-heading']">
</template> {{ data.name }}
<template #header> </n8n-heading>
<n8n-heading tag="h2" bold class="ph-no-capture" :class="$style['card-heading']"> </template>
{{ data.name }} <n8n-text color="text-light" size="small">
</n8n-heading> <span v-if="credentialType">{{ credentialType.displayName }} | </span>
</template> <span v-show="data"
<n8n-text color="text-light" size="small"> >{{ $locale.baseText('credentials.item.updated') }} <time-ago :date="data.updatedAt" /> |
<span v-if="credentialType">{{ credentialType.displayName }} | </span> </span>
<span v-show="data">{{$locale.baseText('credentials.item.updated')}} <time-ago :date="data.updatedAt" /> | </span> <span v-show="data"
<span v-show="data">{{$locale.baseText('credentials.item.created')}} {{ formattedCreatedAtDate }} </span> >{{ $locale.baseText('credentials.item.created') }} {{ formattedCreatedAtDate }}
</n8n-text> </span>
<template #append> </n8n-text>
<div :class="$style['card-actions']"> <template #append>
<enterprise-edition :features="[EnterpriseEditionFeature.Sharing]"> <div :class="$style['card-actions']">
<n8n-badge <enterprise-edition :features="[EnterpriseEditionFeature.Sharing]">
v-if="credentialPermissions.isOwner" <n8n-badge v-if="credentialPermissions.isOwner" class="mr-xs" theme="tertiary" bold>
class="mr-xs" {{ $locale.baseText('credentials.item.owner') }}
theme="tertiary" </n8n-badge>
bold </enterprise-edition>
> <n8n-action-toggle :actions="actions" theme="dark" @action="onAction" />
{{$locale.baseText('credentials.item.owner')}} </div>
</n8n-badge> </template>
</enterprise-edition>
<n8n-action-toggle
:actions="actions"
theme="dark"
@action="onAction"
/>
</div>
</template>
</n8n-card> </n8n-card>
</template> </template>
<script lang="ts"> <script lang="ts">
import mixins from 'vue-typed-mixins'; import mixins from 'vue-typed-mixins';
import {ICredentialsResponse, IUser} from "@/Interface"; import { ICredentialsResponse, IUser } from '@/Interface';
import {ICredentialType} from "n8n-workflow"; import { ICredentialType } from 'n8n-workflow';
import {EnterpriseEditionFeature} from '@/constants'; import { EnterpriseEditionFeature } from '@/constants';
import {showMessage} from "@/mixins/showMessage"; import { showMessage } from '@/mixins/showMessage';
import CredentialIcon from '@/components/CredentialIcon.vue'; import CredentialIcon from '@/components/CredentialIcon.vue';
import {getCredentialPermissions, IPermissions} from "@/permissions"; import { getCredentialPermissions, IPermissions } from '@/permissions';
import dateformat from "dateformat"; import dateformat from 'dateformat';
import { mapStores } from 'pinia'; import { mapStores } from 'pinia';
import { useUIStore } from '@/stores/ui'; import { useUIStore } from '@/stores/ui';
import { useUsersStore } from '@/stores/users'; import { useUsersStore } from '@/stores/users';
@@ -57,9 +49,7 @@ export const CREDENTIAL_LIST_ITEM_ACTIONS = {
DELETE: 'delete', DELETE: 'delete',
}; };
export default mixins( export default mixins(showMessage).extend({
showMessage,
).extend({
data() { data() {
return { return {
EnterpriseEditionFeature, EnterpriseEditionFeature,
@@ -89,12 +79,8 @@ export default mixins(
}, },
}, },
computed: { computed: {
...mapStores( ...mapStores(useCredentialsStore, useUIStore, useUsersStore),
useCredentialsStore, currentUser(): IUser | null {
useUIStore,
useUsersStore,
),
currentUser (): IUser | null {
return this.usersStore.currentUser; return this.usersStore.currentUser;
}, },
credentialType(): ICredentialType { credentialType(): ICredentialType {
@@ -103,7 +89,7 @@ export default mixins(
credentialPermissions(): IPermissions | null { credentialPermissions(): IPermissions | null {
return !this.currentUser ? null : getCredentialPermissions(this.currentUser, this.data); return !this.currentUser ? null : getCredentialPermissions(this.currentUser, this.data);
}, },
actions(): Array<{ label: string; value: string; }> { actions(): Array<{ label: string; value: string }> {
if (!this.credentialPermissions) { if (!this.credentialPermissions) {
return []; return [];
} }
@@ -113,15 +99,24 @@ export default mixins(
label: this.$locale.baseText('credentials.item.open'), label: this.$locale.baseText('credentials.item.open'),
value: CREDENTIAL_LIST_ITEM_ACTIONS.OPEN, value: CREDENTIAL_LIST_ITEM_ACTIONS.OPEN,
}, },
].concat(this.credentialPermissions.delete ? [{ ].concat(
label: this.$locale.baseText('credentials.item.delete'), this.credentialPermissions.delete
value: CREDENTIAL_LIST_ITEM_ACTIONS.DELETE, ? [
}]: []); {
label: this.$locale.baseText('credentials.item.delete'),
value: CREDENTIAL_LIST_ITEM_ACTIONS.DELETE,
},
]
: [],
);
}, },
formattedCreatedAtDate(): string { formattedCreatedAtDate(): string {
const currentYear = new Date().getFullYear(); const currentYear = new Date().getFullYear();
return dateformat(this.data.createdAt, `d mmmm${this.data.createdAt.startsWith(currentYear) ? '' : ', yyyy'}`); return dateformat(
this.data.createdAt,
`d mmmm${this.data.createdAt.startsWith(currentYear) ? '' : ', yyyy'}`,
);
}, },
}, },
methods: { methods: {
@@ -133,16 +128,23 @@ export default mixins(
this.onClick(); this.onClick();
} else if (action === CREDENTIAL_LIST_ITEM_ACTIONS.DELETE) { } else if (action === CREDENTIAL_LIST_ITEM_ACTIONS.DELETE) {
const deleteConfirmed = await this.confirmMessage( const deleteConfirmed = await this.confirmMessage(
this.$locale.baseText('credentialEdit.credentialEdit.confirmMessage.deleteCredential.message', { this.$locale.baseText(
interpolate: { savedCredentialName: this.data.name }, 'credentialEdit.credentialEdit.confirmMessage.deleteCredential.message',
}), {
this.$locale.baseText('credentialEdit.credentialEdit.confirmMessage.deleteCredential.headline'), interpolate: { savedCredentialName: this.data.name },
},
),
this.$locale.baseText(
'credentialEdit.credentialEdit.confirmMessage.deleteCredential.headline',
),
null, null,
this.$locale.baseText('credentialEdit.credentialEdit.confirmMessage.deleteCredential.confirmButtonText'), this.$locale.baseText(
'credentialEdit.credentialEdit.confirmMessage.deleteCredential.confirmButtonText',
),
); );
if (deleteConfirmed) { if (deleteConfirmed) {
this.credentialsStore.deleteCredential({ id: this.data.id }); this.credentialsStore.deleteCredential({ id: this.data.id });
} }
} }
}, },
@@ -156,7 +158,7 @@ export default mixins(
cursor: pointer; cursor: pointer;
&:hover { &:hover {
box-shadow: 0 2px 8px rgba(#441C17, 0.1); box-shadow: 0 2px 8px rgba(#441c17, 0.1);
} }
} }
@@ -171,5 +173,3 @@ export default mixins(
align-items: center; align-items: center;
} }
</style> </style>

View File

@@ -9,7 +9,14 @@
<banner <banner
v-if="authError && !showValidationWarning" v-if="authError && !showValidationWarning"
theme="danger" theme="danger"
:message="$locale.baseText(`credentialEdit.credentialConfig.couldntConnectWithTheseSettings${!credentialPermissions.isOwner ? '.sharee' : ''}`, { interpolate: { owner: credentialOwnerName } })" :message="
$locale.baseText(
`credentialEdit.credentialConfig.couldntConnectWithTheseSettings${
!credentialPermissions.isOwner ? '.sharee' : ''
}`,
{ interpolate: { owner: credentialOwnerName } },
)
"
:details="authError" :details="authError"
:buttonLabel="$locale.baseText('credentialEdit.credentialConfig.retry')" :buttonLabel="$locale.baseText('credentialEdit.credentialConfig.retry')"
buttonLoadingLabel="Retrying" buttonLoadingLabel="Retrying"
@@ -53,17 +60,22 @@
:label="$locale.baseText('credentialEdit.credentialConfig.oAuthRedirectUrl')" :label="$locale.baseText('credentialEdit.credentialConfig.oAuthRedirectUrl')"
:value="oAuthCallbackUrl" :value="oAuthCallbackUrl"
:copyButtonText="$locale.baseText('credentialEdit.credentialConfig.clickToCopy')" :copyButtonText="$locale.baseText('credentialEdit.credentialConfig.clickToCopy')"
:hint="$locale.baseText('credentialEdit.credentialConfig.subtitle', { interpolate: { appName } })" :hint="
:toastTitle="$locale.baseText('credentialEdit.credentialConfig.redirectUrlCopiedToClipboard')" $locale.baseText('credentialEdit.credentialConfig.subtitle', { interpolate: { appName } })
"
:toastTitle="
$locale.baseText('credentialEdit.credentialConfig.redirectUrlCopiedToClipboard')
"
/> />
</template> </template>
<enterprise-edition <enterprise-edition v-else :features="[EnterpriseEditionFeature.Sharing]">
v-else
:features="[EnterpriseEditionFeature.Sharing]"
>
<div class="ph-no-capture"> <div class="ph-no-capture">
<n8n-info-tip :bold="false"> <n8n-info-tip :bold="false">
{{ $locale.baseText('credentialEdit.credentialEdit.info.sharee', { interpolate: { credentialOwnerName } }) }} {{
$locale.baseText('credentialEdit.credentialEdit.info.sharee', {
interpolate: { credentialOwnerName },
})
}}
</n8n-info-tip> </n8n-info-tip>
</div> </div>
</enterprise-edition> </enterprise-edition>
@@ -78,7 +90,12 @@
/> />
<OauthButton <OauthButton
v-if="isOAuthType && requiredPropertiesFilled && !isOAuthConnected && credentialPermissions.isOwner" v-if="
isOAuthType &&
requiredPropertiesFilled &&
!isOAuthConnected &&
credentialPermissions.isOwner
"
:isGoogleOAuthType="isGoogleOAuthType" :isGoogleOAuthType="isGoogleOAuthType"
@click="$emit('oauth')" @click="$emit('oauth')"
/> />
@@ -101,7 +118,7 @@ import { restApi } from '@/mixins/restApi';
import { addCredentialTranslation } from '@/plugins/i18n'; import { addCredentialTranslation } from '@/plugins/i18n';
import mixins from 'vue-typed-mixins'; import mixins from 'vue-typed-mixins';
import { BUILTIN_CREDENTIALS_DOCS_URL, EnterpriseEditionFeature } from '@/constants'; import { BUILTIN_CREDENTIALS_DOCS_URL, EnterpriseEditionFeature } from '@/constants';
import { IPermissions } from "@/permissions"; import { IPermissions } from '@/permissions';
import { mapStores } from 'pinia'; import { mapStores } from 'pinia';
import { useUIStore } from '@/stores/ui'; import { useUIStore } from '@/stores/ui';
import { useWorkflowsStore } from '@/stores/workflows'; import { useWorkflowsStore } from '@/stores/workflows';
@@ -127,8 +144,7 @@ export default mixins(restApi).extend({
parentTypes: { parentTypes: {
type: Array, type: Array,
}, },
credentialData: { credentialData: {},
},
credentialId: { credentialId: {
type: [String, Number], type: [String, Number],
default: '', default: '',
@@ -182,23 +198,18 @@ export default mixins(restApi).extend({
); );
}, },
computed: { computed: {
...mapStores( ...mapStores(useCredentialsStore, useNDVStore, useRootStore, useUIStore, useWorkflowsStore),
useCredentialsStore,
useNDVStore,
useRootStore,
useUIStore,
useWorkflowsStore,
),
appName(): string { appName(): string {
if (!this.credentialType) { if (!this.credentialType) {
return ''; return '';
} }
const appName = getAppNameFromCredType( const appName = getAppNameFromCredType((this.credentialType as ICredentialType).displayName);
(this.credentialType as ICredentialType).displayName,
);
return appName || this.$locale.baseText('credentialEdit.credentialConfig.theServiceYouReConnectingTo'); return (
appName ||
this.$locale.baseText('credentialEdit.credentialConfig.theServiceYouReConnectingTo')
);
}, },
credentialTypeName(): string { credentialTypeName(): string {
return (this.credentialType as ICredentialType).name; return (this.credentialType as ICredentialType).name;
@@ -215,34 +226,44 @@ export default mixins(restApi).extend({
return ''; return '';
} }
if (type.documentationUrl.startsWith('https://') || type.documentationUrl.startsWith('http://')) { if (
type.documentationUrl.startsWith('https://') ||
type.documentationUrl.startsWith('http://')
) {
return type.documentationUrl; return type.documentationUrl;
} }
return isCommunityNode ? return isCommunityNode
'' : // Don't show documentation link for community nodes if the URL is not an absolute path ? '' // 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`; : `${BUILTIN_CREDENTIALS_DOCS_URL}${type.documentationUrl}/?utm_source=n8n_app&utm_medium=left_nav_menu&utm_campaign=create_new_credentials_modal`;
}, },
isGoogleOAuthType(): boolean { isGoogleOAuthType(): boolean {
return this.credentialTypeName === 'googleOAuth2Api' || this.parentTypes.includes('googleOAuth2Api'); return (
this.credentialTypeName === 'googleOAuth2Api' ||
this.parentTypes.includes('googleOAuth2Api')
);
}, },
oAuthCallbackUrl(): string { oAuthCallbackUrl(): string {
const oauthType = const oauthType =
this.credentialTypeName === 'oAuth2Api' || this.credentialTypeName === 'oAuth2Api' || this.parentTypes.includes('oAuth2Api')
this.parentTypes.includes('oAuth2Api')
? 'oauth2' ? 'oauth2'
: 'oauth1'; : 'oauth1';
return this.rootStore.oauthCallbackUrls[oauthType as keyof {}]; return this.rootStore.oauthCallbackUrls[oauthType as keyof {}];
}, },
showOAuthSuccessBanner(): boolean { showOAuthSuccessBanner(): boolean {
return this.isOAuthType && this.requiredPropertiesFilled && this.isOAuthConnected && !this.authError; return (
this.isOAuthType &&
this.requiredPropertiesFilled &&
this.isOAuthConnected &&
!this.authError
);
}, },
}, },
methods: { methods: {
onDataChange (event: { name: string; value: string | number | boolean | Date | null }): void { onDataChange(event: { name: string; value: string | number | boolean | Date | null }): void {
this.$emit('change', event); this.$emit('change', event);
}, },
onDocumentationUrlClick (): void { onDocumentationUrlClick(): void {
this.$telemetry.track('User clicked credential modal docs link', { this.$telemetry.track('User clicked credential modal docs link', {
docs_link: this.documentationUrl, docs_link: this.documentationUrl,
credential_type: this.credentialTypeName, credential_type: this.credentialTypeName,
@@ -268,5 +289,4 @@ export default mixins(restApi).extend({
margin-bottom: var(--spacing-l); margin-bottom: var(--spacing-l);
} }
} }
</style> </style>

View File

@@ -39,9 +39,11 @@
v-if="(hasUnsavedChanges || credentialId) && credentialPermissions.save" v-if="(hasUnsavedChanges || credentialId) && credentialPermissions.save"
:saved="!hasUnsavedChanges && !isTesting" :saved="!hasUnsavedChanges && !isTesting"
:isSaving="isSaving || isTesting" :isSaving="isSaving || isTesting"
:savingLabel="isTesting :savingLabel="
? $locale.baseText('credentialEdit.credentialEdit.testing') isTesting
: $locale.baseText('credentialEdit.credentialEdit.saving')" ? $locale.baseText('credentialEdit.credentialEdit.testing')
: $locale.baseText('credentialEdit.credentialEdit.saving')
"
@click="saveCredential" @click="saveCredential"
data-test-id="credential-save-button" data-test-id="credential-save-button"
/> />
@@ -52,7 +54,7 @@
<template #content> <template #content>
<div :class="$style.container"> <div :class="$style.container">
<div :class="$style.sidebar"> <div :class="$style.sidebar">
<n8n-menu mode="tabs" :items="sidebarItems" @select="onTabSelect" ></n8n-menu> <n8n-menu mode="tabs" :items="sidebarItems" @select="onTabSelect"></n8n-menu>
</div> </div>
<div v-if="activeTab === 'connection'" :class="$style.mainContent" ref="content"> <div v-if="activeTab === 'connection'" :class="$style.mainContent" ref="content">
<CredentialConfig <CredentialConfig
@@ -109,11 +111,7 @@
<script lang="ts"> <script lang="ts">
import Vue from 'vue'; import Vue from 'vue';
import { import { ICredentialsResponse, IFakeDoor, IUser } from '@/Interface';
ICredentialsResponse,
IFakeDoor,
IUser,
} from '@/Interface';
import { import {
CredentialInformation, CredentialInformation,
@@ -136,14 +134,14 @@ import { showMessage } from '@/mixins/showMessage';
import CredentialConfig from './CredentialConfig.vue'; import CredentialConfig from './CredentialConfig.vue';
import CredentialInfo from './CredentialInfo.vue'; import CredentialInfo from './CredentialInfo.vue';
import CredentialSharing from "./CredentialSharing.ee.vue"; import CredentialSharing from './CredentialSharing.ee.vue';
import SaveButton from '../SaveButton.vue'; import SaveButton from '../SaveButton.vue';
import Modal from '../Modal.vue'; import Modal from '../Modal.vue';
import InlineNameEdit from '../InlineNameEdit.vue'; import InlineNameEdit from '../InlineNameEdit.vue';
import {EnterpriseEditionFeature} from "@/constants"; import { EnterpriseEditionFeature } from '@/constants';
import {IDataObject} from "n8n-workflow"; import { IDataObject } from 'n8n-workflow';
import FeatureComingSoon from '../FeatureComingSoon.vue'; import FeatureComingSoon from '../FeatureComingSoon.vue';
import {getCredentialPermissions, IPermissions} from "@/permissions"; import { getCredentialPermissions, IPermissions } from '@/permissions';
import { IMenuItem } from 'n8n-design-system'; import { IMenuItem } from 'n8n-design-system';
import { BaseTextKey } from '@/plugins/i18n'; import { BaseTextKey } from '@/plugins/i18n';
import { mapStores } from 'pinia'; import { mapStores } from 'pinia';
@@ -205,21 +203,20 @@ export default mixins(showMessage, nodeHelpers).extend({
}; };
}, },
async mounted() { async mounted() {
this.nodeAccess = this.nodesWithAccess.reduce( this.nodeAccess = this.nodesWithAccess.reduce((accu: NodeAccessMap, node: { name: string }) => {
(accu: NodeAccessMap, node: { name: string }) => { if (this.mode === 'new') {
if (this.mode === 'new') { accu[node.name] = { nodeType: node.name }; // enable all nodes by default
accu[node.name] = { nodeType: node.name }; // enable all nodes by default } else {
} else { accu[node.name] = null;
accu[node.name] = null; }
}
return accu; return accu;
}, }, {});
{},
);
if (this.mode === 'new' && this.credentialTypeName) { if (this.mode === 'new' && this.credentialTypeName) {
this.credentialName = await this.credentialsStore.getNewCredentialName({ credentialTypeName: this.credentialTypeName }); this.credentialName = await this.credentialsStore.getNewCredentialName({
credentialTypeName: this.credentialTypeName,
});
if (this.currentUser) { if (this.currentUser) {
Vue.set(this.credentialData, 'ownedBy', { Vue.set(this.credentialData, 'ownedBy', {
@@ -254,8 +251,7 @@ export default mixins(showMessage, nodeHelpers).extend({
if (this.credentialId) { if (this.credentialId) {
if (!this.requiredPropertiesFilled) { if (!this.requiredPropertiesFilled) {
this.showValidationWarning = true; this.showValidationWarning = true;
} } else {
else {
this.retestCredential(); this.retestCredential();
} }
} }
@@ -265,13 +261,13 @@ export default mixins(showMessage, nodeHelpers).extend({
}, },
computed: { computed: {
...mapStores( ...mapStores(
useCredentialsStore, useCredentialsStore,
useNDVStore, useNDVStore,
useSettingsStore, useSettingsStore,
useUIStore, useUIStore,
useUsersStore, useUsersStore,
useWorkflowsStore, useWorkflowsStore,
), ),
currentUser(): IUser | null { currentUser(): IUser | null {
return this.usersStore.currentUser; return this.usersStore.currentUser;
}, },
@@ -309,21 +305,25 @@ export default mixins(showMessage, nodeHelpers).extend({
properties: this.getCredentialProperties(this.credentialTypeName), properties: this.getCredentialProperties(this.credentialTypeName),
}; };
}, },
isCredentialTestable (): boolean { isCredentialTestable(): boolean {
if (this.isOAuthType || !this.requiredPropertiesFilled) { if (this.isOAuthType || !this.requiredPropertiesFilled) {
return false; return false;
} }
const { ownedBy, sharedWith, ...credentialData } = this.credentialData; const { ownedBy, sharedWith, ...credentialData } = this.credentialData;
const hasExpressions = Object.values(credentialData).reduce((accu: boolean, value: CredentialInformation) => accu || (typeof value === 'string' && value.startsWith('=')), false); const hasExpressions = Object.values(credentialData).reduce(
(accu: boolean, value: CredentialInformation) =>
accu || (typeof value === 'string' && value.startsWith('=')),
false,
);
if (hasExpressions) { if (hasExpressions) {
return false; return false;
} }
const nodesThatCanTest = this.nodesWithAccess.filter(node => { const nodesThatCanTest = this.nodesWithAccess.filter((node) => {
if (node.credentials) { if (node.credentials) {
// Returns a list of nodes that can test this credentials // Returns a list of nodes that can test this credentials
const eligibleTesters = node.credentials.filter(credential => { const eligibleTesters = node.credentials.filter((credential) => {
return credential.name === this.credentialTypeName && credential.testedBy; return credential.name === this.credentialTypeName && credential.testedBy;
}); });
// If we have any node that can test, return true. // If we have any node that can test, return true.
@@ -349,18 +349,12 @@ export default mixins(showMessage, nodeHelpers).extend({
return []; return [];
}, },
isOAuthType(): boolean { isOAuthType(): boolean {
return !!this.credentialTypeName && ( return (
( !!this.credentialTypeName &&
( (((this.credentialTypeName === 'oAuth2Api' || this.parentTypes.includes('oAuth2Api')) &&
this.credentialTypeName === 'oAuth2Api' || this.credentialData.grantType === 'authorizationCode') ||
this.parentTypes.includes('oAuth2Api')
) && this.credentialData.grantType === 'authorizationCode'
)
||
(
this.credentialTypeName === 'oAuth1Api' || this.credentialTypeName === 'oAuth1Api' ||
this.parentTypes.includes('oAuth1Api') this.parentTypes.includes('oAuth1Api'))
)
); );
}, },
isOAuthConnected(): boolean { isOAuthConnected(): boolean {
@@ -371,19 +365,15 @@ export default mixins(showMessage, nodeHelpers).extend({
return []; return [];
} }
return this.credentialType.properties.filter( return this.credentialType.properties.filter((propertyData: INodeProperties) => {
(propertyData: INodeProperties) => { if (!this.displayCredentialParameter(propertyData)) {
if (!this.displayCredentialParameter(propertyData)) { return false;
return false; }
} return (
return ( !this.credentialType!.__overwrittenProperties ||
!this.credentialType!.__overwrittenProperties || !this.credentialType!.__overwrittenProperties.includes(propertyData.name)
!this.credentialType!.__overwrittenProperties.includes( );
propertyData.name, });
)
);
},
);
}, },
requiredPropertiesFilled(): boolean { requiredPropertiesFilled(): boolean {
for (const property of this.credentialProperties) { for (const property of this.credentialProperties) {
@@ -409,41 +399,42 @@ export default mixins(showMessage, nodeHelpers).extend({
return {}; return {};
} }
return getCredentialPermissions(this.currentUser, (this.credentialId ? this.currentCredential : this.credentialData) as ICredentialsResponse); return getCredentialPermissions(
this.currentUser,
(this.credentialId ? this.currentCredential : this.credentialData) as ICredentialsResponse,
);
}, },
sidebarItems(): IMenuItem[] { sidebarItems(): IMenuItem[] {
const items: IMenuItem[] = [ const items: IMenuItem[] = [
{ {
id: 'connection', id: 'connection',
label: this.$locale.baseText('credentialEdit.credentialEdit.connection'), label: this.$locale.baseText('credentialEdit.credentialEdit.connection'),
position: 'top', position: 'top',
}, },
{ {
id: 'sharing', id: 'sharing',
label: this.$locale.baseText('credentialEdit.credentialEdit.sharing'), label: this.$locale.baseText('credentialEdit.credentialEdit.sharing'),
position: 'top', position: 'top',
available: this.credentialType !== null && this.isSharingAvailable, available: this.credentialType !== null && this.isSharingAvailable,
}, },
]; ];
if (this.credentialType !== null && !this.isSharingAvailable) { if (this.credentialType !== null && !this.isSharingAvailable) {
for (const item of this.credentialsFakeDoorFeatures) { for (const item of this.credentialsFakeDoorFeatures) {
items.push({ items.push({
id: `coming-soon/${item.id}`, id: `coming-soon/${item.id}`,
label: this.$locale.baseText(item.featureName as BaseTextKey), label: this.$locale.baseText(item.featureName as BaseTextKey),
position: 'top', position: 'top',
}); });
}
} }
}
items.push( items.push({
{ id: 'details',
id: 'details', label: this.$locale.baseText('credentialEdit.credentialEdit.details'),
label: this.$locale.baseText('credentialEdit.credentialEdit.details'), position: 'top',
position: 'top', });
}, return items;
);
return items;
}, },
isSharingAvailable(): boolean { isSharingAvailable(): boolean {
return this.settingsStore.isEnterpriseFeatureEnabled(EnterpriseEditionFeature.Sharing); return this.settingsStore.isEnterpriseFeatureEnabled(EnterpriseEditionFeature.Sharing);
@@ -456,30 +447,45 @@ export default mixins(showMessage, nodeHelpers).extend({
if (this.hasUnsavedChanges) { if (this.hasUnsavedChanges) {
const displayName = this.credentialType ? this.credentialType.displayName : ''; const displayName = this.credentialType ? this.credentialType.displayName : '';
keepEditing = await this.confirmMessage( keepEditing = await this.confirmMessage(
this.$locale.baseText('credentialEdit.credentialEdit.confirmMessage.beforeClose1.message', { interpolate: { credentialDisplayName: displayName } }), this.$locale.baseText(
this.$locale.baseText('credentialEdit.credentialEdit.confirmMessage.beforeClose1.headline'), 'credentialEdit.credentialEdit.confirmMessage.beforeClose1.message',
{ interpolate: { credentialDisplayName: displayName } },
),
this.$locale.baseText(
'credentialEdit.credentialEdit.confirmMessage.beforeClose1.headline',
),
null, null,
this.$locale.baseText('credentialEdit.credentialEdit.confirmMessage.beforeClose1.cancelButtonText'), this.$locale.baseText(
this.$locale.baseText('credentialEdit.credentialEdit.confirmMessage.beforeClose1.confirmButtonText'), 'credentialEdit.credentialEdit.confirmMessage.beforeClose1.cancelButtonText',
),
this.$locale.baseText(
'credentialEdit.credentialEdit.confirmMessage.beforeClose1.confirmButtonText',
),
); );
} else if (this.credentialPermissions.isOwner && this.isOAuthType && !this.isOAuthConnected) { } else if (this.credentialPermissions.isOwner && this.isOAuthType && !this.isOAuthConnected) {
keepEditing = await this.confirmMessage( keepEditing = await this.confirmMessage(
this.$locale.baseText('credentialEdit.credentialEdit.confirmMessage.beforeClose2.message'), this.$locale.baseText(
this.$locale.baseText('credentialEdit.credentialEdit.confirmMessage.beforeClose2.headline'), 'credentialEdit.credentialEdit.confirmMessage.beforeClose2.message',
),
this.$locale.baseText(
'credentialEdit.credentialEdit.confirmMessage.beforeClose2.headline',
),
null, null,
this.$locale.baseText('credentialEdit.credentialEdit.confirmMessage.beforeClose2.cancelButtonText'), this.$locale.baseText(
this.$locale.baseText('credentialEdit.credentialEdit.confirmMessage.beforeClose2.confirmButtonText'), 'credentialEdit.credentialEdit.confirmMessage.beforeClose2.cancelButtonText',
),
this.$locale.baseText(
'credentialEdit.credentialEdit.confirmMessage.beforeClose2.confirmButtonText',
),
); );
} }
if (!keepEditing) { if (!keepEditing) {
return true; return true;
} } else if (!this.requiredPropertiesFilled) {
else if (!this.requiredPropertiesFilled) {
this.showValidationWarning = true; this.showValidationWarning = true;
this.scrollToTop(); this.scrollToTop();
} } else if (this.isOAuthType) {
else if (this.isOAuthType) {
this.scrollToBottom(); this.scrollToBottom();
} }
@@ -495,12 +501,7 @@ export default mixins(showMessage, nodeHelpers).extend({
return true; return true;
} }
return this.displayParameter( return this.displayParameter(this.credentialData as INodeParameters, parameter, '', null);
this.credentialData as INodeParameters,
parameter,
'',
null,
);
}, },
getCredentialProperties(name: string): INodeProperties[] { getCredentialProperties(name: string): INodeProperties[] {
const credentialTypeData = this.credentialsStore.getCredentialTypeByName(name); const credentialTypeData = this.credentialsStore.getCredentialTypeByName(name);
@@ -515,19 +516,12 @@ export default mixins(showMessage, nodeHelpers).extend({
const combineProperties = [] as INodeProperties[]; const combineProperties = [] as INodeProperties[];
for (const credentialsTypeName of credentialTypeData.extends) { for (const credentialsTypeName of credentialTypeData.extends) {
const mergeCredentialProperties = const mergeCredentialProperties = this.getCredentialProperties(credentialsTypeName);
this.getCredentialProperties(credentialsTypeName); NodeHelpers.mergeNodeProperties(combineProperties, mergeCredentialProperties);
NodeHelpers.mergeNodeProperties(
combineProperties,
mergeCredentialProperties,
);
} }
// The properties defined on the parent credentials take precedence // The properties defined on the parent credentials take precedence
NodeHelpers.mergeNodeProperties( NodeHelpers.mergeNodeProperties(combineProperties, credentialTypeData.properties);
combineProperties,
credentialTypeData.properties,
);
return combineProperties; return combineProperties;
}, },
@@ -536,11 +530,15 @@ export default mixins(showMessage, nodeHelpers).extend({
this.credentialId = this.activeId; this.credentialId = this.activeId;
try { try {
const currentCredentials = await this.credentialsStore.getCredentialData({ id: this.credentialId }); const currentCredentials = await this.credentialsStore.getCredentialData({
id: this.credentialId,
});
if (!currentCredentials) { if (!currentCredentials) {
throw new Error( throw new Error(
this.$locale.baseText('credentialEdit.credentialEdit.couldNotFindCredentialWithId') + ':' + this.credentialId, this.$locale.baseText('credentialEdit.credentialEdit.couldNotFindCredentialWithId') +
':' +
this.credentialId,
); );
} }
@@ -553,12 +551,10 @@ export default mixins(showMessage, nodeHelpers).extend({
} }
this.credentialName = currentCredentials.name; this.credentialName = currentCredentials.name;
currentCredentials.nodesAccess.forEach( currentCredentials.nodesAccess.forEach((access: { nodeType: string }) => {
(access: { nodeType: string }) => { // keep node access structure to keep dates when updating
// keep node access structure to keep dates when updating this.nodeAccess[access.nodeType] = access;
this.nodeAccess[access.nodeType] = access; });
},
);
} catch (error) { } catch (error) {
this.$showError( this.$showError(
error, error,
@@ -584,7 +580,7 @@ export default mixins(showMessage, nodeHelpers).extend({
sharing_enabled: EnterpriseEditionFeature.Sharing, sharing_enabled: EnterpriseEditionFeature.Sharing,
}); });
}, },
onNodeAccessChange({name, value}: {name: string, value: boolean}) { onNodeAccessChange({ name, value }: { name: string; value: boolean }) {
this.hasUnsavedChanges = true; this.hasUnsavedChanges = true;
if (value) { if (value) {
@@ -605,7 +601,8 @@ export default mixins(showMessage, nodeHelpers).extend({
Vue.set(this.credentialData, 'sharedWith', sharees); Vue.set(this.credentialData, 'sharedWith', sharees);
this.hasUnsavedChanges = true; this.hasUnsavedChanges = true;
}, },
onDataChange({ name, value }: { name: string; value: any }) { // tslint:disable-line:no-any onDataChange({ name, value }: { name: string; value: any }) {
// tslint:disable-line:no-any
this.hasUnsavedChanges = true; this.hasUnsavedChanges = true;
const { oauthTokenData, ...credData } = this.credentialData; const { oauthTokenData, ...credData } = this.credentialData;
@@ -622,10 +619,7 @@ export default mixins(showMessage, nodeHelpers).extend({
getParentTypes(name: string): string[] { getParentTypes(name: string): string[] {
const credentialType = this.credentialsStore.getCredentialTypeByName(name); const credentialType = this.credentialsStore.getCredentialTypeByName(name);
if ( if (credentialType === undefined || credentialType.extends === undefined) {
credentialType === undefined ||
credentialType.extends === undefined
) {
return []; return [];
} }
@@ -692,8 +686,7 @@ export default mixins(showMessage, nodeHelpers).extend({
if (result.status === 'Error') { if (result.status === 'Error') {
this.authError = result.message; this.authError = result.message;
this.testedSuccessfully = false; this.testedSuccessfully = false;
} } else {
else {
this.authError = ''; this.authError = '';
this.testedSuccessfully = true; this.testedSuccessfully = true;
} }
@@ -705,8 +698,7 @@ export default mixins(showMessage, nodeHelpers).extend({
if (!this.requiredPropertiesFilled) { if (!this.requiredPropertiesFilled) {
this.showValidationWarning = true; this.showValidationWarning = true;
this.scrollToTop(); this.scrollToTop();
} } else {
else {
this.showValidationWarning = false; this.showValidationWarning = false;
} }
@@ -746,13 +738,9 @@ export default mixins(showMessage, nodeHelpers).extend({
const isNewCredential = this.mode === 'new' && !this.credentialId; const isNewCredential = this.mode === 'new' && !this.credentialId;
if (isNewCredential) { if (isNewCredential) {
credential = await this.createCredential( credential = await this.createCredential(credentialDetails);
credentialDetails,
);
} else { } else {
credential = await this.updateCredential( credential = await this.updateCredential(credentialDetails);
credentialDetails,
);
} }
this.isSaving = false; this.isSaving = false;
@@ -768,8 +756,7 @@ export default mixins(showMessage, nodeHelpers).extend({
await this.testCredential(credentialDetails); await this.testCredential(credentialDetails);
this.isTesting = false; this.isTesting = false;
} } else {
else {
this.authError = ''; this.authError = '';
this.testedSuccessfully = false; this.testedSuccessfully = false;
} }
@@ -840,7 +827,10 @@ export default mixins(showMessage, nodeHelpers).extend({
): Promise<ICredentialsResponse | null> { ): Promise<ICredentialsResponse | null> {
let credential; let credential;
try { try {
credential = await this.credentialsStore.updateCredential({ id: this.credentialId, data: credentialDetails }); credential = await this.credentialsStore.updateCredential({
id: this.credentialId,
data: credentialDetails,
});
this.hasUnsavedChanges = false; this.hasUnsavedChanges = false;
} catch (error) { } catch (error) {
this.$showError( this.$showError(
@@ -872,10 +862,17 @@ export default mixins(showMessage, nodeHelpers).extend({
const savedCredentialName = this.currentCredential.name; const savedCredentialName = this.currentCredential.name;
const deleteConfirmed = await this.confirmMessage( const deleteConfirmed = await this.confirmMessage(
this.$locale.baseText('credentialEdit.credentialEdit.confirmMessage.deleteCredential.message', { interpolate: { savedCredentialName } }), this.$locale.baseText(
this.$locale.baseText('credentialEdit.credentialEdit.confirmMessage.deleteCredential.headline'), 'credentialEdit.credentialEdit.confirmMessage.deleteCredential.message',
{ interpolate: { savedCredentialName } },
),
this.$locale.baseText(
'credentialEdit.credentialEdit.confirmMessage.deleteCredential.headline',
),
null, null,
this.$locale.baseText('credentialEdit.credentialEdit.confirmMessage.deleteCredential.confirmButtonText'), this.$locale.baseText(
'credentialEdit.credentialEdit.confirmMessage.deleteCredential.confirmButtonText',
),
); );
if (deleteConfirmed === false) { if (deleteConfirmed === false) {
@@ -919,17 +916,11 @@ export default mixins(showMessage, nodeHelpers).extend({
try { try {
const credData = { id: credential.id, ...this.credentialData }; const credData = { id: credential.id, ...this.credentialData };
if ( if (this.credentialTypeName === 'oAuth2Api' || types.includes('oAuth2Api')) {
this.credentialTypeName === 'oAuth2Api' ||
types.includes('oAuth2Api')
) {
if (isValidCredentialResponse(credData)) { if (isValidCredentialResponse(credData)) {
url = await this.credentialsStore.oAuth2Authorize(credData); url = await this.credentialsStore.oAuth2Authorize(credData);
} }
} else if ( } else if (this.credentialTypeName === 'oAuth1Api' || types.includes('oAuth1Api')) {
this.credentialTypeName === 'oAuth1Api' ||
types.includes('oAuth1Api')
) {
if (isValidCredentialResponse(credData)) { if (isValidCredentialResponse(credData)) {
url = await this.credentialsStore.oAuth1Authorize(credData); url = await this.credentialsStore.oAuth1Authorize(credData);
} }
@@ -937,8 +928,12 @@ export default mixins(showMessage, nodeHelpers).extend({
} catch (error) { } catch (error) {
this.$showError( this.$showError(
error, error,
this.$locale.baseText('credentialEdit.credentialEdit.showError.generateAuthorizationUrl.title'), this.$locale.baseText(
this.$locale.baseText('credentialEdit.credentialEdit.showError.generateAuthorizationUrl.message'), 'credentialEdit.credentialEdit.showError.generateAuthorizationUrl.title',
),
this.$locale.baseText(
'credentialEdit.credentialEdit.showError.generateAuthorizationUrl.message',
),
); );
return; return;
@@ -971,7 +966,6 @@ export default mixins(showMessage, nodeHelpers).extend({
window.addEventListener('message', receiveMessage, false); window.addEventListener('message', receiveMessage, false);
}, },
}, },
}); });
</script> </script>

View File

@@ -7,22 +7,25 @@
</n8n-text> </n8n-text>
</el-col> </el-col>
<el-col :span="16"> <el-col :span="16">
<div <div v-for="node in nodesWithAccess" :key="node.name" :class="$style.valueLabel">
v-for="node in nodesWithAccess"
:key="node.name"
:class="$style.valueLabel"
>
<el-checkbox <el-checkbox
v-if="credentialPermissions.updateNodeAccess" v-if="credentialPermissions.updateNodeAccess"
:label="$locale.headerText({ :label="
key: `headers.${shortNodeType(node)}.displayName`, $locale.headerText({
fallback: node.displayName, key: `headers.${shortNodeType(node)}.displayName`,
})" fallback: node.displayName,
})
"
:value="!!nodeAccess[node.name]" :value="!!nodeAccess[node.name]"
@change="(val) => onNodeAccessChange(node.name, val)" @change="(val) => onNodeAccessChange(node.name, val)"
/> />
<n8n-text v-else> <n8n-text v-else>
{{ $locale.headerText({ key: `headers.${shortNodeType(node)}.displayName`, fallback: node.displayName })}} {{
$locale.headerText({
key: `headers.${shortNodeType(node)}.displayName`,
fallback: node.displayName,
})
}}
</n8n-text> </n8n-text>
</div> </div>
</el-col> </el-col>
@@ -34,7 +37,9 @@
</n8n-text> </n8n-text>
</el-col> </el-col>
<el-col :span="16" :class="$style.valueLabel"> <el-col :span="16" :class="$style.valueLabel">
<n8n-text :compact="true"><TimeAgo :date="currentCredential.createdAt" :capitalize="true" /></n8n-text> <n8n-text :compact="true"
><TimeAgo :date="currentCredential.createdAt" :capitalize="true"
/></n8n-text>
</el-col> </el-col>
</el-row> </el-row>
<el-row v-if="currentCredential"> <el-row v-if="currentCredential">
@@ -44,7 +49,9 @@
</n8n-text> </n8n-text>
</el-col> </el-col>
<el-col :span="16" :class="$style.valueLabel"> <el-col :span="16" :class="$style.valueLabel">
<n8n-text :compact="true"><TimeAgo :date="currentCredential.updatedAt" :capitalize="true" /></n8n-text> <n8n-text :compact="true"
><TimeAgo :date="currentCredential.updatedAt" :capitalize="true"
/></n8n-text>
</el-col> </el-col>
</el-row> </el-row>
<el-row v-if="currentCredential"> <el-row v-if="currentCredential">
@@ -106,5 +113,4 @@ export default Vue.extend({
.valueLabel { .valueLabel {
font-weight: var(--font-weight-regular); font-weight: var(--font-weight-regular);
} }
</style> </style>

View File

@@ -1,11 +1,13 @@
<template> <template>
<div @keydown.stop :class="$style.container" v-if="credentialProperties.length"> <div @keydown.stop :class="$style.container" v-if="credentialProperties.length">
<form v-for="parameter in credentialProperties" :key="parameter.name" autocomplete="off" data-test-id="credential-connection-parameter"> <form
v-for="parameter in credentialProperties"
:key="parameter.name"
autocomplete="off"
data-test-id="credential-connection-parameter"
>
<!-- Why form? to break up inputs, to prevent Chrome autofill --> <!-- Why form? to break up inputs, to prevent Chrome autofill -->
<n8n-notice <n8n-notice v-if="parameter.type === 'notice'" :content="parameter.displayName" />
v-if="parameter.type === 'notice'"
:content="parameter.displayName"
/>
<parameter-input-expanded <parameter-input-expanded
v-else v-else
:parameter="parameter" :parameter="parameter"

View File

@@ -2,7 +2,9 @@
<div :class="$style.container"> <div :class="$style.container">
<div v-if="isDefaultUser"> <div v-if="isDefaultUser">
<n8n-action-box <n8n-action-box
:description="$locale.baseText('credentialEdit.credentialSharing.isDefaultUser.description')" :description="
$locale.baseText('credentialEdit.credentialSharing.isDefaultUser.description')
"
:buttonText="$locale.baseText('credentialEdit.credentialSharing.isDefaultUser.button')" :buttonText="$locale.baseText('credentialEdit.credentialSharing.isDefaultUser.button')"
@click="goToUsersSettings" @click="goToUsersSettings"
/> />
@@ -13,10 +15,17 @@
{{ $locale.baseText('credentialEdit.credentialSharing.info.owner') }} {{ $locale.baseText('credentialEdit.credentialSharing.info.owner') }}
</template> </template>
<template v-else> <template v-else>
{{ $locale.baseText('credentialEdit.credentialSharing.info.sharee', { interpolate: { credentialOwnerName } }) }} {{
$locale.baseText('credentialEdit.credentialSharing.info.sharee', {
interpolate: { credentialOwnerName },
})
}}
</template> </template>
</n8n-info-tip> </n8n-info-tip>
<n8n-info-tip :bold="false" v-if="!credentialPermissions.isOwner && credentialPermissions.isInstanceOwner"> <n8n-info-tip
:bold="false"
v-if="!credentialPermissions.isOwner && credentialPermissions.isInstanceOwner"
>
{{ $locale.baseText('credentialEdit.credentialSharing.info.instanceOwner') }} {{ $locale.baseText('credentialEdit.credentialSharing.info.instanceOwner') }}
</n8n-info-tip> </n8n-info-tip>
<n8n-user-select <n8n-user-select
@@ -43,31 +52,35 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import {IUser} from "@/Interface"; import { IUser } from '@/Interface';
import mixins from "vue-typed-mixins"; import mixins from 'vue-typed-mixins';
import {showMessage} from "@/mixins/showMessage"; import { showMessage } from '@/mixins/showMessage';
import { mapStores } from 'pinia'; import { mapStores } from 'pinia';
import { useUsersStore } from '@/stores/users'; import { useUsersStore } from '@/stores/users';
import { useCredentialsStore } from "@/stores/credentials"; import { useCredentialsStore } from '@/stores/credentials';
import {VIEWS} from "@/constants"; import { VIEWS } from '@/constants';
export default mixins( export default mixins(showMessage).extend({
showMessage,
).extend({
name: 'CredentialSharing', name: 'CredentialSharing',
props: ['credential', 'credentialId', 'credentialData', 'sharedWith', 'credentialPermissions', 'modalBus'], props: [
'credential',
'credentialId',
'credentialData',
'sharedWith',
'credentialPermissions',
'modalBus',
],
computed: { computed: {
...mapStores( ...mapStores(useCredentialsStore, useUsersStore),
useCredentialsStore,
useUsersStore,
),
isDefaultUser(): boolean { isDefaultUser(): boolean {
return this.usersStore.isDefaultUser; return this.usersStore.isDefaultUser;
}, },
usersList(): IUser[] { usersList(): IUser[] {
return this.usersStore.allUsers.filter((user: IUser) => { return this.usersStore.allUsers.filter((user: IUser) => {
const isCurrentUser = user.id === this.usersStore.currentUser?.id; const isCurrentUser = user.id === this.usersStore.currentUser?.id;
const isAlreadySharedWithUser = (this.credentialData.sharedWith || []).find((sharee: IUser) => sharee.id === user.id); const isAlreadySharedWithUser = (this.credentialData.sharedWith || []).find(
(sharee: IUser) => sharee.id === user.id,
);
return !isCurrentUser && !isAlreadySharedWithUser; return !isCurrentUser && !isAlreadySharedWithUser;
}); });
@@ -94,17 +107,26 @@ export default mixins(
if (user) { if (user) {
const confirm = await this.confirmMessage( const confirm = await this.confirmMessage(
this.$locale.baseText('credentialEdit.credentialSharing.list.delete.confirm.message', { interpolate: { name: user.fullName || '' } }), this.$locale.baseText('credentialEdit.credentialSharing.list.delete.confirm.message', {
interpolate: { name: user.fullName || '' },
}),
this.$locale.baseText('credentialEdit.credentialSharing.list.delete.confirm.title'), this.$locale.baseText('credentialEdit.credentialSharing.list.delete.confirm.title'),
null, null,
this.$locale.baseText('credentialEdit.credentialSharing.list.delete.confirm.confirmButtonText'), this.$locale.baseText(
this.$locale.baseText('credentialEdit.credentialSharing.list.delete.confirm.cancelButtonText'), 'credentialEdit.credentialSharing.list.delete.confirm.confirmButtonText',
),
this.$locale.baseText(
'credentialEdit.credentialSharing.list.delete.confirm.cancelButtonText',
),
); );
if (confirm) { if (confirm) {
this.$emit('change', this.credentialData.sharedWith.filter((sharee: IUser) => { this.$emit(
return sharee.id !== user.id; 'change',
})); this.credentialData.sharedWith.filter((sharee: IUser) => {
return sharee.id !== user.id;
}),
);
} }
} }
}, },

View File

@@ -28,9 +28,7 @@ export default Vue.extend({
}, },
}, },
computed: { computed: {
...mapStores( ...mapStores(useRootStore),
useRootStore,
),
basePath(): string { basePath(): string {
return this.rootStore.baseUrl; return this.rootStore.baseUrl;
}, },

View File

@@ -21,11 +21,7 @@ export default Vue.extend({
}, },
}, },
computed: { computed: {
...mapStores( ...mapStores(useCredentialsStore, useNodeTypesStore, useRootStore),
useCredentialsStore,
useNodeTypesStore,
useRootStore,
),
credentialWithIcon(): ICredentialType | null { credentialWithIcon(): ICredentialType | null {
return this.credentialTypeName ? this.getCredentialWithIcon(this.credentialTypeName) : null; return this.credentialTypeName ? this.getCredentialWithIcon(this.credentialTypeName) : null;
}, },
@@ -38,7 +34,7 @@ export default Vue.extend({
return this.rootStore.getBaseUrl + iconUrl; return this.rootStore.getBaseUrl + iconUrl;
}, },
relevantNode(): INodeTypeDescription | null { relevantNode(): INodeTypeDescription | null {
if (this.credentialWithIcon?.icon?.startsWith('node:')) { if (this.credentialWithIcon?.icon?.startsWith('node:')) {
const nodeType = this.credentialWithIcon.icon.replace('node:', ''); const nodeType = this.credentialWithIcon.icon.replace('node:', '');
return this.nodeTypesStore.getNodeType(nodeType); return this.nodeTypesStore.getNodeType(nodeType);
@@ -70,7 +66,7 @@ export default Vue.extend({
if (type.extends) { if (type.extends) {
let parentCred = null; let parentCred = null;
type.extends.forEach(name => { type.extends.forEach((name) => {
parentCred = this.getCredentialWithIcon(name); parentCred = this.getCredentialWithIcon(name);
if (parentCred !== null) return; if (parentCred !== null) return;
}); });

View File

@@ -5,7 +5,9 @@
:size="inputSize" :size="inputSize"
filterable filterable
:value="displayValue" :value="displayValue"
:placeholder="parameter.placeholder ? getPlaceholder() : $locale.baseText('parameterInput.select')" :placeholder="
parameter.placeholder ? getPlaceholder() : $locale.baseText('parameterInput.select')
"
:title="displayTitle" :title="displayTitle"
:disabled="isReadOnly" :disabled="isReadOnly"
ref="innerSelect" ref="innerSelect"
@@ -76,9 +78,7 @@ export default Vue.extend({
'displayTitle', 'displayTitle',
], ],
computed: { computed: {
...mapStores( ...mapStores(useCredentialsStore),
useCredentialsStore,
),
allCredentialTypes(): ICredentialType[] { allCredentialTypes(): ICredentialType[] {
return this.credentialsStore.allCredentialTypes; return this.credentialsStore.allCredentialTypes;
}, },
@@ -93,7 +93,7 @@ export default Vue.extend({
}, },
methods: { methods: {
focus() { focus() {
const select = this.$refs.innerSelect as Vue & HTMLElement | undefined; const select = this.$refs.innerSelect as (Vue & HTMLElement) | undefined;
if (select) { if (select) {
select.focus(); select.focus();
} }
@@ -109,7 +109,6 @@ export default Vue.extend({
for (const property of supported.has) { for (const property of supported.has) {
if (checkedCredType[property as keyof ICredentialType] !== undefined) { if (checkedCredType[property as keyof ICredentialType] !== undefined) {
// edge case: `httpHeaderAuth` has `authenticate` auth but belongs to generic auth // edge case: `httpHeaderAuth` has `authenticate` auth but belongs to generic auth
if (name === 'httpHeaderAuth' && property === 'authenticate') continue; if (name === 'httpHeaderAuth' && property === 'authenticate') continue;
@@ -119,9 +118,7 @@ export default Vue.extend({
if ( if (
checkedCredType.extends && checkedCredType.extends &&
checkedCredType.extends.some( checkedCredType.extends.some((parentType: string) => supported.extends.includes(parentType))
(parentType: string) => supported.extends.includes(parentType),
)
) { ) {
return true; return true;
} }
@@ -138,23 +135,26 @@ export default Vue.extend({
return false; return false;
}, },
getSupportedSets(credentialTypes: string[]) { getSupportedSets(credentialTypes: string[]) {
return credentialTypes.reduce<{ extends: string[]; has: string[] }>((acc, cur) => { return credentialTypes.reduce<{ extends: string[]; has: string[] }>(
const _extends = cur.split('extends:'); (acc, cur) => {
const _extends = cur.split('extends:');
if (_extends.length === 2) {
acc.extends.push(_extends[1]);
return acc;
}
const _has = cur.split('has:');
if (_has.length === 2) {
acc.has.push(_has[1]);
return acc;
}
if (_extends.length === 2) {
acc.extends.push(_extends[1]);
return acc; return acc;
} },
{ extends: [], has: [] },
const _has = cur.split('has:'); );
if (_has.length === 2) {
acc.has.push(_has[1]);
return acc;
}
return acc;
}, { extends: [], has: [] });
}, },
}, },
}); });
@@ -165,5 +165,4 @@ export default Vue.extend({
display: flex; display: flex;
align-items: center; align-items: center;
} }
</style> </style>

View File

@@ -9,11 +9,15 @@
minHeight="250px" minHeight="250px"
> >
<template #header> <template #header>
<h2 :class="$style.title">{{ $locale.baseText('credentialSelectModal.addNewCredential') }}</h2> <h2 :class="$style.title">
{{ $locale.baseText('credentialSelectModal.addNewCredential') }}
</h2>
</template> </template>
<template #content> <template #content>
<div> <div>
<div :class="$style.subtitle">{{ $locale.baseText('credentialSelectModal.selectAnAppOrServiceToConnectTo') }}</div> <div :class="$style.subtitle">
{{ $locale.baseText('credentialSelectModal.selectAnAppOrServiceToConnectTo') }}
</div>
<n8n-select <n8n-select
filterable filterable
defaultFirstOption defaultFirstOption
@@ -73,8 +77,7 @@ export default mixins(externalHooks).extend({
async mounted() { async mounted() {
try { try {
await this.credentialsStore.fetchCredentialTypes(false); await this.credentialsStore.fetchCredentialTypes(false);
} catch (e) { } catch (e) {}
}
this.loading = false; this.loading = false;
setTimeout(() => { setTimeout(() => {
@@ -93,17 +96,13 @@ export default mixins(externalHooks).extend({
}; };
}, },
computed: { computed: {
...mapStores( ...mapStores(useCredentialsStore, useUIStore, useWorkflowsStore),
useCredentialsStore,
useUIStore,
useWorkflowsStore,
),
}, },
methods: { methods: {
onSelect(type: string) { onSelect(type: string) {
this.selected = type; this.selected = type;
}, },
openCredentialType () { openCredentialType() {
this.modalBus.$emit('close'); this.modalBus.$emit('close');
this.uiStore.openNewCredential(this.selected); this.uiStore.openNewCredential(this.selected);

View File

@@ -10,12 +10,20 @@
<template #content> <template #content>
<div> <div>
<div v-if="isPending"> <div v-if="isPending">
<n8n-text color="text-base">{{ $locale.baseText('settings.users.confirmUserDeletion') }}</n8n-text> <n8n-text color="text-base">{{
$locale.baseText('settings.users.confirmUserDeletion')
}}</n8n-text>
</div> </div>
<div :class="$style.content" v-else> <div :class="$style.content" v-else>
<div><n8n-text color="text-base">{{ $locale.baseText('settings.users.confirmDataHandlingAfterDeletion') }}</n8n-text></div> <div>
<n8n-text color="text-base">{{
$locale.baseText('settings.users.confirmDataHandlingAfterDeletion')
}}</n8n-text>
</div>
<el-radio :value="operation" label="transfer" @change="() => setOperation('transfer')"> <el-radio :value="operation" label="transfer" @change="() => setOperation('transfer')">
<n8n-text color="text-dark">{{ $locale.baseText('settings.users.transferWorkflowsAndCredentials') }}</n8n-text> <n8n-text color="text-dark">{{
$locale.baseText('settings.users.transferWorkflowsAndCredentials')
}}</n8n-text>
</el-radio> </el-radio>
<div :class="$style.optionInput" v-if="operation === 'transfer'"> <div :class="$style.optionInput" v-if="operation === 'transfer'">
<n8n-input-label :label="$locale.baseText('settings.users.userToTransferTo')"> <n8n-input-label :label="$locale.baseText('settings.users.userToTransferTo')">
@@ -29,38 +37,49 @@
</n8n-input-label> </n8n-input-label>
</div> </div>
<el-radio :value="operation" label="delete" @change="() => setOperation('delete')"> <el-radio :value="operation" label="delete" @change="() => setOperation('delete')">
<n8n-text color="text-dark">{{ $locale.baseText('settings.users.deleteWorkflowsAndCredentials') }}</n8n-text> <n8n-text color="text-dark">{{
$locale.baseText('settings.users.deleteWorkflowsAndCredentials')
}}</n8n-text>
</el-radio> </el-radio>
<div :class="$style.optionInput" v-if="operation === 'delete'"> <div :class="$style.optionInput" v-if="operation === 'delete'">
<n8n-input-label :label="$locale.baseText('settings.users.deleteConfirmationMessage')"> <n8n-input-label :label="$locale.baseText('settings.users.deleteConfirmationMessage')">
<n8n-input :value="deleteConfirmText" :placeholder="$locale.baseText('settings.users.deleteConfirmationText')" @input="setConfirmText" /> <n8n-input
:value="deleteConfirmText"
:placeholder="$locale.baseText('settings.users.deleteConfirmationText')"
@input="setConfirmText"
/>
</n8n-input-label> </n8n-input-label>
</div> </div>
</div> </div>
</div> </div>
</template> </template>
<template #footer> <template #footer>
<n8n-button :loading="loading" :disabled="!enabled" :label="$locale.baseText('settings.users.delete')" @click="onSubmit" float="right" /> <n8n-button
:loading="loading"
:disabled="!enabled"
:label="$locale.baseText('settings.users.delete')"
@click="onSubmit"
float="right"
/>
</template> </template>
</Modal> </Modal>
</template> </template>
<script lang="ts"> <script lang="ts">
import mixins from "vue-typed-mixins"; import mixins from 'vue-typed-mixins';
import { showMessage } from "@/mixins/showMessage"; import { showMessage } from '@/mixins/showMessage';
import Modal from "./Modal.vue"; import Modal from './Modal.vue';
import Vue from "vue"; import Vue from 'vue';
import { IUser } from "../Interface"; import { IUser } from '../Interface';
import { mapStores } from "pinia"; import { mapStores } from 'pinia';
import { useUsersStore } from '@/stores/users'; import { useUsersStore } from '@/stores/users';
export default mixins(showMessage).extend({ export default mixins(showMessage).extend({
components: { components: {
Modal, Modal,
}, },
name: "DeleteUserModal", name: 'DeleteUserModal',
props: { props: {
modalName: { modalName: {
type: String, type: String,
@@ -88,17 +107,18 @@ export default mixins(showMessage).extend({
return this.userToDelete ? this.userToDelete && !this.userToDelete.firstName : false; return this.userToDelete ? this.userToDelete && !this.userToDelete.firstName : false;
}, },
title(): string { title(): string {
const user = this.userToDelete && (this.userToDelete.fullName || this.userToDelete.email) || ''; const user =
return this.$locale.baseText( (this.userToDelete && (this.userToDelete.fullName || this.userToDelete.email)) || '';
'settings.users.deleteUser', return this.$locale.baseText('settings.users.deleteUser', { interpolate: { user } });
{ interpolate: { user }},
);
}, },
enabled(): boolean { enabled(): boolean {
if (this.isPending) { if (this.isPending) {
return true; return true;
} }
if (this.operation === 'delete' && this.deleteConfirmText === this.$locale.baseText('settings.users.deleteConfirmationText')) { if (
this.operation === 'delete' &&
this.deleteConfirmText === this.$locale.baseText('settings.users.deleteConfirmationText')
) {
return true; return true;
} }
@@ -128,7 +148,7 @@ export default mixins(showMessage).extend({
this.loading = true; this.loading = true;
const params = {id: this.activeId} as {id: string, transferId?: string}; const params = { id: this.activeId } as { id: string; transferId?: string };
if (this.operation === 'transfer') { if (this.operation === 'transfer') {
params.transferId = this.transferId; params.transferId = this.transferId;
} }
@@ -139,10 +159,9 @@ export default mixins(showMessage).extend({
if (this.transferId) { if (this.transferId) {
const transferUser: IUser | null = this.usersStore.getUserById(this.transferId); const transferUser: IUser | null = this.usersStore.getUserById(this.transferId);
if (transferUser) { if (transferUser) {
message = this.$locale.baseText( message = this.$locale.baseText('settings.users.transferredToUser', {
'settings.users.transferredToUser', interpolate: { user: transferUser.fullName || '' },
{ interpolate: { user: transferUser.fullName || '' }}, });
);
} }
} }
@@ -153,7 +172,6 @@ export default mixins(showMessage).extend({
}); });
this.modalBus.$emit('close'); this.modalBus.$emit('close');
} catch (error) { } catch (error) {
this.$showError(error, this.$locale.baseText('settings.users.userDeletedError')); this.$showError(error, this.$locale.baseText('settings.users.userDeletedError'));
} }
@@ -161,7 +179,6 @@ export default mixins(showMessage).extend({
}, },
}, },
}); });
</script> </script>
<style lang="scss" module> <style lang="scss" module>

View File

@@ -1,18 +1,14 @@
<template> <template>
<component :is="tag" <component
:class="{[$style.dragging]: isDragging }" :is="tag"
:class="{ [$style.dragging]: isDragging }"
@mousedown="onDragStart" @mousedown="onDragStart"
ref="wrapper" ref="wrapper"
> >
<slot :isDragging="isDragging"></slot> <slot :isDragging="isDragging"></slot>
<Teleport to="body"> <Teleport to="body">
<div <div ref="draggable" :class="$style.draggable" :style="draggableStyle" v-show="isDragging">
ref="draggable"
:class="$style.draggable"
:style="draggableStyle"
v-show="isDragging"
>
<slot name="preview" :canDrop="canDrop" :el="draggingEl"></slot> <slot name="preview" :canDrop="canDrop" :el="draggingEl"></slot>
</div> </div>
</Teleport> </Teleport>
@@ -68,9 +64,7 @@ export default Vue.extend({
}; };
}, },
computed: { computed: {
...mapStores( ...mapStores(useNDVStore),
useNDVStore,
),
canDrop(): boolean { canDrop(): boolean {
return this.ndvStore.canDraggableDrop; return this.ndvStore.canDraggableDrop;
}, },
@@ -91,7 +85,9 @@ export default Vue.extend({
this.draggingEl = e.target as HTMLElement; this.draggingEl = e.target as HTMLElement;
if (this.targetDataKey && this.draggingEl.dataset?.target !== this.targetDataKey) { if (this.targetDataKey && this.draggingEl.dataset?.target !== this.targetDataKey) {
this.draggingEl = this.draggingEl.closest(`[data-target="${this.targetDataKey}"]`) as HTMLElement; this.draggingEl = this.draggingEl.closest(
`[data-target="${this.targetDataKey}"]`,
) as HTMLElement;
} }
if (this.targetDataKey && this.draggingEl?.dataset?.target !== this.targetDataKey) { if (this.targetDataKey && this.draggingEl?.dataset?.target !== this.targetDataKey) {
@@ -116,11 +112,12 @@ export default Vue.extend({
return; return;
} }
if(!this.isDragging) { if (!this.isDragging) {
this.isDragging = true; this.isDragging = true;
const data = this.targetDataKey && this.draggingEl ? this.draggingEl.dataset.value : (this.data || ''); const data =
this.ndvStore.draggableStartDragging({type: this.type, data: data || '' }); this.targetDataKey && this.draggingEl ? this.draggingEl.dataset.value : this.data || '';
this.ndvStore.draggableStartDragging({ type: this.type, data: data || '' });
this.$emit('dragstart', this.draggingEl); this.$emit('dragstart', this.draggingEl);
document.body.style.cursor = 'grabbing'; document.body.style.cursor = 'grabbing';
@@ -128,7 +125,7 @@ export default Vue.extend({
this.animationFrameId = window.requestAnimationFrame(() => { this.animationFrameId = window.requestAnimationFrame(() => {
if (this.canDrop && this.stickyPosition) { if (this.canDrop && this.stickyPosition) {
this.draggablePosition = { x: this.stickyPosition[0], y: this.stickyPosition[1]}; this.draggablePosition = { x: this.stickyPosition[0], y: this.stickyPosition[1] };
} else { } else {
this.draggablePosition = { x: e.pageX, y: e.pageY }; this.draggablePosition = { x: e.pageX, y: e.pageY };
} }

View File

@@ -39,9 +39,7 @@ export default Vue.extend({
window.removeEventListener('mouseup', this.onMouseUp); window.removeEventListener('mouseup', this.onMouseUp);
}, },
computed: { computed: {
...mapStores( ...mapStores(useNDVStore),
useNDVStore,
),
isDragging(): boolean { isDragging(): boolean {
return this.ndvStore.isDraggableDragging; return this.ndvStore.isDraggableDragging;
}, },
@@ -62,10 +60,17 @@ export default Vue.extend({
if (target && this.isDragging) { if (target && this.isDragging) {
const dim = target.getBoundingClientRect(); const dim = target.getBoundingClientRect();
this.hovering = e.clientX >= dim.left && e.clientX <= dim.right && e.clientY >= dim.top && e.clientY <= dim.bottom; this.hovering =
e.clientX >= dim.left &&
e.clientX <= dim.right &&
e.clientY >= dim.top &&
e.clientY <= dim.bottom;
if (!this.disabled && this.sticky && this.hovering) { if (!this.disabled && this.sticky && this.hovering) {
this.ndvStore.setDraggableStickyPos([dim.left + this.stickyOffset, dim.top + this.stickyOffset]); this.ndvStore.setDraggableStickyPos([
dim.left + this.stickyOffset,
dim.top + this.stickyOffset,
]);
} }
} }
}, },

View File

@@ -30,32 +30,43 @@
</template> </template>
<template #footer="{ close }"> <template #footer="{ close }">
<div :class="$style.footer"> <div :class="$style.footer">
<n8n-button @click="save" :loading="isSaving" :label="$locale.baseText('duplicateWorkflowDialog.save')" float="right" /> <n8n-button
<n8n-button type="secondary" @click="close" :disabled="isSaving" :label="$locale.baseText('duplicateWorkflowDialog.cancel')" float="right" /> @click="save"
:loading="isSaving"
:label="$locale.baseText('duplicateWorkflowDialog.save')"
float="right"
/>
<n8n-button
type="secondary"
@click="close"
:disabled="isSaving"
:label="$locale.baseText('duplicateWorkflowDialog.cancel')"
float="right"
/>
</div> </div>
</template> </template>
</Modal> </Modal>
</template> </template>
<script lang="ts"> <script lang="ts">
import Vue from "vue"; import Vue from 'vue';
import mixins from "vue-typed-mixins"; import mixins from 'vue-typed-mixins';
import { MAX_WORKFLOW_NAME_LENGTH, PLACEHOLDER_EMPTY_WORKFLOW_ID } from "@/constants"; import { MAX_WORKFLOW_NAME_LENGTH, PLACEHOLDER_EMPTY_WORKFLOW_ID } from '@/constants';
import { workflowHelpers } from "@/mixins/workflowHelpers"; import { workflowHelpers } from '@/mixins/workflowHelpers';
import { showMessage } from "@/mixins/showMessage"; import { showMessage } from '@/mixins/showMessage';
import TagsDropdown from "@/components/TagsDropdown.vue"; import TagsDropdown from '@/components/TagsDropdown.vue';
import Modal from "./Modal.vue"; import Modal from './Modal.vue';
import {restApi} from "@/mixins/restApi"; import { restApi } from '@/mixins/restApi';
import { mapStores } from "pinia"; import { mapStores } from 'pinia';
import { useSettingsStore } from "@/stores/settings"; import { useSettingsStore } from '@/stores/settings';
import { useWorkflowsStore } from "@/stores/workflows"; import { useWorkflowsStore } from '@/stores/workflows';
import { IWorkflowDataUpdate } from "@/Interface"; import { IWorkflowDataUpdate } from '@/Interface';
export default mixins(showMessage, workflowHelpers, restApi).extend({ export default mixins(showMessage, workflowHelpers, restApi).extend({
components: { TagsDropdown, Modal }, components: { TagsDropdown, Modal },
name: "DuplicateWorkflow", name: 'DuplicateWorkflow',
props: ["modalName", "isActive", "data"], props: ['modalName', 'isActive', 'data'],
data() { data() {
const currentTagIds = this.data.tags; const currentTagIds = this.data.tags;
@@ -74,10 +85,7 @@ export default mixins(showMessage, workflowHelpers, restApi).extend({
this.$nextTick(() => this.focusOnNameInput()); this.$nextTick(() => this.focusOnNameInput());
}, },
computed: { computed: {
...mapStores( ...mapStores(useSettingsStore, useWorkflowsStore),
useSettingsStore,
useWorkflowsStore,
),
}, },
watch: { watch: {
isActive(active) { isActive(active) {
@@ -112,7 +120,7 @@ export default mixins(showMessage, workflowHelpers, restApi).extend({
this.$showMessage({ this.$showMessage({
title: this.$locale.baseText('duplicateWorkflowDialog.errors.missingName.title'), title: this.$locale.baseText('duplicateWorkflowDialog.errors.missingName.title'),
message: this.$locale.baseText('duplicateWorkflowDialog.errors.missingName.message'), message: this.$locale.baseText('duplicateWorkflowDialog.errors.missingName.message'),
type: "error", type: 'error',
}); });
return; return;
@@ -125,10 +133,14 @@ export default mixins(showMessage, workflowHelpers, restApi).extend({
try { try {
let workflowToUpdate: IWorkflowDataUpdate | undefined; let workflowToUpdate: IWorkflowDataUpdate | undefined;
if (currentWorkflowId !== PLACEHOLDER_EMPTY_WORKFLOW_ID) { if (currentWorkflowId !== PLACEHOLDER_EMPTY_WORKFLOW_ID) {
const { createdAt, updatedAt, usedCredentials, ...workflow } = await this.restApi().getWorkflow(this.data.id); const { createdAt, updatedAt, usedCredentials, ...workflow } =
await this.restApi().getWorkflow(this.data.id);
workflowToUpdate = workflow; workflowToUpdate = workflow;
this.removeForeignCredentialsFromWorkflow(workflowToUpdate, this.credentialsStore.allCredentials); this.removeForeignCredentialsFromWorkflow(
workflowToUpdate,
this.credentialsStore.allCredentials,
);
} }
const saved = await this.saveAsNewWorkflow({ const saved = await this.saveAsNewWorkflow({
@@ -166,7 +178,7 @@ export default mixins(showMessage, workflowHelpers, restApi).extend({
} }
}, },
closeDialog(): void { closeDialog(): void {
this.modalBus.$emit("close"); this.modalBus.$emit('close');
}, },
}, },
}); });

View File

@@ -7,7 +7,7 @@
<script lang="ts"> <script lang="ts">
import Vue from 'vue'; import Vue from 'vue';
import {EnterpriseEditionFeature} from "@/constants"; import { EnterpriseEditionFeature } from '@/constants';
import { mapStores } from 'pinia'; import { mapStores } from 'pinia';
import { useSettingsStore } from '@/stores/settings'; import { useSettingsStore } from '@/stores/settings';
@@ -23,7 +23,10 @@ export default Vue.extend({
...mapStores(useSettingsStore), ...mapStores(useSettingsStore),
canAccess(): boolean { canAccess(): boolean {
return this.features.reduce((acc: boolean, feature) => { return this.features.reduce((acc: boolean, feature) => {
return acc && !!this.settingsStore.isEnterpriseFeatureEnabled(feature as EnterpriseEditionFeature); return (
acc &&
!!this.settingsStore.isEnterpriseFeatureEnabled(feature as EnterpriseEditionFeature)
);
}, true); }, true);
}, },
}, },

View File

@@ -6,13 +6,14 @@
</div> </div>
<details> <details>
<summary class="error-details__summary"> <summary class="error-details__summary">
<font-awesome-icon class="error-details__icon" icon="angle-right" /> {{ $locale.baseText('nodeErrorView.details') }} <font-awesome-icon class="error-details__icon" icon="angle-right" />
{{ $locale.baseText('nodeErrorView.details') }}
</summary> </summary>
<div class="error-details__content"> <div class="error-details__content">
<div v-if="error.context && error.context.causeDetailed"> <div v-if="error.context && error.context.causeDetailed">
<el-card class="box-card" shadow="never"> <el-card class="box-card" shadow="never">
<div> <div>
{{error.context.causeDetailed}} {{ error.context.causeDetailed }}
</div> </div>
</el-card> </el-card>
</div> </div>
@@ -24,22 +25,33 @@
</div> </div>
</template> </template>
<div> <div>
{{new Date(error.timestamp).toLocaleString()}} {{ new Date(error.timestamp).toLocaleString() }}
</div> </div>
</el-card> </el-card>
</div> </div>
<div v-if="error.context && error.context.itemIndex !== undefined" class="el-card box-card is-never-shadow el-card__body"> <div
<span class="error-details__summary">{{ $locale.baseText('nodeErrorView.itemIndex') }}:</span> v-if="error.context && error.context.itemIndex !== undefined"
{{error.context.itemIndex}} class="el-card box-card is-never-shadow el-card__body"
<span v-if="error.context.runIndex"> >
| <span class="error-details__summary">{{ $locale.baseText('nodeErrorView.itemIndex') }}:</span> <span class="error-details__summary"
{{error.context.runIndex}} >{{ $locale.baseText('nodeErrorView.itemIndex') }}:</span
</span> >
<span v-if="error.context.parameter"> {{ error.context.itemIndex }}
| <span class="error-details__summary">{{ $locale.baseText('nodeErrorView.inParameter') }}:</span> <span v-if="error.context.runIndex">
{{ parameterDisplayName(error.context.parameter) }} |
</span> <span class="error-details__summary"
</div> >{{ $locale.baseText('nodeErrorView.itemIndex') }}:</span
>
{{ error.context.runIndex }}
</span>
<span v-if="error.context.parameter">
|
<span class="error-details__summary"
>{{ $locale.baseText('nodeErrorView.inParameter') }}:</span
>
{{ parameterDisplayName(error.context.parameter) }}
</span>
</div>
<div v-if="error.httpCode"> <div v-if="error.httpCode">
<el-card class="box-card" shadow="never"> <el-card class="box-card" shadow="never">
<template #header> <template #header>
@@ -48,7 +60,7 @@
</div> </div>
</template> </template>
<div> <div>
{{error.httpCode}} {{ error.httpCode }}
</div> </div>
</el-card> </el-card>
</div> </div>
@@ -57,13 +69,19 @@
<template #header> <template #header>
<div class="clearfix box-card__title"> <div class="clearfix box-card__title">
<span>{{ $locale.baseText('nodeErrorView.cause') }}</span> <span>{{ $locale.baseText('nodeErrorView.cause') }}</span>
<br> <br />
<span class="box-card__subtitle">{{ $locale.baseText('nodeErrorView.dataBelowMayContain') }}</span> <span class="box-card__subtitle">{{
$locale.baseText('nodeErrorView.dataBelowMayContain')
}}</span>
</div> </div>
</template> </template>
<div> <div>
<div class="copy-button" v-if="displayCause"> <div class="copy-button" v-if="displayCause">
<n8n-icon-button @click="copyCause" :title="$locale.baseText('nodeErrorView.copyToClipboard')" icon="copy" /> <n8n-icon-button
@click="copyCause"
:title="$locale.baseText('nodeErrorView.copyToClipboard')"
icon="copy"
/>
</div> </div>
<vue-json-pretty <vue-json-pretty
v-if="displayCause" v-if="displayCause"
@@ -75,7 +93,9 @@
class="json-data" class="json-data"
/> />
<span v-else> <span v-else>
<font-awesome-icon icon="info-circle" />{{ $locale.baseText('nodeErrorView.theErrorCauseIsTooLargeToBeDisplayed') }} <font-awesome-icon icon="info-circle" />{{
$locale.baseText('nodeErrorView.theErrorCauseIsTooLargeToBeDisplayed')
}}
</span> </span>
</div> </div>
</el-card> </el-card>
@@ -103,43 +123,27 @@ import VueJsonPretty from 'vue-json-pretty';
import { copyPaste } from '@/mixins/copyPaste'; import { copyPaste } from '@/mixins/copyPaste';
import { showMessage } from '@/mixins/showMessage'; import { showMessage } from '@/mixins/showMessage';
import mixins from 'vue-typed-mixins'; import mixins from 'vue-typed-mixins';
import { import { MAX_DISPLAY_DATA_SIZE } from '@/constants';
MAX_DISPLAY_DATA_SIZE, import { INodeUi } from '@/Interface';
} from '@/constants';
import {
INodeUi,
} from '@/Interface';
import { import { INodeProperties, INodePropertyCollection, INodePropertyOptions } from 'n8n-workflow';
INodeProperties,
INodePropertyCollection,
INodePropertyOptions,
} from 'n8n-workflow';
import { sanitizeHtml } from '@/utils'; import { sanitizeHtml } from '@/utils';
import { mapStores } from 'pinia'; import { mapStores } from 'pinia';
import { useNDVStore } from '@/stores/ndv'; import { useNDVStore } from '@/stores/ndv';
import { useNodeTypesStore } from '@/stores/nodeTypes'; import { useNodeTypesStore } from '@/stores/nodeTypes';
export default mixins( export default mixins(copyPaste, showMessage).extend({
copyPaste,
showMessage,
).extend({
name: 'NodeErrorView', name: 'NodeErrorView',
props: [ props: ['error'],
'error',
],
components: { components: {
VueJsonPretty, VueJsonPretty,
}, },
computed: { computed: {
...mapStores( ...mapStores(useNodeTypesStore, useNDVStore),
useNodeTypesStore,
useNDVStore,
),
displayCause(): boolean { displayCause(): boolean {
return JSON.stringify(this.error.cause).length < MAX_DISPLAY_DATA_SIZE; return JSON.stringify(this.error.cause).length < MAX_DISPLAY_DATA_SIZE;
}, },
parameters (): INodeProperties[] { parameters(): INodeProperties[] {
const node = this.ndvStore.activeNode; const node = this.ndvStore.activeNode;
if (!node) { if (!node) {
return []; return [];
@@ -154,20 +158,24 @@ export default mixins(
}, },
}, },
methods: { methods: {
replacePlaceholders (parameter: string, message: string): string { replacePlaceholders(parameter: string, message: string): string {
const parameterName = this.parameterDisplayName(parameter, false); const parameterName = this.parameterDisplayName(parameter, false);
const parameterFullName = this.parameterDisplayName(parameter, true); const parameterFullName = this.parameterDisplayName(parameter, true);
return message.replace(/%%PARAMETER%%/g, parameterName).replace(/%%PARAMETER_FULL%%/g, parameterFullName); return message
.replace(/%%PARAMETER%%/g, parameterName)
.replace(/%%PARAMETER_FULL%%/g, parameterFullName);
}, },
getErrorDescription (): string { getErrorDescription(): string {
if (!this.error.context || !this.error.context.descriptionTemplate) { if (!this.error.context || !this.error.context.descriptionTemplate) {
return sanitizeHtml(this.error.description); return sanitizeHtml(this.error.description);
} }
const parameterName = this.parameterDisplayName(this.error.context.parameter); const parameterName = this.parameterDisplayName(this.error.context.parameter);
return sanitizeHtml(this.error.context.descriptionTemplate.replace(/%%PARAMETER%%/g, parameterName)); return sanitizeHtml(
this.error.context.descriptionTemplate.replace(/%%PARAMETER%%/g, parameterName),
);
}, },
getErrorMessage (): string { getErrorMessage(): string {
const baseErrorMessage = this.$locale.baseText('nodeErrorView.error') + ': '; const baseErrorMessage = this.$locale.baseText('nodeErrorView.error') + ': ';
if (!this.error.context || !this.error.context.messageTemplate) { if (!this.error.context || !this.error.context.messageTemplate) {
@@ -176,7 +184,10 @@ export default mixins(
const parameterName = this.parameterDisplayName(this.error.context.parameter); const parameterName = this.parameterDisplayName(this.error.context.parameter);
return baseErrorMessage + this.error.context.messageTemplate.replace(/%%PARAMETER%%/g, parameterName); return (
baseErrorMessage +
this.error.context.messageTemplate.replace(/%%PARAMETER%%/g, parameterName)
);
}, },
parameterDisplayName(path: string, fullPath = true) { parameterDisplayName(path: string, fullPath = true) {
try { try {
@@ -188,12 +199,15 @@ export default mixins(
if (fullPath === false) { if (fullPath === false) {
return parameters.pop()!.displayName; return parameters.pop()!.displayName;
} }
return parameters.map(parameter => parameter.displayName).join(' > '); return parameters.map((parameter) => parameter.displayName).join(' > ');
} catch (error) { } catch (error) {
return `Could not find parameter "${path}"`; return `Could not find parameter "${path}"`;
} }
}, },
parameterName(parameters: Array<(INodePropertyOptions | INodeProperties | INodePropertyCollection)>, pathParts: string[]): Array<(INodeProperties | INodePropertyCollection)> { parameterName(
parameters: Array<INodePropertyOptions | INodeProperties | INodePropertyCollection>,
pathParts: string[],
): Array<INodeProperties | INodePropertyCollection> {
let currentParameterName = pathParts.shift(); let currentParameterName = pathParts.shift();
if (currentParameterName === undefined) { if (currentParameterName === undefined) {
@@ -204,7 +218,9 @@ export default mixins(
if (arrayMatch !== null && arrayMatch.length > 0) { if (arrayMatch !== null && arrayMatch.length > 0) {
currentParameterName = arrayMatch[1]; currentParameterName = arrayMatch[1];
} }
const currentParameter = parameters.find(parameter => parameter.name === currentParameterName) as unknown as INodeProperties | INodePropertyCollection; const currentParameter = parameters.find(
(parameter) => parameter.name === currentParameterName,
) as unknown as INodeProperties | INodePropertyCollection;
if (currentParameter === undefined) { if (currentParameter === undefined) {
throw new Error(`Could not find parameter "${currentParameterName}"`); throw new Error(`Could not find parameter "${currentParameterName}"`);
@@ -215,11 +231,17 @@ export default mixins(
} }
if (currentParameter.hasOwnProperty('options')) { if (currentParameter.hasOwnProperty('options')) {
return [currentParameter, ...this.parameterName((currentParameter as INodeProperties).options!, pathParts)]; return [
currentParameter,
...this.parameterName((currentParameter as INodeProperties).options!, pathParts),
];
} }
if (currentParameter.hasOwnProperty('values')) { if (currentParameter.hasOwnProperty('values')) {
return [currentParameter, ...this.parameterName((currentParameter as INodePropertyCollection).values, pathParts)]; return [
currentParameter,
...this.parameterName((currentParameter as INodePropertyCollection).values, pathParts),
];
} }
// We can not resolve any deeper so lets stop here and at least return hopefully something useful // We can not resolve any deeper so lets stop here and at least return hopefully something useful
@@ -240,7 +262,6 @@ export default mixins(
</script> </script>
<style lang="scss"> <style lang="scss">
.error-header { .error-header {
margin-bottom: 10px; margin-bottom: 10px;
} }
@@ -260,7 +281,7 @@ export default mixins(
font-weight: 600; font-weight: 600;
font-size: 16px; font-size: 16px;
cursor: pointer; cursor: pointer;
outline:none; outline: none;
} }
.error-details__icon { .error-details__icon {
@@ -268,15 +289,15 @@ export default mixins(
} }
details > summary { details > summary {
list-style-type: none; list-style-type: none;
} }
details > summary::-webkit-details-marker { details > summary::-webkit-details-marker {
display: none; display: none;
} }
details[open] { details[open] {
.error-details__icon { .error-details__icon {
transform: rotate(90deg); transform: rotate(90deg);
} }
} }
@@ -309,5 +330,4 @@ details[open] {
right: 50px; right: 50px;
z-index: 1000; z-index: 1000;
} }
</style> </style>

View File

@@ -1,62 +1,54 @@
<template> <template>
<span> <span>
{{time}} {{ time }}
</span> </span>
</template> </template>
<script lang="ts"> <script lang="ts">
import { genericHelpers } from '@/mixins/genericHelpers'; import { genericHelpers } from '@/mixins/genericHelpers';
import mixins from 'vue-typed-mixins'; import mixins from 'vue-typed-mixins';
export default mixins( export default mixins(genericHelpers).extend({
genericHelpers, name: 'ExecutionTime',
) props: ['startTime'],
.extend({ computed: {
name: 'ExecutionTime', time(): string {
props: [ if (!this.startTime) {
'startTime', return '...';
],
computed: {
time (): string {
if (!this.startTime) {
return '...';
}
const msPassed = this.nowTime - new Date(this.startTime).getTime();
return this.displayTimer(msPassed);
},
},
data () {
return {
nowTime: -1,
intervalTimer: null as null | NodeJS.Timeout,
};
},
mounted () {
this.setNow();
this.intervalTimer = setInterval(() => {
this.setNow();
}, 1000);
},
destroyed () {
// Make sure that the timer gets destroyed once no longer needed
if (this.intervalTimer !== null) {
clearInterval(this.intervalTimer);
} }
const msPassed = this.nowTime - new Date(this.startTime).getTime();
return this.displayTimer(msPassed);
}, },
methods: { },
setNow () { data() {
this.nowTime = (new Date()).getTime(); return {
}, nowTime: -1,
intervalTimer: null as null | NodeJS.Timeout,
};
},
mounted() {
this.setNow();
this.intervalTimer = setInterval(() => {
this.setNow();
}, 1000);
},
destroyed() {
// Make sure that the timer gets destroyed once no longer needed
if (this.intervalTimer !== null) {
clearInterval(this.intervalTimer);
}
},
methods: {
setNow() {
this.nowTime = new Date().getTime();
}, },
}); },
});
</script> </script>
<style lang="scss"> <style lang="scss">
// .data-display-wrapper { // .data-display-wrapper {
// } // }
</style> </style>

View File

@@ -2,65 +2,108 @@
<Modal <Modal
:name="EXECUTIONS_MODAL_KEY" :name="EXECUTIONS_MODAL_KEY"
width="80%" width="80%"
:title="`${$locale.baseText('executionsList.workflowExecutions')} ${combinedExecutions.length}/${finishedExecutionsCountEstimated === true ? '~' : ''}${combinedExecutionsCount}`" :title="`${$locale.baseText('executionsList.workflowExecutions')} ${
combinedExecutions.length
}/${finishedExecutionsCountEstimated === true ? '~' : ''}${combinedExecutionsCount}`"
:eventBus="modalBus" :eventBus="modalBus"
> >
<template #content> <template #content>
<div class="filters"> <div class="filters">
<el-row> <el-row>
<el-col :span="2" class="filter-headline"> <el-col :span="2" class="filter-headline">
{{ $locale.baseText('executionsList.filters') }}: {{ $locale.baseText('executionsList.filters') }}:
</el-col> </el-col>
<el-col :span="7"> <el-col :span="7">
<n8n-select v-model="filter.workflowId" :placeholder="$locale.baseText('executionsList.selectWorkflow')" size="medium" filterable @change="handleFilterChanged"> <n8n-select
v-model="filter.workflowId"
:placeholder="$locale.baseText('executionsList.selectWorkflow')"
size="medium"
filterable
@change="handleFilterChanged"
>
<div class="ph-no-capture"> <div class="ph-no-capture">
<n8n-option <n8n-option
v-for="item in workflows" v-for="item in workflows"
:key="item.id" :key="item.id"
:label="item.name" :label="item.name"
:value="item.id"> :value="item.id"
>
</n8n-option> </n8n-option>
</div> </div>
</n8n-select> </n8n-select>
</el-col> </el-col>
<el-col :span="5" :offset="1"> <el-col :span="5" :offset="1">
<n8n-select v-model="filter.status" :placeholder="$locale.baseText('executionsList.selectStatus')" size="medium" filterable @change="handleFilterChanged"> <n8n-select
v-model="filter.status"
:placeholder="$locale.baseText('executionsList.selectStatus')"
size="medium"
filterable
@change="handleFilterChanged"
>
<n8n-option <n8n-option
v-for="item in statuses" v-for="item in statuses"
:key="item.id" :key="item.id"
:label="item.name" :label="item.name"
:value="item.id"> :value="item.id"
>
</n8n-option> </n8n-option>
</n8n-select> </n8n-select>
</el-col> </el-col>
<el-col :span="4" :offset="5" class="autorefresh"> <el-col :span="4" :offset="5" class="autorefresh">
<el-checkbox v-model="autoRefresh" @change="handleAutoRefreshToggle">{{ $locale.baseText('executionsList.autoRefresh') }}</el-checkbox> <el-checkbox v-model="autoRefresh" @change="handleAutoRefreshToggle">{{
$locale.baseText('executionsList.autoRefresh')
}}</el-checkbox>
</el-col> </el-col>
</el-row> </el-row>
</div> </div>
<div class="selection-options"> <div class="selection-options">
<span v-if="checkAll === true || isIndeterminate === true"> <span v-if="checkAll === true || isIndeterminate === true">
{{ $locale.baseText('executionsList.selected') }}: {{numSelected}} / <span v-if="finishedExecutionsCountEstimated === true">~</span>{{finishedExecutionsCount}} {{ $locale.baseText('executionsList.selected') }}: {{ numSelected }} /
<n8n-icon-button :title="$locale.baseText('executionsList.deleteSelected')" icon="trash" size="mini" @click="handleDeleteSelected" /> <span v-if="finishedExecutionsCountEstimated === true">~</span
>{{ finishedExecutionsCount }}
<n8n-icon-button
:title="$locale.baseText('executionsList.deleteSelected')"
icon="trash"
size="mini"
@click="handleDeleteSelected"
/>
</span> </span>
</div> </div>
<el-table :data="combinedExecutions" stripe v-loading="isDataLoading" :row-class-name="getRowClass"> <el-table
:data="combinedExecutions"
stripe
v-loading="isDataLoading"
:row-class-name="getRowClass"
>
<el-table-column label="" width="30"> <el-table-column label="" width="30">
<!-- eslint-disable-next-line vue/no-unused-vars --> <!-- eslint-disable-next-line vue/no-unused-vars -->
<template #header="scope" > <template #header="scope">
<el-checkbox :indeterminate="isIndeterminate" v-model="checkAll" @change="handleCheckAllChange" label=" "></el-checkbox> <el-checkbox
:indeterminate="isIndeterminate"
v-model="checkAll"
@change="handleCheckAllChange"
label=" "
></el-checkbox>
</template> </template>
<template #default="scope"> <template #default="scope">
<el-checkbox v-if="scope.row.stoppedAt !== undefined && scope.row.id" :value="selectedItems[scope.row.id.toString()] || checkAll" @change="handleCheckboxChanged(scope.row.id)" label=" "></el-checkbox> <el-checkbox
v-if="scope.row.stoppedAt !== undefined && scope.row.id"
:value="selectedItems[scope.row.id.toString()] || checkAll"
@change="handleCheckboxChanged(scope.row.id)"
label=" "
></el-checkbox>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column property="startedAt" :label="$locale.baseText('executionsList.startedAtId')" width="205"> <el-table-column
property="startedAt"
:label="$locale.baseText('executionsList.startedAtId')"
width="205"
>
<template #default="scope"> <template #default="scope">
{{convertToDisplayDate(scope.row.startedAt)}}<br /> {{ convertToDisplayDate(scope.row.startedAt) }}<br />
<small v-if="scope.row.id">ID: {{scope.row.id}}</small> <small v-if="scope.row.id">ID: {{ scope.row.id }}</small>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column property="workflowName" :label="$locale.baseText('executionsList.name')"> <el-table-column property="workflowName" :label="$locale.baseText('executionsList.name')">
@@ -75,16 +118,26 @@
({{ $locale.baseText('executionsList.running') }}) ({{ $locale.baseText('executionsList.running') }})
</span> </span>
<span v-if="scope.row.retryOf !== undefined"> <span v-if="scope.row.retryOf !== undefined">
<br /><small>{{ $locale.baseText('executionsList.retryOf') }} "{{scope.row.retryOf}}"</small> <br /><small
>{{ $locale.baseText('executionsList.retryOf') }} "{{ scope.row.retryOf }}"</small
>
</span> </span>
<span v-else-if="scope.row.retrySuccessId !== undefined"> <span v-else-if="scope.row.retrySuccessId !== undefined">
<br /><small>{{ $locale.baseText('executionsList.successRetry') }} "{{scope.row.retrySuccessId}}"</small> <br /><small
>{{ $locale.baseText('executionsList.successRetry') }} "{{
scope.row.retrySuccessId
}}"</small
>
</span> </span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column :label="$locale.baseText('executionsList.status')" width="122" align="center"> <el-table-column
:label="$locale.baseText('executionsList.status')"
width="122"
align="center"
>
<template #default="scope" align="center"> <template #default="scope" align="center">
<n8n-tooltip placement="top" > <n8n-tooltip placement="top">
<template #content> <template #content>
<div v-html="statusTooltipText(scope.row)"></div> <div v-html="statusTooltipText(scope.row)"></div>
</template> </template>
@@ -108,8 +161,14 @@
<el-dropdown trigger="click" @command="handleRetryClick"> <el-dropdown trigger="click" @command="handleRetryClick">
<span class="retry-button"> <span class="retry-button">
<n8n-icon-button <n8n-icon-button
v-if="scope.row.stoppedAt !== undefined && !scope.row.finished && scope.row.retryOf === undefined && scope.row.retrySuccessId === undefined && !scope.row.waitTill" v-if="
:type="scope.row.stoppedAt === null ? 'warning': 'danger'" scope.row.stoppedAt !== undefined &&
!scope.row.finished &&
scope.row.retryOf === undefined &&
scope.row.retrySuccessId === undefined &&
!scope.row.waitTill
"
:type="scope.row.stoppedAt === null ? 'warning' : 'danger'"
class="ml-3xs" class="ml-3xs"
size="mini" size="mini"
:title="$locale.baseText('executionsList.retryExecution')" :title="$locale.baseText('executionsList.retryExecution')"
@@ -118,35 +177,46 @@
</span> </span>
<template #dropdown> <template #dropdown>
<el-dropdown-menu> <el-dropdown-menu>
<el-dropdown-item :command="{command: 'currentlySaved', row: scope.row}"> <el-dropdown-item :command="{ command: 'currentlySaved', row: scope.row }">
{{ $locale.baseText('executionsList.retryWithCurrentlySavedWorkflow') }} {{ $locale.baseText('executionsList.retryWithCurrentlySavedWorkflow') }}
</el-dropdown-item> </el-dropdown-item>
<el-dropdown-item :command="{command: 'original', row: scope.row}"> <el-dropdown-item :command="{ command: 'original', row: scope.row }">
{{ $locale.baseText('executionsList.retryWithOriginalworkflow') }} {{ $locale.baseText('executionsList.retryWithOriginalworkflow') }}
</el-dropdown-item> </el-dropdown-item>
</el-dropdown-menu> </el-dropdown-menu>
</template> </template>
</el-dropdown> </el-dropdown>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column property="mode" :label="$locale.baseText('executionsList.mode')" width="100" align="center"> <el-table-column
property="mode"
:label="$locale.baseText('executionsList.mode')"
width="100"
align="center"
>
<template #default="scope"> <template #default="scope">
{{ $locale.baseText(`executionsList.modes.${scope.row.mode}`) }} {{ $locale.baseText(`executionsList.modes.${scope.row.mode}`) }}
</template> </template>
</el-table-column> </el-table-column>
<el-table-column :label="$locale.baseText('executionsList.runningTime')" width="150" align="center"> <el-table-column
:label="$locale.baseText('executionsList.runningTime')"
width="150"
align="center"
>
<template #default="scope"> <template #default="scope">
<span v-if="scope.row.stoppedAt === undefined"> <span v-if="scope.row.stoppedAt === undefined">
<font-awesome-icon icon="spinner" spin /> <font-awesome-icon icon="spinner" spin />
<execution-time :start-time="scope.row.startedAt"/> <execution-time :start-time="scope.row.startedAt" />
</span> </span>
<!-- stoppedAt will be null if process crashed --> <!-- stoppedAt will be null if process crashed -->
<span v-else-if="scope.row.stoppedAt === null"> <span v-else-if="scope.row.stoppedAt === null"> -- </span>
--
</span>
<span v-else> <span v-else>
{{ displayTimer(new Date(scope.row.stoppedAt).getTime() - new Date(scope.row.startedAt).getTime(), true) }} {{
displayTimer(
new Date(scope.row.stoppedAt).getTime() - new Date(scope.row.startedAt).getTime(),
true,
)
}}
</span> </span>
</template> </template>
</el-table-column> </el-table-column>
@@ -154,18 +224,41 @@
<template #default="scope"> <template #default="scope">
<div class="actions-container"> <div class="actions-container">
<span v-if="scope.row.stoppedAt === undefined || scope.row.waitTill"> <span v-if="scope.row.stoppedAt === undefined || scope.row.waitTill">
<n8n-icon-button icon="stop" size="small" :title="$locale.baseText('executionsList.stopExecution')" @click.stop="stopExecution(scope.row.id)" :loading="stoppingExecutions.includes(scope.row.id)" /> <n8n-icon-button
icon="stop"
size="small"
:title="$locale.baseText('executionsList.stopExecution')"
@click.stop="stopExecution(scope.row.id)"
:loading="stoppingExecutions.includes(scope.row.id)"
/>
</span> </span>
<span v-if="scope.row.stoppedAt !== undefined && scope.row.id" > <span v-if="scope.row.stoppedAt !== undefined && scope.row.id">
<n8n-icon-button icon="folder-open" size="small" :title="$locale.baseText('executionsList.openPastExecution')" @click.stop="(e) => displayExecution(scope.row, e)" /> <n8n-icon-button
icon="folder-open"
size="small"
:title="$locale.baseText('executionsList.openPastExecution')"
@click.stop="(e) => displayExecution(scope.row, e)"
/>
</span> </span>
</div> </div>
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
<div class="load-more" v-if="finishedExecutionsCount > finishedExecutions.length || finishedExecutionsCountEstimated === true"> <div
<n8n-button icon="sync" :title="$locale.baseText('executionsList.loadMore')" :label="$locale.baseText('executionsList.loadMore')" @click="loadMore()" :loading="isDataLoading" /> class="load-more"
v-if="
finishedExecutionsCount > finishedExecutions.length ||
finishedExecutionsCountEstimated === true
"
>
<n8n-button
icon="sync"
:title="$locale.baseText('executionsList.loadMore')"
:label="$locale.baseText('executionsList.loadMore')"
@click="loadMore()"
:loading="isDataLoading"
/>
</div> </div>
</template> </template>
</Modal> </Modal>
@@ -194,36 +287,25 @@ import {
IWorkflowShortResponse, IWorkflowShortResponse,
} from '@/Interface'; } from '@/Interface';
import { import { convertToDisplayDate } from '@/utils';
convertToDisplayDate,
} from '@/utils';
import { import { IDataObject } from 'n8n-workflow';
IDataObject,
} from 'n8n-workflow';
import { import { range as _range } from 'lodash';
range as _range,
} from 'lodash';
import mixins from 'vue-typed-mixins'; import mixins from 'vue-typed-mixins';
import { mapStores } from 'pinia'; import { mapStores } from 'pinia';
import { useUIStore } from '@/stores/ui'; import { useUIStore } from '@/stores/ui';
import { useWorkflowsStore } from '@/stores/workflows'; import { useWorkflowsStore } from '@/stores/workflows';
export default mixins( export default mixins(externalHooks, genericHelpers, restApi, showMessage).extend({
externalHooks,
genericHelpers,
restApi,
showMessage,
).extend({
name: 'ExecutionsList', name: 'ExecutionsList',
components: { components: {
ExecutionTime, ExecutionTime,
WorkflowActivator, WorkflowActivator,
Modal, Modal,
}, },
data () { data() {
return { return {
finishedExecutions: [] as IExecutionsSummary[], finishedExecutions: [] as IExecutionsSummary[],
finishedExecutionsCount: 0, finishedExecutionsCount: 0,
@@ -242,7 +324,7 @@ export default mixins(
requestItemsPerRequest: 10, requestItemsPerRequest: 10,
selectedItems: {} as { [key: string]: boolean; }, selectedItems: {} as { [key: string]: boolean },
stoppingExecutions: [] as string[], stoppingExecutions: [] as string[],
workflows: [] as IWorkflowShortResponse[], workflows: [] as IWorkflowShortResponse[],
@@ -256,7 +338,9 @@ export default mixins(
this.handleAutoRefreshToggle(); this.handleAutoRefreshToggle();
this.$externalHooks().run('executionsList.openDialog'); this.$externalHooks().run('executionsList.openDialog');
this.$telemetry.track('User opened Executions log', { workflow_id: this.workflowsStore.workflowId }); this.$telemetry.track('User opened Executions log', {
workflow_id: this.workflowsStore.workflowId,
});
}, },
beforeDestroy() { beforeDestroy() {
if (this.autoRefreshInterval) { if (this.autoRefreshInterval) {
@@ -265,11 +349,8 @@ export default mixins(
} }
}, },
computed: { computed: {
...mapStores( ...mapStores(useUIStore, useWorkflowsStore),
useUIStore, statuses() {
useWorkflowsStore,
),
statuses () {
return [ return [
{ {
id: 'ALL', id: 'ALL',
@@ -293,10 +374,10 @@ export default mixins(
}, },
]; ];
}, },
activeExecutions (): IExecutionsCurrentSummaryExtended[] { activeExecutions(): IExecutionsCurrentSummaryExtended[] {
return this.workflowsStore.activeExecutions; return this.workflowsStore.activeExecutions;
}, },
combinedExecutions (): IExecutionsSummary[] { combinedExecutions(): IExecutionsSummary[] {
const returnData: IExecutionsSummary[] = []; const returnData: IExecutionsSummary[] = [];
if (['ALL', 'running'].includes(this.filter.status)) { if (['ALL', 'running'].includes(this.filter.status)) {
@@ -308,17 +389,17 @@ export default mixins(
return returnData; return returnData;
}, },
combinedExecutionsCount (): number { combinedExecutionsCount(): number {
return 0 + this.activeExecutions.length + this.finishedExecutionsCount; return 0 + this.activeExecutions.length + this.finishedExecutionsCount;
}, },
numSelected (): number { numSelected(): number {
if (this.checkAll === true) { if (this.checkAll === true) {
return this.finishedExecutionsCount; return this.finishedExecutionsCount;
} }
return Object.keys(this.selectedItems).length; return Object.keys(this.selectedItems).length;
}, },
isIndeterminate (): boolean { isIndeterminate(): boolean {
if (this.checkAll === true) { if (this.checkAll === true) {
return false; return false;
} }
@@ -328,14 +409,14 @@ export default mixins(
} }
return false; return false;
}, },
workflowFilterCurrent (): IDataObject { workflowFilterCurrent(): IDataObject {
const filter: IDataObject = {}; const filter: IDataObject = {};
if (this.filter.workflowId !== 'ALL') { if (this.filter.workflowId !== 'ALL') {
filter.workflowId = this.filter.workflowId; filter.workflowId = this.filter.workflowId;
} }
return filter; return filter;
}, },
workflowFilterPast (): IDataObject { workflowFilterPast(): IDataObject {
const filter: IDataObject = {}; const filter: IDataObject = {};
if (this.filter.workflowId !== 'ALL') { if (this.filter.workflowId !== 'ALL') {
filter.workflowId = this.filter.workflowId; filter.workflowId = this.filter.workflowId;
@@ -353,47 +434,53 @@ export default mixins(
this.modalBus.$emit('close'); this.modalBus.$emit('close');
}, },
convertToDisplayDate, convertToDisplayDate,
displayExecution (execution: IExecutionShortResponse, e: PointerEvent) { displayExecution(execution: IExecutionShortResponse, e: PointerEvent) {
if (e.metaKey || e.ctrlKey) { if (e.metaKey || e.ctrlKey) {
const route = this.$router.resolve({ name: VIEWS.EXECUTION_PREVIEW, params: { name: execution.workflowId, executionId: execution.id } }); const route = this.$router.resolve({
name: VIEWS.EXECUTION_PREVIEW,
params: { name: execution.workflowId, executionId: execution.id },
});
window.open(route.href, '_blank'); window.open(route.href, '_blank');
return; return;
} }
this.$router.push({ name: VIEWS.EXECUTION_PREVIEW, params: { name: execution.workflowId, executionId: execution.id } }).catch(()=>{});; this.$router
.push({
name: VIEWS.EXECUTION_PREVIEW,
params: { name: execution.workflowId, executionId: execution.id },
})
.catch(() => {});
this.modalBus.$emit('closeAll'); this.modalBus.$emit('closeAll');
}, },
handleAutoRefreshToggle () { handleAutoRefreshToggle() {
if (this.autoRefreshInterval) { if (this.autoRefreshInterval) {
// Clear any previously existing intervals (if any - there shouldn't) // Clear any previously existing intervals (if any - there shouldn't)
clearInterval(this.autoRefreshInterval); clearInterval(this.autoRefreshInterval);
this.autoRefreshInterval = undefined; this.autoRefreshInterval = undefined;
} }
if (this.autoRefresh) { if (this.autoRefresh) {
this.autoRefreshInterval = setInterval(() => this.loadAutoRefresh(), 4 * 1000); // refresh data every 4 secs this.autoRefreshInterval = setInterval(() => this.loadAutoRefresh(), 4 * 1000); // refresh data every 4 secs
} }
}, },
handleCheckAllChange () { handleCheckAllChange() {
if (this.checkAll === false) { if (this.checkAll === false) {
Vue.set(this, 'selectedItems', {}); Vue.set(this, 'selectedItems', {});
} }
}, },
handleCheckboxChanged (executionId: string) { handleCheckboxChanged(executionId: string) {
if (this.selectedItems[executionId]) { if (this.selectedItems[executionId]) {
Vue.delete(this.selectedItems, executionId); Vue.delete(this.selectedItems, executionId);
} else { } else {
Vue.set(this.selectedItems, executionId, true); Vue.set(this.selectedItems, executionId, true);
} }
}, },
async handleDeleteSelected () { async handleDeleteSelected() {
const deleteExecutions = await this.confirmMessage( const deleteExecutions = await this.confirmMessage(
this.$locale.baseText( this.$locale.baseText('executionsList.confirmMessage.message', {
'executionsList.confirmMessage.message', interpolate: { numSelected: this.numSelected.toString() },
{ interpolate: { numSelected: this.numSelected.toString() }}, }),
),
this.$locale.baseText('executionsList.confirmMessage.headline'), this.$locale.baseText('executionsList.confirmMessage.headline'),
'warning', 'warning',
this.$locale.baseText('executionsList.confirmMessage.confirmButtonText'), this.$locale.baseText('executionsList.confirmMessage.confirmButtonText'),
@@ -420,31 +507,40 @@ export default mixins(
let removedCurrentlyLoadedExecution = false; let removedCurrentlyLoadedExecution = false;
let removedActiveExecution = false; let removedActiveExecution = false;
const currentWorkflow: string = this.workflowsStore.workflowId; const currentWorkflow: string = this.workflowsStore.workflowId;
const activeExecution: IExecutionsSummary | null = this.workflowsStore.activeWorkflowExecution; const activeExecution: IExecutionsSummary | null =
this.workflowsStore.activeWorkflowExecution;
// Also update current workflow executions view if needed // Also update current workflow executions view if needed
for (const selectedId of Object.keys(this.selectedItems)) { for (const selectedId of Object.keys(this.selectedItems)) {
const execution: IExecutionsSummary | undefined = this.workflowsStore.getExecutionDataById(selectedId); const execution: IExecutionsSummary | undefined =
this.workflowsStore.getExecutionDataById(selectedId);
if (execution && execution.workflowId === currentWorkflow) { if (execution && execution.workflowId === currentWorkflow) {
this.workflowsStore.deleteExecution(execution); this.workflowsStore.deleteExecution(execution);
removedCurrentlyLoadedExecution = true; removedCurrentlyLoadedExecution = true;
} }
if ((execution !== undefined && activeExecution !== null) && execution.id === activeExecution.id) { if (
execution !== undefined &&
activeExecution !== null &&
execution.id === activeExecution.id
) {
removedActiveExecution = true; removedActiveExecution = true;
} }
} }
// Also update route if needed // Also update route if needed
if (removedCurrentlyLoadedExecution) { if (removedCurrentlyLoadedExecution) {
const currentWorkflowExecutions: IExecutionsSummary[] = this.workflowsStore.currentWorkflowExecutions; const currentWorkflowExecutions: IExecutionsSummary[] =
this.workflowsStore.currentWorkflowExecutions;
if (currentWorkflowExecutions.length === 0) { if (currentWorkflowExecutions.length === 0) {
this.workflowsStore.activeWorkflowExecution = null; this.workflowsStore.activeWorkflowExecution = null;
this.$router.push({ name: VIEWS.EXECUTION_HOME, params: { name: currentWorkflow } }); this.$router.push({ name: VIEWS.EXECUTION_HOME, params: { name: currentWorkflow } });
} else if (removedActiveExecution) { } else if (removedActiveExecution) {
this.workflowsStore.activeWorkflowExecution = currentWorkflowExecutions[0]; this.workflowsStore.activeWorkflowExecution = currentWorkflowExecutions[0];
this.$router.push({ this.$router
name: VIEWS.EXECUTION_PREVIEW, .push({
params: { name: currentWorkflow, executionId: currentWorkflowExecutions[0].id }, name: VIEWS.EXECUTION_PREVIEW,
}).catch(()=>{});; params: { name: currentWorkflow, executionId: currentWorkflowExecutions[0].id },
})
.catch(() => {});
} }
} }
} catch (error) { } catch (error) {
@@ -468,10 +564,10 @@ export default mixins(
this.refreshData(); this.refreshData();
}, },
handleFilterChanged () { handleFilterChanged() {
this.refreshData(); this.refreshData();
}, },
handleRetryClick (commandData: { command: string, row: IExecutionShortResponse }) { handleRetryClick(commandData: { command: string; row: IExecutionShortResponse }) {
let loadWorkflow = false; let loadWorkflow = false;
if (commandData.command === 'currentlySaved') { if (commandData.command === 'currentlySaved') {
loadWorkflow = true; loadWorkflow = true;
@@ -485,7 +581,7 @@ export default mixins(
retry_type: loadWorkflow ? 'current' : 'original', retry_type: loadWorkflow ? 'current' : 'original',
}); });
}, },
getRowClass (data: IDataObject): string { getRowClass(data: IDataObject): string {
const classes: string[] = []; const classes: string[] = [];
if ((data.row as IExecutionsSummary).stoppedAt === undefined) { if ((data.row as IExecutionsSummary).stoppedAt === undefined) {
classes.push('currently-running'); classes.push('currently-running');
@@ -493,7 +589,7 @@ export default mixins(
return classes.join(' '); return classes.join(' ');
}, },
getWorkflowName (workflowId: string): string | undefined { getWorkflowName(workflowId: string): string | undefined {
const workflow = this.workflows.find((data) => data.id === workflowId); const workflow = this.workflows.find((data) => data.id === workflowId);
if (workflow === undefined) { if (workflow === undefined) {
return undefined; return undefined;
@@ -501,30 +597,40 @@ export default mixins(
return workflow.name; return workflow.name;
}, },
async loadActiveExecutions (): Promise<void> { async loadActiveExecutions(): Promise<void> {
const activeExecutions = await this.restApi().getCurrentExecutions(this.workflowFilterCurrent); const activeExecutions = await this.restApi().getCurrentExecutions(
this.workflowFilterCurrent,
);
for (const activeExecution of activeExecutions) { for (const activeExecution of activeExecutions) {
if (activeExecution.workflowId !== undefined && activeExecution.workflowName === undefined) { if (
activeExecution.workflowId !== undefined &&
activeExecution.workflowName === undefined
) {
activeExecution.workflowName = this.getWorkflowName(activeExecution.workflowId); activeExecution.workflowName = this.getWorkflowName(activeExecution.workflowId);
} }
} }
this.workflowsStore.activeExecutions = activeExecutions; this.workflowsStore.activeExecutions = activeExecutions;
}, },
async loadAutoRefresh () : Promise<void> { async loadAutoRefresh(): Promise<void> {
const filter = this.workflowFilterPast; const filter = this.workflowFilterPast;
// We cannot use firstId here as some executions finish out of order. Let's say // We cannot use firstId here as some executions finish out of order. Let's say
// You have execution ids 500 to 505 running. // You have execution ids 500 to 505 running.
// Suppose 504 finishes before 500, 501, 502 and 503. // Suppose 504 finishes before 500, 501, 502 and 503.
// iF you use firstId, filtering id >= 504 you won't // iF you use firstId, filtering id >= 504 you won't
// ever get ids 500, 501, 502 and 503 when they finish // ever get ids 500, 501, 502 and 503 when they finish
const pastExecutionsPromise: Promise<IExecutionsListResponse> = this.restApi().getPastExecutions(filter, 30); const pastExecutionsPromise: Promise<IExecutionsListResponse> =
const currentExecutionsPromise: Promise<IExecutionsCurrentSummaryExtended[]> = this.restApi().getCurrentExecutions({}); this.restApi().getPastExecutions(filter, 30);
const currentExecutionsPromise: Promise<IExecutionsCurrentSummaryExtended[]> =
this.restApi().getCurrentExecutions({});
const results = await Promise.all([pastExecutionsPromise, currentExecutionsPromise]); const results = await Promise.all([pastExecutionsPromise, currentExecutionsPromise]);
for (const activeExecution of results[1]) { for (const activeExecution of results[1]) {
if (activeExecution.workflowId !== undefined && activeExecution.workflowName === undefined) { if (
activeExecution.workflowId !== undefined &&
activeExecution.workflowName === undefined
) {
activeExecution.workflowName = this.getWorkflowName(activeExecution.workflowId); activeExecution.workflowName = this.getWorkflowName(activeExecution.workflowId);
} }
} }
@@ -532,10 +638,12 @@ export default mixins(
this.workflowsStore.activeExecutions = results[1]; this.workflowsStore.activeExecutions = results[1];
// execution IDs are typed as string, int conversion is necessary so we can order. // execution IDs are typed as string, int conversion is necessary so we can order.
const alreadyPresentExecutionIds = this.finishedExecutions.map(exec => parseInt(exec.id, 10)); const alreadyPresentExecutionIds = this.finishedExecutions.map((exec) =>
parseInt(exec.id, 10),
);
let lastId = 0; let lastId = 0;
const gaps = [] as number[]; const gaps = [] as number[];
for(let i = results[0].results.length - 1; i >= 0; i--) { for (let i = results[0].results.length - 1; i >= 0; i--) {
const currentItem = results[0].results[i]; const currentItem = results[0].results[i];
const currentId = parseInt(currentItem.id, 10); const currentId = parseInt(currentItem.id, 10);
if (lastId !== 0 && isNaN(currentId) === false) { if (lastId !== 0 && isNaN(currentId) === false) {
@@ -557,7 +665,10 @@ export default mixins(
if (executionIndex !== -1) { if (executionIndex !== -1) {
// Execution that we received is already present. // Execution that we received is already present.
if (this.finishedExecutions[executionIndex].finished === false && currentItem.finished === true) { if (
this.finishedExecutions[executionIndex].finished === false &&
currentItem.finished === true
) {
// Concurrency stuff. This might happen if the execution finishes // Concurrency stuff. This might happen if the execution finishes
// prior to saving all information to database. Somewhat rare but // prior to saving all information to database. Somewhat rare but
// With auto refresh and several executions, it happens sometimes. // With auto refresh and several executions, it happens sometimes.
@@ -580,23 +691,29 @@ export default mixins(
this.finishedExecutions.unshift(currentItem); this.finishedExecutions.unshift(currentItem);
} }
} }
this.finishedExecutions = this.finishedExecutions.filter(execution => !gaps.includes(parseInt(execution.id, 10)) && lastId >= parseInt(execution.id, 10)); this.finishedExecutions = this.finishedExecutions.filter(
(execution) =>
!gaps.includes(parseInt(execution.id, 10)) && lastId >= parseInt(execution.id, 10),
);
this.finishedExecutionsCount = results[0].count; this.finishedExecutionsCount = results[0].count;
this.finishedExecutionsCountEstimated = results[0].estimated; this.finishedExecutionsCountEstimated = results[0].estimated;
}, },
async loadFinishedExecutions (): Promise<void> { async loadFinishedExecutions(): Promise<void> {
if (this.filter.status === 'running') { if (this.filter.status === 'running') {
this.finishedExecutions = []; this.finishedExecutions = [];
this.finishedExecutionsCount = 0; this.finishedExecutionsCount = 0;
this.finishedExecutionsCountEstimated = false; this.finishedExecutionsCountEstimated = false;
return; return;
} }
const data = await this.restApi().getPastExecutions(this.workflowFilterPast, this.requestItemsPerRequest); const data = await this.restApi().getPastExecutions(
this.workflowFilterPast,
this.requestItemsPerRequest,
);
this.finishedExecutions = data.results; this.finishedExecutions = data.results;
this.finishedExecutionsCount = data.count; this.finishedExecutionsCount = data.count;
this.finishedExecutionsCountEstimated = data.estimated; this.finishedExecutionsCountEstimated = data.estimated;
}, },
async loadMore () { async loadMore() {
if (this.filter.status === 'running') { if (this.filter.status === 'running') {
return; return;
} }
@@ -616,10 +733,7 @@ export default mixins(
data = await this.restApi().getPastExecutions(filter, this.requestItemsPerRequest, lastId); data = await this.restApi().getPastExecutions(filter, this.requestItemsPerRequest, lastId);
} catch (error) { } catch (error) {
this.isDataLoading = false; this.isDataLoading = false;
this.$showError( this.$showError(error, this.$locale.baseText('executionsList.showError.loadMore.title'));
error,
this.$locale.baseText('executionsList.showError.loadMore.title'),
);
return; return;
} }
@@ -634,7 +748,7 @@ export default mixins(
this.isDataLoading = false; this.isDataLoading = false;
}, },
async loadWorkflows () { async loadWorkflows() {
try { try {
const workflows = await this.restApi().getWorkflows(); const workflows = await this.restApi().getWorkflows();
workflows.sort((a, b) => { workflows.sort((a, b) => {
@@ -661,7 +775,7 @@ export default mixins(
); );
} }
}, },
async retryExecution (execution: IExecutionShortResponse, loadWorkflow?: boolean) { async retryExecution(execution: IExecutionShortResponse, loadWorkflow?: boolean) {
this.isDataLoading = true; this.isDataLoading = true;
try { try {
@@ -689,7 +803,7 @@ export default mixins(
this.isDataLoading = false; this.isDataLoading = false;
} }
}, },
async refreshData () { async refreshData() {
this.isDataLoading = true; this.isDataLoading = true;
try { try {
@@ -697,56 +811,58 @@ export default mixins(
const finishedExecutionsPromise = this.loadFinishedExecutions(); const finishedExecutionsPromise = this.loadFinishedExecutions();
await Promise.all([activeExecutionsPromise, finishedExecutionsPromise]); await Promise.all([activeExecutionsPromise, finishedExecutionsPromise]);
} catch (error) { } catch (error) {
this.$showError( this.$showError(error, this.$locale.baseText('executionsList.showError.refreshData.title'));
error,
this.$locale.baseText('executionsList.showError.refreshData.title'),
);
} }
this.isDataLoading = false; this.isDataLoading = false;
}, },
statusTooltipText (entry: IExecutionsSummary): string { statusTooltipText(entry: IExecutionsSummary): string {
if (entry.waitTill) { if (entry.waitTill) {
const waitDate = new Date(entry.waitTill); const waitDate = new Date(entry.waitTill);
if (waitDate.toISOString() === WAIT_TIME_UNLIMITED) { if (waitDate.toISOString() === WAIT_TIME_UNLIMITED) {
return this.$locale.baseText('executionsList.statusTooltipText.theWorkflowIsWaitingIndefinitely'); return this.$locale.baseText(
'executionsList.statusTooltipText.theWorkflowIsWaitingIndefinitely',
);
} }
return this.$locale.baseText( return this.$locale.baseText('executionsList.statusTooltipText.theWorkflowIsWaitingTill', {
'executionsList.statusTooltipText.theWorkflowIsWaitingTill', interpolate: {
{ waitDateDate: waitDate.toLocaleDateString(),
interpolate: { waitDateTime: waitDate.toLocaleTimeString(),
waitDateDate: waitDate.toLocaleDateString(),
waitDateTime: waitDate.toLocaleTimeString(),
},
}, },
); });
} else if (entry.stoppedAt === undefined) { } else if (entry.stoppedAt === undefined) {
return this.$locale.baseText('executionsList.statusTooltipText.theWorkflowIsCurrentlyExecuting'); return this.$locale.baseText(
'executionsList.statusTooltipText.theWorkflowIsCurrentlyExecuting',
);
} else if (entry.finished === true && entry.retryOf !== undefined) { } else if (entry.finished === true && entry.retryOf !== undefined) {
return this.$locale.baseText( return this.$locale.baseText(
'executionsList.statusTooltipText.theWorkflowExecutionWasARetryOfAndItWasSuccessful', 'executionsList.statusTooltipText.theWorkflowExecutionWasARetryOfAndItWasSuccessful',
{ interpolate: { entryRetryOf: entry.retryOf }}, { interpolate: { entryRetryOf: entry.retryOf } },
); );
} else if (entry.finished === true) { } else if (entry.finished === true) {
return this.$locale.baseText('executionsList.statusTooltipText.theWorkflowExecutionWasSuccessful'); return this.$locale.baseText(
'executionsList.statusTooltipText.theWorkflowExecutionWasSuccessful',
);
} else if (entry.retryOf !== undefined) { } else if (entry.retryOf !== undefined) {
return this.$locale.baseText( return this.$locale.baseText(
'executionsList.statusTooltipText.theWorkflowExecutionWasARetryOfAndFailed', 'executionsList.statusTooltipText.theWorkflowExecutionWasARetryOfAndFailed',
{ interpolate: { entryRetryOf: entry.retryOf }}, { interpolate: { entryRetryOf: entry.retryOf } },
); );
} else if (entry.retrySuccessId !== undefined) { } else if (entry.retrySuccessId !== undefined) {
return this.$locale.baseText( return this.$locale.baseText(
'executionsList.statusTooltipText.theWorkflowExecutionFailedButTheRetryWasSuccessful', 'executionsList.statusTooltipText.theWorkflowExecutionFailedButTheRetryWasSuccessful',
{ interpolate: { entryRetrySuccessId: entry.retrySuccessId }}, { interpolate: { entryRetrySuccessId: entry.retrySuccessId } },
); );
} else if (entry.stoppedAt === null) { } else if (entry.stoppedAt === null) {
return this.$locale.baseText('executionsList.statusTooltipText.theWorkflowExecutionIsProbablyStillRunning'); return this.$locale.baseText(
'executionsList.statusTooltipText.theWorkflowExecutionIsProbablyStillRunning',
);
} else { } else {
return this.$locale.baseText('executionsList.statusTooltipText.theWorkflowExecutionFailed'); return this.$locale.baseText('executionsList.statusTooltipText.theWorkflowExecutionFailed');
} }
}, },
async stopExecution (activeExecutionId: string) { async stopExecution(activeExecutionId: string) {
try { try {
// Add it to the list of currently stopping executions that we // Add it to the list of currently stopping executions that we
// can show the user in the UI that it is in progress // can show the user in the UI that it is in progress
@@ -760,10 +876,9 @@ export default mixins(
this.$showMessage({ this.$showMessage({
title: this.$locale.baseText('executionsList.showMessage.stopExecution.title'), title: this.$locale.baseText('executionsList.showMessage.stopExecution.title'),
message: this.$locale.baseText( message: this.$locale.baseText('executionsList.showMessage.stopExecution.message', {
'executionsList.showMessage.stopExecution.message', interpolate: { activeExecutionId },
{ interpolate: { activeExecutionId } }, }),
),
type: 'success', type: 'success',
}); });
@@ -780,7 +895,6 @@ export default mixins(
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
.autorefresh { .autorefresh {
padding-right: 0.5em; padding-right: 0.5em;
text-align: right; text-align: right;
@@ -829,7 +943,8 @@ export default mixins(
color: var(--color-success); color: var(--color-success);
} }
&.running, &.warning { &.running,
&.warning {
background-color: var(--color-warning-tint-2); background-color: var(--color-warning-tint-2);
color: var(--color-warning); color: var(--color-warning);
} }
@@ -842,11 +957,9 @@ export default mixins(
.actions-container > * { .actions-container > * {
margin-left: 5px; margin-left: 5px;
} }
</style> </style>
<style lang="scss"> <style lang="scss">
.currently-running { .currently-running {
background-color: var(--color-primary-tint-3) !important; background-color: var(--color-primary-tint-3) !important;
} }
@@ -854,5 +967,4 @@ export default mixins(
.el-table tr:hover.currently-running td { .el-table tr:hover.currently-running td {
background-color: var(--color-primary-tint-2) !important; background-color: var(--color-primary-tint-2) !important;
} }
</style> </style>

View File

@@ -10,23 +10,48 @@
> >
<router-link <router-link
:class="$style.executionLink" :class="$style.executionLink"
:to="{ name: VIEWS.EXECUTION_PREVIEW, params: { workflowId: currentWorkflow, executionId: execution.id }}" :to="{
name: VIEWS.EXECUTION_PREVIEW,
params: { workflowId: currentWorkflow, executionId: execution.id },
}"
> >
<div :class="$style.description"> <div :class="$style.description">
<n8n-text color="text-dark" :bold="true" size="medium">{{ executionUIDetails.startTime }}</n8n-text> <n8n-text color="text-dark" :bold="true" size="medium">{{
executionUIDetails.startTime
}}</n8n-text>
<div :class="$style.executionStatus"> <div :class="$style.executionStatus">
<n8n-spinner v-if="executionUIDetails.name === 'running'" size="small" :class="[$style.spinner, 'mr-4xs']"/> <n8n-spinner
<n8n-text :class="$style.statusLabel" size="small">{{ executionUIDetails.label }}</n8n-text> v-if="executionUIDetails.name === 'running'"
<n8n-text v-if="executionUIDetails.name === 'running'" :color="isActive? 'text-dark' : 'text-base'" size="small"> size="small"
:class="[$style.spinner, 'mr-4xs']"
/>
<n8n-text :class="$style.statusLabel" size="small">{{
executionUIDetails.label
}}</n8n-text>
<n8n-text
v-if="executionUIDetails.name === 'running'"
:color="isActive ? 'text-dark' : 'text-base'"
size="small"
>
{{ $locale.baseText('executionDetails.runningTimeRunning') }} {{ $locale.baseText('executionDetails.runningTimeRunning') }}
<execution-time :start-time="execution.startedAt"/> <execution-time :start-time="execution.startedAt" />
</n8n-text> </n8n-text>
<n8n-text v-else-if="executionUIDetails.name !== 'waiting' && executionUIDetails.name !== 'unknown'" :color="isActive? 'text-dark' : 'text-base'" size="small"> <n8n-text
{{ $locale.baseText('executionDetails.runningTimeFinished', { interpolate: { time: executionUIDetails.runningTime } }) }} v-else-if="
executionUIDetails.name !== 'waiting' && executionUIDetails.name !== 'unknown'
"
:color="isActive ? 'text-dark' : 'text-base'"
size="small"
>
{{
$locale.baseText('executionDetails.runningTimeFinished', {
interpolate: { time: executionUIDetails.runningTime },
})
}}
</n8n-text> </n8n-text>
</div> </div>
<div v-if="execution.mode === 'retry'"> <div v-if="execution.mode === 'retry'">
<n8n-text :color="isActive? 'text-dark' : 'text-base'" size="small"> <n8n-text :color="isActive ? 'text-dark' : 'text-base'" size="small">
{{ $locale.baseText('executionDetails.retry') }} #{{ execution.retryOf }} {{ $locale.baseText('executionDetails.retry') }} #{{ execution.retryOf }}
</n8n-text> </n8n-text>
</div> </div>
@@ -44,7 +69,7 @@
:class="[$style.icon, $style.manual]" :class="[$style.icon, $style.manual]"
:title="$locale.baseText('executionsList.manual')" :title="$locale.baseText('executionsList.manual')"
icon="flask" icon="flask"
/> />
</div> </div>
</router-link> </router-link>
</div> </div>
@@ -59,11 +84,7 @@ import { showMessage } from '@/mixins/showMessage';
import { restApi } from '@/mixins/restApi'; import { restApi } from '@/mixins/restApi';
import ExecutionTime from '@/components/ExecutionTime.vue'; import ExecutionTime from '@/components/ExecutionTime.vue';
export default mixins( export default mixins(executionHelpers, showMessage, restApi).extend({
executionHelpers,
showMessage,
restApi,
).extend({
name: 'execution-card', name: 'execution-card',
components: { components: {
ExecutionTime, ExecutionTime,
@@ -86,8 +107,14 @@ export default mixins(
computed: { computed: {
retryExecutionActions(): object[] { retryExecutionActions(): object[] {
return [ return [
{ id: 'current-workflow', label: this.$locale.baseText('executionsList.retryWithCurrentlySavedWorkflow') }, {
{ id: 'original-workflow', label: this.$locale.baseText('executionsList.retryWithOriginalWorkflow') }, id: 'current-workflow',
label: this.$locale.baseText('executionsList.retryWithCurrentlySavedWorkflow'),
},
{
id: 'original-workflow',
label: this.$locale.baseText('executionsList.retryWithOriginalWorkflow'),
},
]; ];
}, },
executionUIDetails(): IExecutionUIData { executionUIDetails(): IExecutionUIData {
@@ -119,9 +146,12 @@ export default mixins(
} }
} }
& + &.active { padding-top: var(--spacing-2xs); } & + &.active {
padding-top: var(--spacing-2xs);
}
&:hover, &.active { &:hover,
&.active {
.executionLink { .executionLink {
background-color: var(--color-foreground-base); background-color: var(--color-foreground-base);
} }
@@ -132,34 +162,47 @@ export default mixins(
position: relative; position: relative;
top: 1px; top: 1px;
} }
&, & .executionLink { &,
& .executionLink {
border-left: var(--spacing-4xs) var(--border-style-base) hsl(var(--color-warning-h), 94%, 80%); border-left: var(--spacing-4xs) var(--border-style-base) hsl(var(--color-warning-h), 94%, 80%);
} }
.statusLabel, .spinner { color: var(--color-warning); } .statusLabel,
.spinner {
color: var(--color-warning);
}
} }
&.success { &.success {
&, & .executionLink { &,
& .executionLink {
border-left: var(--spacing-4xs) var(--border-style-base) hsl(var(--color-success-h), 60%, 70%); border-left: var(--spacing-4xs) var(--border-style-base) hsl(var(--color-success-h), 60%, 70%);
} }
} }
&.waiting { &.waiting {
&, & .executionLink { &,
border-left: var(--spacing-4xs) var(--border-style-base) hsl(var(--color-secondary-h), 94%, 80%); & .executionLink {
border-left: var(--spacing-4xs) var(--border-style-base)
hsl(var(--color-secondary-h), 94%, 80%);
}
.statusLabel {
color: var(--color-secondary);
} }
.statusLabel { color: var(--color-secondary); }
} }
&.error { &.error {
&, & .executionLink { &,
& .executionLink {
border-left: var(--spacing-4xs) var(--border-style-base) hsl(var(--color-danger-h), 94%, 80%); border-left: var(--spacing-4xs) var(--border-style-base) hsl(var(--color-danger-h), 94%, 80%);
} }
.statusLabel { color: var(--color-danger ); } .statusLabel {
color: var(--color-danger);
}
} }
&.unknown { &.unknown {
&, & .executionLink { &,
& .executionLink {
border-left: var(--spacing-4xs) var(--border-style-base) var(--color-text-light); border-left: var(--spacing-4xs) var(--border-style-base) var(--color-text-light);
} }
} }
@@ -176,11 +219,14 @@ export default mixins(
padding-right: var(--spacing-s); padding-right: var(--spacing-s);
border-radius: var(--border-radius-base); border-radius: var(--border-radius-base);
position: relative; position: relative;
left: calc(-1 * var(--spacing-4xs)); // Hide link border under card border so it's not visible when not hovered left: calc(
-1 * var(--spacing-4xs)
); // Hide link border under card border so it's not visible when not hovered
&:active { &:active {
.icon, .statusLabel { .icon,
color: var(--color-text-base);; .statusLabel {
color: var(--color-text-base);
} }
} }
} }

View File

@@ -1,5 +1,8 @@
<template> <template>
<div v-if="executionUIDetails && executionUIDetails.name === 'running'" :class="$style.runningInfo"> <div
v-if="executionUIDetails && executionUIDetails.name === 'running'"
:class="$style.runningInfo"
>
<div :class="$style.spinner"> <div :class="$style.spinner">
<n8n-spinner type="ring" /> <n8n-spinner type="ring" />
</div> </div>
@@ -11,32 +14,66 @@
</n8n-button> </n8n-button>
</div> </div>
<div v-else :class="$style.previewContainer"> <div v-else :class="$style.previewContainer">
<div :class="{[$style.executionDetails]: true, [$style.sidebarCollapsed]: sidebarCollapsed }" v-if="activeExecution"> <div
:class="{ [$style.executionDetails]: true, [$style.sidebarCollapsed]: sidebarCollapsed }"
v-if="activeExecution"
>
<div> <div>
<n8n-text size="large" color="text-base" :bold="true">{{ executionUIDetails.startTime }}</n8n-text><br> <n8n-text size="large" color="text-base" :bold="true">{{
<n8n-spinner v-if="executionUIDetails.name === 'running'" size="small" :class="[$style.spinner, 'mr-4xs']"/> executionUIDetails.startTime
<n8n-text size="medium" :class="[$style.status, $style[executionUIDetails.name]]">{{ executionUIDetails.label }}</n8n-text> }}</n8n-text
><br />
<n8n-spinner
v-if="executionUIDetails.name === 'running'"
size="small"
:class="[$style.spinner, 'mr-4xs']"
/>
<n8n-text size="medium" :class="[$style.status, $style[executionUIDetails.name]]">{{
executionUIDetails.label
}}</n8n-text>
<n8n-text v-if="executionUIDetails.name === 'running'" color="text-base" size="medium"> <n8n-text v-if="executionUIDetails.name === 'running'" color="text-base" size="medium">
{{ $locale.baseText('executionDetails.runningTimeRunning', { interpolate: { time: executionUIDetails.runningTime } }) }} | ID#{{ activeExecution.id }} {{
$locale.baseText('executionDetails.runningTimeRunning', {
interpolate: { time: executionUIDetails.runningTime },
})
}}
| ID#{{ activeExecution.id }}
</n8n-text> </n8n-text>
<n8n-text v-else-if="executionUIDetails.name !== 'waiting'" color="text-base" size="medium"> <n8n-text v-else-if="executionUIDetails.name !== 'waiting'" color="text-base" size="medium">
{{ $locale.baseText('executionDetails.runningTimeFinished', { interpolate: { time: executionUIDetails.runningTime } }) }} | ID#{{ activeExecution.id }} {{
$locale.baseText('executionDetails.runningTimeFinished', {
interpolate: { time: executionUIDetails.runningTime },
})
}}
| ID#{{ activeExecution.id }}
</n8n-text> </n8n-text>
<n8n-text v-else-if="executionUIDetails.name === 'waiting'" color="text-base" size="medium"> <n8n-text v-else-if="executionUIDetails.name === 'waiting'" color="text-base" size="medium">
| ID#{{ activeExecution.id }} | ID#{{ activeExecution.id }}
</n8n-text> </n8n-text>
<br><n8n-text v-if="activeExecution.mode === 'retry'" color="text-base" size= "medium"> <br /><n8n-text v-if="activeExecution.mode === 'retry'" color="text-base" size="medium">
{{ $locale.baseText('executionDetails.retry') }} {{ $locale.baseText('executionDetails.retry') }}
<router-link <router-link
:class="$style.executionLink" :class="$style.executionLink"
:to="{ name: VIEWS.EXECUTION_PREVIEW, params: { workflowId: activeExecution.workflowId, executionId: activeExecution.retryOf }}" :to="{
name: VIEWS.EXECUTION_PREVIEW,
params: {
workflowId: activeExecution.workflowId,
executionId: activeExecution.retryOf,
},
}"
> >
#{{ activeExecution.retryOf }} #{{ activeExecution.retryOf }}
</router-link> </router-link>
</n8n-text> </n8n-text>
</div> </div>
<div> <div>
<el-dropdown v-if="executionUIDetails.name === 'error'" trigger="click" class="mr-xs" @command="handleRetryClick" ref="retryDropdown"> <el-dropdown
v-if="executionUIDetails.name === 'error'"
trigger="click"
class="mr-xs"
@command="handleRetryClick"
ref="retryDropdown"
>
<span class="retry-button"> <span class="retry-button">
<n8n-icon-button <n8n-icon-button
size="large" size="large"
@@ -57,10 +94,21 @@
</el-dropdown-menu> </el-dropdown-menu>
</template> </template>
</el-dropdown> </el-dropdown>
<n8n-icon-button :title="$locale.baseText('executionDetails.deleteExecution')" icon="trash" size="large" type="tertiary" @click="onDeleteExecution" /> <n8n-icon-button
:title="$locale.baseText('executionDetails.deleteExecution')"
icon="trash"
size="large"
type="tertiary"
@click="onDeleteExecution"
/>
</div> </div>
</div> </div>
<workflow-preview mode="execution" loaderType="spinner" :executionId="executionId" :executionMode="executionMode"/> <workflow-preview
mode="execution"
loaderType="spinner"
:executionId="executionId"
:executionMode="executionMode"
/>
</div> </div>
</template> </template>
@@ -87,9 +135,7 @@ export default mixins(restApi, showMessage, executionHelpers).extend({
}; };
}, },
computed: { computed: {
...mapStores( ...mapStores(useUIStore),
useUIStore,
),
executionUIDetails(): IExecutionUIData | null { executionUIDetails(): IExecutionUIData | null {
return this.activeExecution ? this.getExecutionUIDetails(this.activeExecution) : null; return this.activeExecution ? this.getExecutionUIDetails(this.activeExecution) : null;
}, },
@@ -122,7 +168,7 @@ export default mixins(restApi, showMessage, executionHelpers).extend({
}, },
onRetryButtonBlur(event: FocusEvent): void { onRetryButtonBlur(event: FocusEvent): void {
// Hide dropdown when clicking outside of current document // Hide dropdown when clicking outside of current document
const retryDropdown = this.$refs.retryDropdown as Vue & { hide: () => void } | undefined; const retryDropdown = this.$refs.retryDropdown as (Vue & { hide: () => void }) | undefined;
if (retryDropdown && event.relatedTarget === null) { if (retryDropdown && event.relatedTarget === null) {
retryDropdown.hide(); retryDropdown.hide();
} }
@@ -132,7 +178,6 @@ export default mixins(restApi, showMessage, executionHelpers).extend({
</script> </script>
<style module lang="scss"> <style module lang="scss">
.previewContainer { .previewContainer {
height: calc(100% - $header-height); height: calc(100% - $header-height);
overflow: hidden; overflow: hidden;
@@ -148,7 +193,9 @@ export default mixins(restApi, showMessage, executionHelpers).extend({
transition: all 150ms ease-in-out; transition: all 150ms ease-in-out;
pointer-events: none; pointer-events: none;
& * { pointer-events: all; } & * {
pointer-events: all;
}
&.sidebarCollapsed { &.sidebarCollapsed {
width: calc(100% - 375px); width: calc(100% - 375px);
@@ -163,10 +210,19 @@ export default mixins(restApi, showMessage, executionHelpers).extend({
} }
} }
.running, .spinner { color: var(--color-warning); } .running,
.waiting { color: var(--color-secondary); } .spinner {
.success { color: var(--color-success); } color: var(--color-warning);
.error { color: var(--color-danger); } }
.waiting {
color: var(--color-secondary);
}
.success {
color: var(--color-success);
}
.error {
color: var(--color-danger);
}
.runningInfo { .runningInfo {
display: flex; display: flex;

View File

@@ -14,11 +14,19 @@
<n8n-tooltip :disabled="!isNewWorkflow"> <n8n-tooltip :disabled="!isNewWorkflow">
<template #content> <template #content>
<div> <div>
<n8n-link @click.prevent="onSaveWorkflowClick">{{ $locale.baseText('executionsLandingPage.emptyState.accordion.footer.tooltipLink') }}</n8n-link> <n8n-link @click.prevent="onSaveWorkflowClick">{{
{{ $locale.baseText('executionsLandingPage.emptyState.accordion.footer.tooltipText') }} $locale.baseText('executionsLandingPage.emptyState.accordion.footer.tooltipLink')
}}</n8n-link>
{{
$locale.baseText('executionsLandingPage.emptyState.accordion.footer.tooltipText')
}}
</div> </div>
</template> </template>
<n8n-link @click.prevent="openWorkflowSettings" :class="{[$style.disabled]: isNewWorkflow}" size="small"> <n8n-link
@click.prevent="openWorkflowSettings"
:class="{ [$style.disabled]: isNewWorkflow }"
size="small"
>
{{ $locale.baseText('executionsLandingPage.emptyState.accordion.footer.settingsLink') }} {{ $locale.baseText('executionsLandingPage.emptyState.accordion.footer.settingsLink') }}
</n8n-link> </n8n-link>
</n8n-tooltip> </n8n-tooltip>
@@ -39,10 +47,10 @@ import mixins from 'vue-typed-mixins';
import { workflowHelpers } from '@/mixins/workflowHelpers'; import { workflowHelpers } from '@/mixins/workflowHelpers';
interface IWorkflowSaveSettings { interface IWorkflowSaveSettings {
saveFailedExecutions: boolean, saveFailedExecutions: boolean;
saveSuccessfulExecutions: boolean, saveSuccessfulExecutions: boolean;
saveManualExecutions: boolean, saveManualExecutions: boolean;
}; }
export default mixins(workflowHelpers).extend({ export default mixins(workflowHelpers).extend({
name: 'executions-info-accordion', name: 'executions-info-accordion',
@@ -78,24 +86,28 @@ export default mixins(workflowHelpers).extend({
}, },
}, },
computed: { computed: {
...mapStores( ...mapStores(useRootStore, useSettingsStore, useUIStore, useWorkflowsStore),
useRootStore,
useSettingsStore,
useUIStore,
useWorkflowsStore,
),
accordionItems(): Object[] { accordionItems(): Object[] {
return [ return [
{ {
id: 'productionExecutions', id: 'productionExecutions',
label: this.$locale.baseText('executionsLandingPage.emptyState.accordion.productionExecutions'), label: this.$locale.baseText(
'executionsLandingPage.emptyState.accordion.productionExecutions',
),
icon: this.productionExecutionsIcon.icon, icon: this.productionExecutionsIcon.icon,
iconColor: this.productionExecutionsIcon.color, iconColor: this.productionExecutionsIcon.color,
tooltip: this.productionExecutionsStatus === 'unknown' ? this.$locale.baseText('executionsLandingPage.emptyState.accordion.productionExecutionsWarningTooltip') : null, tooltip:
this.productionExecutionsStatus === 'unknown'
? this.$locale.baseText(
'executionsLandingPage.emptyState.accordion.productionExecutionsWarningTooltip',
)
: null,
}, },
{ {
id: 'manualExecutions', id: 'manualExecutions',
label: this.$locale.baseText('executionsLandingPage.emptyState.accordion.manualExecutions'), label: this.$locale.baseText(
'executionsLandingPage.emptyState.accordion.manualExecutions',
),
icon: this.workflowSaveSettings.saveManualExecutions ? 'check' : 'times', icon: this.workflowSaveSettings.saveManualExecutions ? 'check' : 'times',
iconColor: this.workflowSaveSettings.saveManualExecutions ? 'success' : 'danger', iconColor: this.workflowSaveSettings.saveManualExecutions ? 'success' : 'danger',
}, },
@@ -105,11 +117,13 @@ export default mixins(workflowHelpers).extend({
if (this.initiallyExpanded === false) { if (this.initiallyExpanded === false) {
return false; return false;
} }
return this.workflowSaveSettings.saveFailedExecutions === false || return (
this.workflowSaveSettings.saveFailedExecutions === false ||
this.workflowSaveSettings.saveSuccessfulExecutions === false || this.workflowSaveSettings.saveSuccessfulExecutions === false ||
this.workflowSaveSettings.saveManualExecutions === false; this.workflowSaveSettings.saveManualExecutions === false
);
}, },
productionExecutionsIcon(): { icon: string, color: string } { productionExecutionsIcon(): { icon: string; color: string } {
if (this.productionExecutionsStatus === 'saving') { if (this.productionExecutionsStatus === 'saving') {
return { icon: 'check', color: 'success' }; return { icon: 'check', color: 'success' };
} else if (this.productionExecutionsStatus === 'not-saving') { } else if (this.productionExecutionsStatus === 'not-saving') {
@@ -118,7 +132,10 @@ export default mixins(workflowHelpers).extend({
return { icon: 'exclamation-triangle', color: 'warning' }; return { icon: 'exclamation-triangle', color: 'warning' };
}, },
productionExecutionsStatus(): string { productionExecutionsStatus(): string {
if (this.workflowSaveSettings.saveSuccessfulExecutions === this.workflowSaveSettings.saveFailedExecutions) { if (
this.workflowSaveSettings.saveSuccessfulExecutions ===
this.workflowSaveSettings.saveFailedExecutions
) {
if (this.workflowSaveSettings.saveSuccessfulExecutions === true) { if (this.workflowSaveSettings.saveSuccessfulExecutions === true) {
return 'saving'; return 'saving';
} }
@@ -131,8 +148,11 @@ export default mixins(workflowHelpers).extend({
const workflowSettings = deepCopy(this.workflowsStore.workflowSettings); const workflowSettings = deepCopy(this.workflowsStore.workflowSettings);
return workflowSettings; return workflowSettings;
}, },
accordionIcon(): { icon: string, color: string }|null { accordionIcon(): { icon: string; color: string } | null {
if (this.workflowSaveSettings.saveManualExecutions !== true || this.productionExecutionsStatus !== 'saving') { if (
this.workflowSaveSettings.saveManualExecutions !== true ||
this.productionExecutionsStatus !== 'saving'
) {
return { icon: 'exclamation-triangle', color: 'warning' }; return { icon: 'exclamation-triangle', color: 'warning' };
} }
return null; return null;
@@ -141,7 +161,11 @@ export default mixins(workflowHelpers).extend({
return this.workflowsStore.workflowId; return this.workflowsStore.workflowId;
}, },
isNewWorkflow(): boolean { isNewWorkflow(): boolean {
return !this.currentWorkflowId || (this.currentWorkflowId === PLACEHOLDER_EMPTY_WORKFLOW_ID || this.currentWorkflowId === 'new'); return (
!this.currentWorkflowId ||
this.currentWorkflowId === PLACEHOLDER_EMPTY_WORKFLOW_ID ||
this.currentWorkflowId === 'new'
);
}, },
workflowName(): string { workflowName(): string {
return this.workflowsStore.workflowName; return this.workflowsStore.workflowName;
@@ -152,9 +176,18 @@ export default mixins(workflowHelpers).extend({
}, },
methods: { methods: {
updateSettings(workflowSettings: IWorkflowSettings): void { updateSettings(workflowSettings: IWorkflowSettings): void {
this.workflowSaveSettings.saveFailedExecutions = workflowSettings.saveDataErrorExecution === undefined ? this.defaultValues.saveFailedExecutions === 'all' : workflowSettings.saveDataErrorExecution === 'all'; this.workflowSaveSettings.saveFailedExecutions =
this.workflowSaveSettings.saveSuccessfulExecutions = workflowSettings.saveDataSuccessExecution === undefined ? this.defaultValues.saveSuccessfulExecutions === 'all' : workflowSettings.saveDataSuccessExecution === 'all'; workflowSettings.saveDataErrorExecution === undefined
this.workflowSaveSettings.saveManualExecutions = workflowSettings.saveManualExecutions === undefined ? this.defaultValues.saveManualExecutions : workflowSettings.saveManualExecutions as boolean; ? this.defaultValues.saveFailedExecutions === 'all'
: workflowSettings.saveDataErrorExecution === 'all';
this.workflowSaveSettings.saveSuccessfulExecutions =
workflowSettings.saveDataSuccessExecution === undefined
? this.defaultValues.saveSuccessfulExecutions === 'all'
: workflowSettings.saveDataSuccessExecution === 'all';
this.workflowSaveSettings.saveManualExecutions =
workflowSettings.saveManualExecutions === undefined
? this.defaultValues.saveManualExecutions
: (workflowSettings.saveManualExecutions as boolean);
}, },
onAccordionClick(event: MouseEvent): void { onAccordionClick(event: MouseEvent): void {
if (event.target instanceof HTMLAnchorElement) { if (event.target instanceof HTMLAnchorElement) {
@@ -178,7 +211,11 @@ export default mixins(workflowHelpers).extend({
} else if (this.$route.params.name && this.$route.params.name !== 'new') { } else if (this.$route.params.name && this.$route.params.name !== 'new') {
currentId = this.$route.params.name; currentId = this.$route.params.name;
} }
const saved = await this.saveCurrentWorkflow({ id: currentId, name: this.workflowName, tags: this.currentWorkflowTagIds }); const saved = await this.saveCurrentWorkflow({
id: currentId,
name: this.workflowName,
tags: this.currentWorkflowTagIds,
});
if (saved) this.settingsStore.fetchPromptsData(); if (saved) this.settingsStore.fetchPromptsData();
}, },
}, },
@@ -186,7 +223,6 @@ export default mixins(workflowHelpers).extend({
</script> </script>
<style module lang="scss"> <style module lang="scss">
.accordion { .accordion {
background: none; background: none;
width: 320px; width: 320px;
@@ -208,7 +244,9 @@ export default mixins(workflowHelpers).extend({
width: 100%; width: 100%;
padding: 0 var(--spacing-l) var(--spacing-s) !important; padding: 0 var(--spacing-l) var(--spacing-s) !important;
span { width: 100%; } span {
width: 100%;
}
} }
footer { footer {
@@ -224,5 +262,4 @@ export default mixins(workflowHelpers).extend({
text-decoration: none; text-decoration: none;
} }
} }
</style> </style>

View File

@@ -39,10 +39,7 @@ export default Vue.extend({
ExecutionsInfoAccordion, ExecutionsInfoAccordion,
}, },
computed: { computed: {
...mapStores( ...mapStores(useUIStore, useWorkflowsStore),
useUIStore,
useWorkflowsStore,
),
executionCount(): number { executionCount(): number {
return this.workflowsStore.currentWorkflowExecutions.length; return this.workflowsStore.currentWorkflowExecutions.length;
}, },
@@ -56,7 +53,7 @@ export default Vue.extend({
const workflowRoute = this.getWorkflowRoute(); const workflowRoute = this.getWorkflowRoute();
this.$router.push(workflowRoute); this.$router.push(workflowRoute);
}, },
getWorkflowRoute(): { name: string, params: {}} { getWorkflowRoute(): { name: string; params: {} } {
const workflowId = this.workflowsStore.workflowId || this.$route.params.name; const workflowId = this.workflowsStore.workflowId || this.$route.params.name;
if (workflowId === PLACEHOLDER_EMPTY_WORKFLOW_ID) { if (workflowId === PLACEHOLDER_EMPTY_WORKFLOW_ID) {
return { name: VIEWS.NEW_WORKFLOW, params: {} }; return { name: VIEWS.NEW_WORKFLOW, params: {} };
@@ -69,7 +66,6 @@ export default Vue.extend({
</script> </script>
<style module lang="scss"> <style module lang="scss">
.container { .container {
width: 100%; width: 100%;
height: 100%; height: 100%;

View File

@@ -1,13 +1,15 @@
<template> <template>
<div :class="['executions-sidebar', $style.container]"> <div :class="['executions-sidebar', $style.container]">
<div :class="$style.heading"> <div :class="$style.heading">
<n8n-heading tag="h2" size="medium" color="text-dark"> <n8n-heading tag="h2" size="medium" color="text-dark">
{{ $locale.baseText('generic.executions') }} {{ $locale.baseText('generic.executions') }}
</n8n-heading> </n8n-heading>
</div> </div>
<div :class="$style.controls"> <div :class="$style.controls">
<el-checkbox v-model="autoRefresh" @change="onAutoRefreshToggle">{{ $locale.baseText('executionsList.autoRefresh') }}</el-checkbox> <el-checkbox v-model="autoRefresh" @change="onAutoRefreshToggle">{{
<n8n-popover trigger="click" > $locale.baseText('executionsList.autoRefresh')
}}</el-checkbox>
<n8n-popover trigger="click">
<template #reference> <template #reference>
<div :class="$style.filterButton"> <div :class="$style.filterButton">
<n8n-button icon="filter" type="tertiary" size="medium" :active="statusFilterApplied"> <n8n-button icon="filter" type="tertiary" size="medium" :active="statusFilterApplied">
@@ -37,7 +39,8 @@
v-for="item in executionStatuses" v-for="item in executionStatuses"
:key="item.id" :key="item.id"
:label="item.name" :label="item.name"
:value="item.id"> :value="item.id"
>
</n8n-option> </n8n-option>
</n8n-select> </n8n-select>
</div> </div>
@@ -86,7 +89,7 @@ import ExecutionCard from '@/components/ExecutionsView/ExecutionCard.vue';
import ExecutionsInfoAccordion from '@/components/ExecutionsView/ExecutionsInfoAccordion.vue'; import ExecutionsInfoAccordion from '@/components/ExecutionsView/ExecutionsInfoAccordion.vue';
import { VIEWS } from '../../constants'; import { VIEWS } from '../../constants';
import { range as _range } from 'lodash'; import { range as _range } from 'lodash';
import { IExecutionsSummary } from "@/Interface"; import { IExecutionsSummary } from '@/Interface';
import { Route } from 'vue-router'; import { Route } from 'vue-router';
import Vue from 'vue'; import Vue from 'vue';
import { PropType } from 'vue'; import { PropType } from 'vue';
@@ -124,13 +127,11 @@ export default Vue.extend({
}; };
}, },
computed: { computed: {
...mapStores( ...mapStores(useUIStore),
useUIStore,
),
statusFilterApplied(): boolean { statusFilterApplied(): boolean {
return this.filter.status !== ''; return this.filter.status !== '';
}, },
executionStatuses(): Array<{ id: string, name: string }> { executionStatuses(): Array<{ id: string; name: string }> {
return [ return [
{ id: 'error', name: this.$locale.baseText('executionsList.error') }, { id: 'error', name: this.$locale.baseText('executionsList.error') },
{ id: 'running', name: this.$locale.baseText('executionsList.running') }, { id: 'running', name: this.$locale.baseText('executionsList.running') },
@@ -140,12 +141,12 @@ export default Vue.extend({
}, },
}, },
watch: { watch: {
$route (to: Route, from: Route) { $route(to: Route, from: Route) {
if (from.name === VIEWS.EXECUTION_PREVIEW && to.name === VIEWS.EXECUTION_HOME) { if (from.name === VIEWS.EXECUTION_PREVIEW && to.name === VIEWS.EXECUTION_HOME) {
// Skip parent route when navigating through executions with back button // Skip parent route when navigating through executions with back button
this.$router.go(-1); this.$router.go(-1);
} }
}, },
}, },
mounted() { mounted() {
this.autoRefresh = this.uiStore.executionSidebarAutoRefresh === true; this.autoRefresh = this.uiStore.executionSidebarAutoRefresh === true;
@@ -164,8 +165,9 @@ export default Vue.extend({
if (!this.loading) { if (!this.loading) {
const executionsList = this.$refs.executionList as HTMLElement; const executionsList = this.$refs.executionList as HTMLElement;
if (executionsList) { if (executionsList) {
const diff = executionsList.offsetHeight - (executionsList.scrollHeight - executionsList.scrollTop); const diff =
if (diff > -10 && diff < 10) { executionsList.offsetHeight - (executionsList.scrollHeight - executionsList.scrollTop);
if (diff > -10 && diff < 10) {
this.$emit('loadMore'); this.$emit('loadMore');
} }
} }
@@ -269,7 +271,7 @@ export default Vue.extend({
& > div { & > div {
width: 309px; width: 309px;
background-color: var(--color-background-light); background-color: var(--color-background-light);
margin-top: 0 !important; margin-top: 0 !important;
} }
} }
</style> </style>

View File

@@ -29,9 +29,29 @@
<script lang="ts"> <script lang="ts">
import ExecutionsSidebar from '@/components/ExecutionsView/ExecutionsSidebar.vue'; import ExecutionsSidebar from '@/components/ExecutionsView/ExecutionsSidebar.vue';
import { MODAL_CANCEL, MODAL_CLOSE, MODAL_CONFIRMED, PLACEHOLDER_EMPTY_WORKFLOW_ID, VIEWS, WEBHOOK_NODE_TYPE } from '@/constants'; import {
import { IExecutionsListResponse, IExecutionsSummary, INodeUi, ITag, IWorkflowDb } from '@/Interface'; MODAL_CANCEL,
import { IConnection, IConnections, IDataObject, INodeTypeDescription, INodeTypeNameVersion, NodeHelpers } from 'n8n-workflow'; MODAL_CLOSE,
MODAL_CONFIRMED,
PLACEHOLDER_EMPTY_WORKFLOW_ID,
VIEWS,
WEBHOOK_NODE_TYPE,
} from '@/constants';
import {
IExecutionsListResponse,
IExecutionsSummary,
INodeUi,
ITag,
IWorkflowDb,
} from '@/Interface';
import {
IConnection,
IConnections,
IDataObject,
INodeTypeDescription,
INodeTypeNameVersion,
NodeHelpers,
} from 'n8n-workflow';
import mixins from 'vue-typed-mixins'; import mixins from 'vue-typed-mixins';
import { restApi } from '@/mixins/restApi'; import { restApi } from '@/mixins/restApi';
import { showMessage } from '@/mixins/showMessage'; import { showMessage } from '@/mixins/showMessage';
@@ -49,7 +69,13 @@ import { useSettingsStore } from '@/stores/settings';
import { useNodeTypesStore } from '@/stores/nodeTypes'; import { useNodeTypesStore } from '@/stores/nodeTypes';
import { useTagsStore } from '@/stores/tags'; import { useTagsStore } from '@/stores/tags';
export default mixins(restApi, showMessage, executionHelpers, debounceHelper, workflowHelpers).extend({ export default mixins(
restApi,
showMessage,
executionHelpers,
debounceHelper,
workflowHelpers,
).extend({
name: 'executions-page', name: 'executions-page',
components: { components: {
ExecutionsSidebar, ExecutionsSidebar,
@@ -62,16 +88,14 @@ export default mixins(restApi, showMessage, executionHelpers, debounceHelper, wo
}; };
}, },
computed: { computed: {
...mapStores( ...mapStores(useTagsStore, useNodeTypesStore, useSettingsStore, useUIStore, useWorkflowsStore),
useTagsStore,
useNodeTypesStore,
useSettingsStore,
useUIStore,
useWorkflowsStore,
),
hidePreview(): boolean { hidePreview(): boolean {
const nothingToShow = this.executions.length === 0 && this.filterApplied; const nothingToShow = this.executions.length === 0 && this.filterApplied;
const activeNotPresent = this.filterApplied && (this.executions as IExecutionsSummary[]).find(ex => ex.id === this.activeExecution.id) === undefined; const activeNotPresent =
this.filterApplied &&
(this.executions as IExecutionsSummary[]).find(
(ex) => ex.id === this.activeExecution.id,
) === undefined;
return this.loading || nothingToShow || activeNotPresent; return this.loading || nothingToShow || activeNotPresent;
}, },
showSidebar(): boolean { showSidebar(): boolean {
@@ -84,7 +108,10 @@ export default mixins(restApi, showMessage, executionHelpers, debounceHelper, wo
return this.filter.status !== ''; return this.filter.status !== '';
}, },
workflowDataNotLoaded(): boolean { workflowDataNotLoaded(): boolean {
return this.workflowsStore.workflowId === PLACEHOLDER_EMPTY_WORKFLOW_ID && this.workflowsStore.workflowName === ''; return (
this.workflowsStore.workflowId === PLACEHOLDER_EMPTY_WORKFLOW_ID &&
this.workflowsStore.workflowName === ''
);
}, },
loadedFinishedExecutionsCount(): number { loadedFinishedExecutionsCount(): number {
return this.workflowsStore.getAllLoadedFinishedExecutions.length; return this.workflowsStore.getAllLoadedFinishedExecutions.length;
@@ -93,8 +120,8 @@ export default mixins(restApi, showMessage, executionHelpers, debounceHelper, wo
return this.workflowsStore.getTotalFinishedExecutionsCount; return this.workflowsStore.getTotalFinishedExecutionsCount;
}, },
}, },
watch:{ watch: {
$route (to: Route, from: Route) { $route(to: Route, from: Route) {
const workflowChanged = from.params.name !== to.params.name; const workflowChanged = from.params.name !== to.params.name;
this.initView(workflowChanged); this.initView(workflowChanged);
@@ -141,7 +168,9 @@ export default mixins(restApi, showMessage, executionHelpers, debounceHelper, wo
async mounted() { async mounted() {
this.loading = true; this.loading = true;
const workflowUpdated = this.$route.params.name !== this.workflowsStore.workflowId; const workflowUpdated = this.$route.params.name !== this.workflowsStore.workflowId;
const onNewWorkflow = this.$route.params.name === 'new' && this.workflowsStore.workflowId === PLACEHOLDER_EMPTY_WORKFLOW_ID; const onNewWorkflow =
this.$route.params.name === 'new' &&
this.workflowsStore.workflowId === PLACEHOLDER_EMPTY_WORKFLOW_ID;
const shouldUpdate = workflowUpdated && !onNewWorkflow; const shouldUpdate = workflowUpdated && !onNewWorkflow;
await this.initView(shouldUpdate); await this.initView(shouldUpdate);
if (!shouldUpdate) { if (!shouldUpdate) {
@@ -150,7 +179,7 @@ export default mixins(restApi, showMessage, executionHelpers, debounceHelper, wo
this.loading = false; this.loading = false;
}, },
methods: { methods: {
async initView(loadWorkflow: boolean) : Promise<void> { async initView(loadWorkflow: boolean): Promise<void> {
if (loadWorkflow) { if (loadWorkflow) {
if (this.nodeTypesStore.allNodeTypes.length === 0) { if (this.nodeTypesStore.allNodeTypes.length === 0) {
await this.nodeTypesStore.getNodeTypes(); await this.nodeTypesStore.getNodeTypes();
@@ -159,20 +188,25 @@ export default mixins(restApi, showMessage, executionHelpers, debounceHelper, wo
this.uiStore.nodeViewInitialized = false; this.uiStore.nodeViewInitialized = false;
this.setExecutions(); this.setExecutions();
if (this.activeExecution) { if (this.activeExecution) {
this.$router.push({ this.$router
name: VIEWS.EXECUTION_PREVIEW, .push({
params: { name: this.currentWorkflow, executionId: this.activeExecution.id }, name: VIEWS.EXECUTION_PREVIEW,
}).catch(()=>{});; params: { name: this.currentWorkflow, executionId: this.activeExecution.id },
})
.catch(() => {});
} }
} }
}, },
async onLoadMore(): Promise<void> { async onLoadMore(): Promise<void> {
if (!this.loadingMore) { if (!this.loadingMore) {
this.callDebounced("loadMore", { debounceTime: 1000 }); this.callDebounced('loadMore', { debounceTime: 1000 });
} }
}, },
async loadMore(): Promise<void> { async loadMore(): Promise<void> {
if (this.filter.status === 'running' || this.loadedFinishedExecutionsCount >= this.totalFinishedExecutionsCount) { if (
this.filter.status === 'running' ||
this.loadedFinishedExecutionsCount >= this.totalFinishedExecutionsCount
) {
return; return;
} }
this.loadingMore = true; this.loadingMore = true;
@@ -186,7 +220,7 @@ export default mixins(restApi, showMessage, executionHelpers, debounceHelper, wo
const requestFilter: IDataObject = { workflowId: this.currentWorkflow }; const requestFilter: IDataObject = { workflowId: this.currentWorkflow };
if (this.filter.status === 'waiting') { if (this.filter.status === 'waiting') {
requestFilter.waitTill = true; requestFilter.waitTill = true;
} else if (this.filter.status !== '') { } else if (this.filter.status !== '') {
requestFilter.finished = this.filter.status === 'success'; requestFilter.finished = this.filter.status === 'success';
} }
let data: IExecutionsListResponse; let data: IExecutionsListResponse;
@@ -194,10 +228,7 @@ export default mixins(restApi, showMessage, executionHelpers, debounceHelper, wo
data = await this.restApi().getPastExecutions(requestFilter, 20, lastId); data = await this.restApi().getPastExecutions(requestFilter, 20, lastId);
} catch (error) { } catch (error) {
this.loadingMore = false; this.loadingMore = false;
this.$showError( this.$showError(error, this.$locale.baseText('executionsList.showError.loadMore.title'));
error,
this.$locale.baseText('executionsList.showError.loadMore.title'),
);
return; return;
} }
@@ -205,9 +236,9 @@ export default mixins(restApi, showMessage, executionHelpers, debounceHelper, wo
// @ts-ignore // @ts-ignore
return { ...execution, mode: execution.mode }; return { ...execution, mode: execution.mode };
}); });
const currentExecutions = [ ...this.executions ]; const currentExecutions = [...this.executions];
for (const newExecution of data.results) { for (const newExecution of data.results) {
if (currentExecutions.find(ex => ex.id === newExecution.id) === undefined) { if (currentExecutions.find((ex) => ex.id === newExecution.id) === undefined) {
currentExecutions.push(newExecution); currentExecutions.push(newExecution);
} }
} }
@@ -217,16 +248,19 @@ export default mixins(restApi, showMessage, executionHelpers, debounceHelper, wo
async onDeleteCurrentExecution(): Promise<void> { async onDeleteCurrentExecution(): Promise<void> {
this.loading = true; this.loading = true;
try { try {
await this.restApi().deleteExecutions({ ids: [ this.$route.params.executionId ] }); await this.restApi().deleteExecutions({ ids: [this.$route.params.executionId] });
await this.setExecutions(); await this.setExecutions();
// Select first execution in the list after deleting the current one // Select first execution in the list after deleting the current one
if (this.executions.length > 0) { if (this.executions.length > 0) {
this.workflowsStore.activeWorkflowExecution = this.executions[0]; this.workflowsStore.activeWorkflowExecution = this.executions[0];
this.$router.push({ this.$router
name: VIEWS.EXECUTION_PREVIEW, .push({
params: { name: this.currentWorkflow, executionId: this.executions[0].id }, name: VIEWS.EXECUTION_PREVIEW,
}).catch(()=>{});; params: { name: this.currentWorkflow, executionId: this.executions[0].id },
} else { // If there are no executions left, show empty state and clear active execution from the store })
.catch(() => {});
} else {
// If there are no executions left, show empty state and clear active execution from the store
this.workflowsStore.activeWorkflowExecution = null; this.workflowsStore.activeWorkflowExecution = null;
this.$router.push({ name: VIEWS.EXECUTION_HOME, params: { name: this.currentWorkflow } }); this.$router.push({ name: VIEWS.EXECUTION_HOME, params: { name: this.currentWorkflow } });
} }
@@ -253,10 +287,9 @@ export default mixins(restApi, showMessage, executionHelpers, debounceHelper, wo
this.$showMessage({ this.$showMessage({
title: this.$locale.baseText('executionsList.showMessage.stopExecution.title'), title: this.$locale.baseText('executionsList.showMessage.stopExecution.title'),
message: this.$locale.baseText( message: this.$locale.baseText('executionsList.showMessage.stopExecution.message', {
'executionsList.showMessage.stopExecution.message', interpolate: { activeExecutionId },
{ interpolate: { activeExecutionId } }, }),
),
type: 'success', type: 'success',
}); });
@@ -268,7 +301,7 @@ export default mixins(restApi, showMessage, executionHelpers, debounceHelper, wo
); );
} }
}, },
onFilterUpdated(newFilter: { finished: boolean, status: string }): void { onFilterUpdated(newFilter: { finished: boolean; status: string }): void {
this.filter = newFilter; this.filter = newFilter;
this.setExecutions(); this.setExecutions();
}, },
@@ -280,13 +313,13 @@ export default mixins(restApi, showMessage, executionHelpers, debounceHelper, wo
async loadAutoRefresh(): Promise<void> { async loadAutoRefresh(): Promise<void> {
// Most of the auto-refresh logic is taken from the `ExecutionsList` component // Most of the auto-refresh logic is taken from the `ExecutionsList` component
const fetchedExecutions: IExecutionsSummary[] = await this.loadExecutions(); const fetchedExecutions: IExecutionsSummary[] = await this.loadExecutions();
let existingExecutions: IExecutionsSummary[] = [ ...this.executions ]; let existingExecutions: IExecutionsSummary[] = [...this.executions];
const alreadyPresentExecutionIds = existingExecutions.map(exec => parseInt(exec.id, 10)); const alreadyPresentExecutionIds = existingExecutions.map((exec) => parseInt(exec.id, 10));
let lastId = 0; let lastId = 0;
const gaps = [] as number[]; const gaps = [] as number[];
let updatedActiveExecution = null; let updatedActiveExecution = null;
for(let i = fetchedExecutions.length - 1; i >= 0; i--) { for (let i = fetchedExecutions.length - 1; i >= 0; i--) {
const currentItem = fetchedExecutions[i]; const currentItem = fetchedExecutions[i];
const currentId = parseInt(currentItem.id, 10); const currentId = parseInt(currentItem.id, 10);
if (lastId !== 0 && isNaN(currentId) === false) { if (lastId !== 0 && isNaN(currentId) === false) {
@@ -299,9 +332,12 @@ export default mixins(restApi, showMessage, executionHelpers, debounceHelper, wo
const executionIndex = alreadyPresentExecutionIds.indexOf(currentId); const executionIndex = alreadyPresentExecutionIds.indexOf(currentId);
if (executionIndex !== -1) { if (executionIndex !== -1) {
const existingExecution = existingExecutions.find(ex => ex.id === currentItem.id); const existingExecution = existingExecutions.find((ex) => ex.id === currentItem.id);
const existingStillRunning = existingExecution && existingExecution.finished === false || existingExecution?.stoppedAt === undefined; const existingStillRunning =
const currentFinished = currentItem.finished === true || currentItem.stoppedAt !== undefined; (existingExecution && existingExecution.finished === false) ||
existingExecution?.stoppedAt === undefined;
const currentFinished =
currentItem.finished === true || currentItem.stoppedAt !== undefined;
if (existingStillRunning && currentFinished) { if (existingStillRunning && currentFinished) {
existingExecutions[executionIndex] = currentItem; existingExecutions[executionIndex] = currentItem;
@@ -324,19 +360,25 @@ export default mixins(restApi, showMessage, executionHelpers, debounceHelper, wo
} }
} }
existingExecutions = existingExecutions.filter(execution => !gaps.includes(parseInt(execution.id, 10)) && lastId >= parseInt(execution.id, 10)); existingExecutions = existingExecutions.filter(
(execution) =>
!gaps.includes(parseInt(execution.id, 10)) && lastId >= parseInt(execution.id, 10),
);
this.workflowsStore.currentWorkflowExecutions = existingExecutions; this.workflowsStore.currentWorkflowExecutions = existingExecutions;
if (updatedActiveExecution !== null) { if (updatedActiveExecution !== null) {
this.workflowsStore.activeWorkflowExecution = updatedActiveExecution; this.workflowsStore.activeWorkflowExecution = updatedActiveExecution;
} else { } else {
const activeNotInTheList = existingExecutions.find(ex => ex.id === this.activeExecution.id) === undefined; const activeNotInTheList =
existingExecutions.find((ex) => ex.id === this.activeExecution.id) === undefined;
if (activeNotInTheList && this.executions.length > 0) { if (activeNotInTheList && this.executions.length > 0) {
this.$router.push({ this.$router
name: VIEWS.EXECUTION_PREVIEW, .push({
params: { name: this.currentWorkflow, executionId: this.executions[0].id }, name: VIEWS.EXECUTION_PREVIEW,
}).catch(()=>{}); params: { name: this.currentWorkflow, executionId: this.executions[0].id },
})
.catch(() => {});
} else if (this.executions.length === 0) { } else if (this.executions.length === 0) {
this.$router.push({ name: VIEWS.EXECUTION_HOME }).catch(()=>{}); this.$router.push({ name: VIEWS.EXECUTION_HOME }).catch(() => {});
this.workflowsStore.activeWorkflowExecution = null; this.workflowsStore.activeWorkflowExecution = null;
} }
} }
@@ -350,10 +392,7 @@ export default mixins(restApi, showMessage, executionHelpers, debounceHelper, wo
await this.workflowsStore.loadCurrentWorkflowExecutions(this.filter); await this.workflowsStore.loadCurrentWorkflowExecutions(this.filter);
return executions; return executions;
} catch (error) { } catch (error) {
this.$showError( this.$showError(error, this.$locale.baseText('executionsList.showError.refreshData.title'));
error,
this.$locale.baseText('executionsList.showError.refreshData.title'),
);
return []; return [];
} }
}, },
@@ -368,56 +407,56 @@ export default mixins(restApi, showMessage, executionHelpers, debounceHelper, wo
// If there is no execution in the route, select the first one // If there is no execution in the route, select the first one
if (this.workflowsStore.activeWorkflowExecution === null && this.executions.length > 0) { if (this.workflowsStore.activeWorkflowExecution === null && this.executions.length > 0) {
this.workflowsStore.activeWorkflowExecution = this.executions[0]; this.workflowsStore.activeWorkflowExecution = this.executions[0];
this.$router.push({ this.$router
name: VIEWS.EXECUTION_PREVIEW, .push({
params: { name: this.currentWorkflow, executionId: this.executions[0].id }, name: VIEWS.EXECUTION_PREVIEW,
}).catch(()=>{});; params: { name: this.currentWorkflow, executionId: this.executions[0].id },
})
.catch(() => {});
} }
}, },
async openWorkflow(workflowId: string): Promise<void> { async openWorkflow(workflowId: string): Promise<void> {
await this.loadActiveWorkflows(); await this.loadActiveWorkflows();
let data: IWorkflowDb | undefined; let data: IWorkflowDb | undefined;
try { try {
data = await this.restApi().getWorkflow(workflowId); data = await this.restApi().getWorkflow(workflowId);
} catch (error) { } catch (error) {
this.$showError( this.$showError(error, this.$locale.baseText('nodeView.showError.openWorkflow.title'));
error, return;
this.$locale.baseText('nodeView.showError.openWorkflow.title'), }
); if (data === undefined) {
return; throw new Error(
} this.$locale.baseText('nodeView.workflowWithIdCouldNotBeFound', {
if (data === undefined) { interpolate: { workflowId },
throw new Error( }),
this.$locale.baseText( );
'nodeView.workflowWithIdCouldNotBeFound', }
{ interpolate: { workflowId } }, await this.addNodes(data.nodes, data.connections);
),
);
}
await this.addNodes(data.nodes, data.connections);
this.workflowsStore.setActive(data.active || false); this.workflowsStore.setActive(data.active || false);
this.workflowsStore.setWorkflowId(workflowId); this.workflowsStore.setWorkflowId(workflowId);
this.workflowsStore.setWorkflowName({ newName: data.name, setStateDirty: false }); this.workflowsStore.setWorkflowName({ newName: data.name, setStateDirty: false });
this.workflowsStore.setWorkflowSettings(data.settings || {}); this.workflowsStore.setWorkflowSettings(data.settings || {});
this.workflowsStore.setWorkflowPinData(data.pinData || {}); this.workflowsStore.setWorkflowPinData(data.pinData || {});
const tags = (data.tags || []) as ITag[]; const tags = (data.tags || []) as ITag[];
const tagIds = tags.map((tag) => tag.id); const tagIds = tags.map((tag) => tag.id);
this.workflowsStore.setWorkflowTagIds(tagIds || []); this.workflowsStore.setWorkflowTagIds(tagIds || []);
this.workflowsStore.setWorkflowVersionId(data.versionId); this.workflowsStore.setWorkflowVersionId(data.versionId);
this.tagsStore.upsertTags(tags); this.tagsStore.upsertTags(tags);
this.$externalHooks().run('workflow.open', { workflowId, workflowName: data.name }); this.$externalHooks().run('workflow.open', { workflowId, workflowName: data.name });
this.uiStore.stateIsDirty = false; this.uiStore.stateIsDirty = false;
}, },
async addNodes(nodes: INodeUi[], connections?: IConnections) { async addNodes(nodes: INodeUi[], connections?: IConnections) {
if (!nodes || !nodes.length) { if (!nodes || !nodes.length) {
return; return;
} }
await this.loadNodesProperties(nodes.map(node => ({ name: node.type, version: node.typeVersion }))); await this.loadNodesProperties(
nodes.map((node) => ({ name: node.type, version: node.typeVersion })),
);
let nodeType: INodeTypeDescription | null; let nodeType: INodeTypeDescription | null;
nodes.forEach((node) => { nodes.forEach((node) => {
@@ -441,9 +480,18 @@ export default mixins(restApi, showMessage, executionHelpers, debounceHelper, wo
if (nodeType !== null) { if (nodeType !== null) {
let nodeParameters = null; let nodeParameters = null;
try { try {
nodeParameters = NodeHelpers.getNodeParameters(nodeType.properties, node.parameters, true, false, node); nodeParameters = NodeHelpers.getNodeParameters(
nodeType.properties,
node.parameters,
true,
false,
node,
);
} catch (e) { } catch (e) {
console.error(this.$locale.baseText('nodeView.thereWasAProblemLoadingTheNodeParametersOfNode') + `: "${node.name}"`); // eslint-disable-line no-console console.error(
this.$locale.baseText('nodeView.thereWasAProblemLoadingTheNodeParametersOfNode') +
`: "${node.name}"`,
); // eslint-disable-line no-console
console.error(e); // eslint-disable-line no-console console.error(e); // eslint-disable-line no-console
} }
node.parameters = nodeParameters !== null ? nodeParameters : {}; node.parameters = nodeParameters !== null ? nodeParameters : {};
@@ -462,14 +510,16 @@ export default mixins(restApi, showMessage, executionHelpers, debounceHelper, wo
let connectionData; let connectionData;
for (const sourceNode of Object.keys(connections)) { for (const sourceNode of Object.keys(connections)) {
for (const type of Object.keys(connections[sourceNode])) { for (const type of Object.keys(connections[sourceNode])) {
for (let sourceIndex = 0; sourceIndex < connections[sourceNode][type].length; sourceIndex++) { for (
let sourceIndex = 0;
sourceIndex < connections[sourceNode][type].length;
sourceIndex++
) {
const outwardConnections = connections[sourceNode][type][sourceIndex]; const outwardConnections = connections[sourceNode][type][sourceIndex];
if (!outwardConnections) { if (!outwardConnections) {
continue; continue;
} }
outwardConnections.forEach(( outwardConnections.forEach((targetData) => {
targetData,
) => {
connectionData = [ connectionData = [
{ {
node: sourceNode, node: sourceNode,
@@ -483,7 +533,10 @@ export default mixins(restApi, showMessage, executionHelpers, debounceHelper, wo
}, },
] as [IConnection, IConnection]; ] as [IConnection, IConnection];
this.workflowsStore.addConnection({ connection: connectionData, setStateDirty: false }); this.workflowsStore.addConnection({
connection: connectionData,
setStateDirty: false,
});
}); });
} }
} }
@@ -494,14 +547,15 @@ export default mixins(restApi, showMessage, executionHelpers, debounceHelper, wo
const allNodes: INodeTypeDescription[] = this.nodeTypesStore.allNodeTypes; const allNodes: INodeTypeDescription[] = this.nodeTypesStore.allNodeTypes;
const nodesToBeFetched: INodeTypeNameVersion[] = []; const nodesToBeFetched: INodeTypeNameVersion[] = [];
allNodes.forEach(node => { allNodes.forEach((node) => {
const nodeVersions = Array.isArray(node.version) ? node.version : [node.version]; const nodeVersions = Array.isArray(node.version) ? node.version : [node.version];
if (!!nodeInfos.find(n => n.name === node.name && nodeVersions.includes(n.version)) && !node.hasOwnProperty('properties')) { if (
!!nodeInfos.find((n) => n.name === node.name && nodeVersions.includes(n.version)) &&
!node.hasOwnProperty('properties')
) {
nodesToBeFetched.push({ nodesToBeFetched.push({
name: node.name, name: node.name,
version: Array.isArray(node.version) version: Array.isArray(node.version) ? node.version.slice(-1)[0] : node.version,
? node.version.slice(-1)[0]
: node.version,
}); });
} }
}); });
@@ -515,7 +569,7 @@ export default mixins(restApi, showMessage, executionHelpers, debounceHelper, wo
const activeWorkflows = await this.restApi().getActiveWorkflows(); const activeWorkflows = await this.restApi().getActiveWorkflows();
this.workflowsStore.activeWorkflows = activeWorkflows; this.workflowsStore.activeWorkflows = activeWorkflows;
}, },
async onRetryExecution(payload: { execution: IExecutionsSummary, command: string }) { async onRetryExecution(payload: { execution: IExecutionsSummary; command: string }) {
const loadWorkflow = payload.command === 'current-workflow'; const loadWorkflow = payload.command === 'current-workflow';
this.$showMessage({ this.$showMessage({
@@ -547,7 +601,6 @@ export default mixins(restApi, showMessage, executionHelpers, debounceHelper, wo
type: 'error', type: 'error',
}); });
} }
} catch (error) { } catch (error) {
this.$showError( this.$showError(
error, error,
@@ -560,7 +613,6 @@ export default mixins(restApi, showMessage, executionHelpers, debounceHelper, wo
</script> </script>
<style module lang="scss"> <style module lang="scss">
.container { .container {
display: flex; display: flex;
height: 100%; height: 100%;
@@ -575,5 +627,4 @@ export default mixins(restApi, showMessage, executionHelpers, debounceHelper, wo
margin-top: var(--spacing-2xl); margin-top: var(--spacing-2xl);
text-align: center; text-align: center;
} }
</style> </style>

View File

@@ -1,15 +1,15 @@
<template> <template>
<!-- mock el-input element to apply styles --> <!-- mock el-input element to apply styles -->
<div :class="{'el-input': true, 'static-size': staticSize}" :data-value="hiddenValue"> <div :class="{ 'el-input': true, 'static-size': staticSize }" :data-value="hiddenValue">
<slot></slot> <slot></slot>
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts">
import Vue from "vue"; import Vue from 'vue';
export default Vue.extend({ export default Vue.extend({
name: "ExpandableInputBase", name: 'ExpandableInputBase',
props: ['value', 'placeholder', 'staticSize'], props: ['value', 'placeholder', 'staticSize'],
computed: { computed: {
hiddenValue() { hiddenValue() {
@@ -19,7 +19,7 @@ export default Vue.extend({
value = this.$props.placeholder; value = this.$props.placeholder;
} }
return `${value}`; // adjust for padding return `${value}`; // adjust for padding
}, },
}, },
}); });
@@ -44,7 +44,6 @@ div.el-input {
font: inherit; font: inherit;
} }
&::after { &::after {
content: attr(data-value) ' '; content: attr(data-value) ' ';
visibility: hidden; visibility: hidden;

View File

@@ -16,12 +16,12 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import Vue from "vue"; import Vue from 'vue';
import ExpandableInputBase from "./ExpandableInputBase.vue"; import ExpandableInputBase from './ExpandableInputBase.vue';
export default Vue.extend({ export default Vue.extend({
components: { ExpandableInputBase }, components: { ExpandableInputBase },
name: "ExpandableInputEdit", name: 'ExpandableInputEdit',
props: ['value', 'placeholder', 'maxlength', 'autofocus', 'eventBus'], props: ['value', 'placeholder', 'maxlength', 'autofocus', 'eventBus'],
mounted() { mounted() {
// autofocus on input element is not reliable // autofocus on input element is not reliable

View File

@@ -12,13 +12,13 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import Vue from "vue"; import Vue from 'vue';
import ExpandableInputBase from "./ExpandableInputBase.vue"; import ExpandableInputBase from './ExpandableInputBase.vue';
export default Vue.extend({ export default Vue.extend({
components: { ExpandableInputBase }, components: { ExpandableInputBase },
name: "ExpandableInputPreview", name: 'ExpandableInputPreview',
props: ["value"], props: ['value'],
}); });
</script> </script>

View File

@@ -1,6 +1,13 @@
<template> <template>
<div v-if="dialogVisible" @keydown.stop> <div v-if="dialogVisible" @keydown.stop>
<el-dialog :visible="dialogVisible" custom-class="expression-dialog classic" append-to-body width="80%" :title="$locale.baseText('expressionEdit.editExpression')" :before-close="closeDialog"> <el-dialog
:visible="dialogVisible"
custom-class="expression-dialog classic"
append-to-body
width="80%"
:title="$locale.baseText('expressionEdit.editExpression')"
:before-close="closeDialog"
>
<el-row> <el-row>
<el-col :span="8"> <el-col :span="8">
<div class="header-side-menu"> <div class="header-side-menu">
@@ -58,10 +65,8 @@
/> />
</div> </div>
</div> </div>
</el-col> </el-col>
</el-row> </el-row>
</el-dialog> </el-dialog>
</div> </div>
</template> </template>
@@ -87,25 +92,15 @@ import { useNDVStore } from '@/stores/ndv';
import type { Resolvable, Segment } from './ExpressionEditorModal/types'; import type { Resolvable, Segment } from './ExpressionEditorModal/types';
export default mixins( export default mixins(externalHooks, genericHelpers, debounceHelper).extend({
externalHooks,
genericHelpers,
debounceHelper,
).extend({
name: 'ExpressionEdit', name: 'ExpressionEdit',
props: [ props: ['dialogVisible', 'parameter', 'path', 'value', 'eventSource'],
'dialogVisible',
'parameter',
'path',
'value',
'eventSource',
],
components: { components: {
ExpressionModalInput, ExpressionModalInput,
ExpressionModalOutput, ExpressionModalOutput,
VariableSelector, VariableSelector,
}, },
data () { data() {
return { return {
displayValue: '', displayValue: '',
latestValue: '', latestValue: '',
@@ -114,13 +109,10 @@ export default mixins(
}; };
}, },
computed: { computed: {
...mapStores( ...mapStores(useNDVStore, useWorkflowsStore),
useNDVStore,
useWorkflowsStore,
),
}, },
methods: { methods: {
valueChanged ({ value, segments }: { value: string, segments: Segment[] }, forceUpdate = false) { valueChanged({ value, segments }: { value: string; segments: Segment[] }, forceUpdate = false) {
this.latestValue = value; this.latestValue = value;
this.segments = segments; this.segments = segments;
@@ -132,11 +124,11 @@ export default mixins(
} }
}, },
updateDisplayValue () { updateDisplayValue() {
this.displayValue = this.latestValue; this.displayValue = this.latestValue;
}, },
closeDialog () { closeDialog() {
if (this.latestValue !== this.value) { if (this.latestValue !== this.value) {
// Handle the close externally as the visible parameter is an external prop // Handle the close externally as the visible parameter is an external prop
// and is so not allowed to be changed here. // and is so not allowed to be changed here.
@@ -146,9 +138,13 @@ export default mixins(
return false; return false;
}, },
itemSelected (eventData: IVariableItemSelected) { itemSelected(eventData: IVariableItemSelected) {
(this.$refs.inputFieldExpression as any).itemSelected(eventData); // tslint:disable-line:no-any (this.$refs.inputFieldExpression as any).itemSelected(eventData); // tslint:disable-line:no-any
this.$externalHooks().run('expressionEdit.itemSelected', { parameter: this.parameter, value: this.value, selectedItem: eventData }); this.$externalHooks().run('expressionEdit.itemSelected', {
parameter: this.parameter,
value: this.value,
selectedItem: eventData,
});
const trackProperties: { const trackProperties: {
event_version: string; event_version: string;
@@ -162,11 +158,11 @@ export default mixins(
node_name: string; node_name: string;
} = { } = {
event_version: '2', event_version: '2',
node_type_dest: this.ndvStore.activeNode? this.ndvStore.activeNode.type : '', node_type_dest: this.ndvStore.activeNode ? this.ndvStore.activeNode.type : '',
parameter_name_dest: this.parameter.displayName, parameter_name_dest: this.parameter.displayName,
is_immediate_input: false, is_immediate_input: false,
variable_expression: eventData.variable, variable_expression: eventData.variable,
node_name: this.ndvStore.activeNode? this.ndvStore.activeNode.name : '', node_name: this.ndvStore.activeNode ? this.ndvStore.activeNode.name : '',
}; };
if (eventData.variable) { if (eventData.variable) {
@@ -184,47 +180,63 @@ export default mixins(
if (splitVar[0].startsWith('$node')) { if (splitVar[0].startsWith('$node')) {
const sourceNodeName = splitVar[0].split('"')[1]; const sourceNodeName = splitVar[0].split('"')[1];
trackProperties.node_type_source = this.workflowsStore.getNodeByName(sourceNodeName)?.type; trackProperties.node_type_source =
const nodeConnections: Array<Array<{ node: string }>> = this.workflowsStore.outgoingConnectionsByNodeName(sourceNodeName).main; this.workflowsStore.getNodeByName(sourceNodeName)?.type;
trackProperties.is_immediate_input = (nodeConnections && nodeConnections[0] && !!nodeConnections[0].find(({ node }) => node === this.ndvStore.activeNode?.name || '')) ? true : false; const nodeConnections: Array<Array<{ node: string }>> =
this.workflowsStore.outgoingConnectionsByNodeName(sourceNodeName).main;
trackProperties.is_immediate_input =
nodeConnections &&
nodeConnections[0] &&
!!nodeConnections[0].find(({ node }) => node === this.ndvStore.activeNode?.name || '')
? true
: false;
if (splitVar[1].startsWith('parameter')) { if (splitVar[1].startsWith('parameter')) {
trackProperties.parameter_name_source = splitVar[1].split('"')[1]; trackProperties.parameter_name_source = splitVar[1].split('"')[1];
} }
} else { } else {
trackProperties.is_immediate_input = true; trackProperties.is_immediate_input = true;
if(splitVar[0].startsWith('$parameter')) { if (splitVar[0].startsWith('$parameter')) {
trackProperties.parameter_name_source = splitVar[0].split('"')[1]; trackProperties.parameter_name_source = splitVar[0].split('"')[1];
} }
} }
} }
this.$telemetry.track('User inserted item from Expression Editor variable selector', trackProperties); this.$telemetry.track(
'User inserted item from Expression Editor variable selector',
trackProperties,
);
}, },
}, },
watch: { watch: {
dialogVisible (newValue) { dialogVisible(newValue) {
this.displayValue = this.value; this.displayValue = this.value;
this.latestValue = this.value; this.latestValue = this.value;
const resolvedExpressionValue = this.$refs.expressionResult && (this.$refs.expressionResult as any).getValue() || undefined; // tslint:disable-line:no-any const resolvedExpressionValue =
this.$externalHooks().run('expressionEdit.dialogVisibleChanged', { dialogVisible: newValue, parameter: this.parameter, value: this.value, resolvedExpressionValue }); (this.$refs.expressionResult && (this.$refs.expressionResult as any).getValue()) ||
undefined; // tslint:disable-line:no-any
this.$externalHooks().run('expressionEdit.dialogVisibleChanged', {
dialogVisible: newValue,
parameter: this.parameter,
value: this.value,
resolvedExpressionValue,
});
if (!newValue) { if (!newValue) {
const resolvables = this.segments.filter((s): s is Resolvable => s.kind === 'resolvable'); const resolvables = this.segments.filter((s): s is Resolvable => s.kind === 'resolvable');
const errorResolvables = resolvables.filter(r => r.error); const errorResolvables = resolvables.filter((r) => r.error);
const exposeErrorProperties = (error: Error) => { const exposeErrorProperties = (error: Error) => {
return Object.getOwnPropertyNames(error).reduce<Record<string, unknown>>((acc, key) => { return Object.getOwnPropertyNames(error).reduce<Record<string, unknown>>((acc, key) => {
// @ts-ignore // @ts-ignore
return acc[key] = error[key], acc; return (acc[key] = error[key]), acc;
}, {}); }, {});
}; };
const telemetryPayload = { const telemetryPayload = {
empty_expression: (this.value === '=') || (this.value === '={{}}') || !this.value, empty_expression: this.value === '=' || this.value === '={{}}' || !this.value,
workflow_id: this.workflowsStore.workflowId, workflow_id: this.workflowsStore.workflowId,
source: this.eventSource, source: this.eventSource,
session_id: this.ndvStore.sessionId, session_id: this.ndvStore.sessionId,
@@ -233,12 +245,15 @@ export default mixins(
node_type: this.ndvStore.activeNode?.type ?? '', node_type: this.ndvStore.activeNode?.type ?? '',
handlebar_count: resolvables.length, handlebar_count: resolvables.length,
handlebar_error_count: errorResolvables.length, handlebar_error_count: errorResolvables.length,
full_errors: errorResolvables.map(errorResolvable => { full_errors: errorResolvables.map((errorResolvable) => {
return errorResolvable.fullError return errorResolvable.fullError
? { ...exposeErrorProperties(errorResolvable.fullError), stack: errorResolvable.fullError.stack } ? {
...exposeErrorProperties(errorResolvable.fullError),
stack: errorResolvable.fullError.stack,
}
: null; : null;
}), }),
short_errors: errorResolvables.map(r => r.resolved ?? null), short_errors: errorResolvables.map((r) => r.resolved ?? null),
}; };
this.$telemetry.track('User closed Expression Editor', telemetryPayload); this.$telemetry.track('User closed Expression Editor', telemetryPayload);
@@ -255,7 +270,7 @@ export default mixins(
font-weight: bold; font-weight: bold;
padding: 0 0 0.5em 0.2em; padding: 0 0 0.5em 0.2em;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
.hint { .hint {
color: var(--color-text-base); color: var(--color-text-base);

View File

@@ -9,47 +9,46 @@ var highlight = require('@lezer/highlight');
// This file was generated by lezer-generator. You probably shouldn't edit it. // This file was generated by lezer-generator. You probably shouldn't edit it.
const parser = lr.LRParser.deserialize({ const parser = lr.LRParser.deserialize({
version: 14, version: 14,
states: "nQQOPOOOOOO'#Cc'#CcOOOO'#Ca'#CaQQOPOOOOOO-E6_-E6_", states: "nQQOPOOOOOO'#Cc'#CcOOOO'#Ca'#CaQQOPOOOOOO-E6_-E6_",
stateData: "]~OQPORPOSPO~O", stateData: ']~OQPORPOSPO~O',
goto: "cWPPPPPXP_QRORSRTQOR", goto: 'cWPPPPPXP_QRORSRTQOR',
nodeNames: "⚠ Program Plaintext Resolvable BrokenResolvable", nodeNames: '⚠ Program Plaintext Resolvable BrokenResolvable',
maxTerm: 7, maxTerm: 7,
skippedNodes: [0], skippedNodes: [0],
repeatNodeCount: 1, repeatNodeCount: 1,
tokenData: "4f~RRO#o[#o#p{#p~[~aRQ~O#o[#o#pj#p~[~mRO#o[#p~[~~v~{OQ~~!OSO#o[#o#p![#p~[~~v~!a!YS~OX&PX^![^p&Ppq![qr![rs![st![tu![uv![vw![wx![xy![yz![z{![{|![|}![}!O![!O!P![!P!Q![!Q![![![!]![!]!^![!^!_![!_!`![!`!a![!a!b![!b!c![!c!}![!}#O![#O#P&d#P#Q![#Q#R![#R#S![#S#T![#T#o![#o#p![#p#q![#q#r4W#r#s![#s#y&P#y#z![#z$f&P$f$g![$g#BY&P#BY#BZ![#BZ$IS&P$IS$I_![$I_$I|&P$I|$JO![$JO$JT&P$JT$JU![$JU$KV&P$KV$KW![$KW&FU&P&FU&FV![&FV~&P~&URS~O#q&P#q#r&_#r~&P~&dOS~~&i!YS~OX&PX^![^p&Ppq![qr![rs![st![tu![uv![vw![wx![xy![yz![z{![{|![|}![}!O![!O!P![!P!Q![!Q![![![!]![!]!^![!^!_![!_!`![!`!a![!a!b![!b!c![!c!}![!}#O![#O#P&d#P#Q![#Q#R![#R#S![#S#T![#T#o![#o#p![#p#q![#q#r*X#r#s![#s#y&P#y#z![#z$f&P$f$g![$g#BY&P#BY#BZ![#BZ$IS&P$IS$I_![$I_$I|&P$I|$JO![$JO$JT&P$JT$JU![$JU$KV&P$KV$KW![$KW&FU&P&FU&FV![&FV~&P~*^RS~O#q*g#q#r0s#r~*g~*j}X^*gpq*gqr*grs*gst*gtu*guv*gvw*gwx*gxy*gyz*gz{*g{|*g|}*g}!O*g!O!P*g!P!Q*g!Q![*g![!]*g!]!^*g!^!_*g!_!`*g!`!a*g!a!b*g!b!c*g!c!}*g!}#O*g#O#P-g#P#Q*g#Q#R*g#R#S*g#S#T*g#T#o*g#o#p*g#p#q*g#q#r3u#r#s*g#y#z*g$f$g*g#BY#BZ*g$IS$I_*g$I|$JO*g$JT$JU*g$KV$KW*g&FU&FV*g~-j}X^*gpq*gqr*grs*gst*gtu*guv*gvw*gwx*gxy*gyz*gz{*g{|*g|}*g}!O*g!O!P*g!P!Q*g!Q![*g![!]*g!]!^*g!^!_*g!_!`*g!`!a*g!a!b*g!b!c*g!c!}*g!}#O*g#O#P-g#P#Q*g#Q#R*g#R#S*g#S#T*g#T#o*g#o#p*g#p#q*g#q#r0g#r#s*g#y#z*g$f$g*g#BY#BZ*g$IS$I_*g$I|$JO*g$JT$JU*g$KV$KW*g&FU&FV*g~0jRO#q*g#q#r0s#r~*g~0x}R~X^*gpq*gqr*grs*gst*gtu*guv*gvw*gwx*gxy*gyz*gz{*g{|*g|}*g}!O*g!O!P*g!P!Q*g!Q![*g![!]*g!]!^*g!^!_*g!_!`*g!`!a*g!a!b*g!b!c*g!c!}*g!}#O*g#O#P-g#P#Q*g#Q#R*g#R#S*g#S#T*g#T#o*g#o#p*g#p#q*g#q#r3u#r#s*g#y#z*g$f$g*g#BY#BZ*g$IS$I_*g$I|$JO*g$JT$JU*g$KV$KW*g&FU&FV*g~3xRO#q*g#q#r4R#r~*g~4WOR~~4]RS~O#q*g#q#r4R#r~*g", tokenData:
tokenizers: [0], '4f~RRO#o[#o#p{#p~[~aRQ~O#o[#o#pj#p~[~mRO#o[#p~[~~v~{OQ~~!OSO#o[#o#p![#p~[~~v~!a!YS~OX&PX^![^p&Ppq![qr![rs![st![tu![uv![vw![wx![xy![yz![z{![{|![|}![}!O![!O!P![!P!Q![!Q![![![!]![!]!^![!^!_![!_!`![!`!a![!a!b![!b!c![!c!}![!}#O![#O#P&d#P#Q![#Q#R![#R#S![#S#T![#T#o![#o#p![#p#q![#q#r4W#r#s![#s#y&P#y#z![#z$f&P$f$g![$g#BY&P#BY#BZ![#BZ$IS&P$IS$I_![$I_$I|&P$I|$JO![$JO$JT&P$JT$JU![$JU$KV&P$KV$KW![$KW&FU&P&FU&FV![&FV~&P~&URS~O#q&P#q#r&_#r~&P~&dOS~~&i!YS~OX&PX^![^p&Ppq![qr![rs![st![tu![uv![vw![wx![xy![yz![z{![{|![|}![}!O![!O!P![!P!Q![!Q![![![!]![!]!^![!^!_![!_!`![!`!a![!a!b![!b!c![!c!}![!}#O![#O#P&d#P#Q![#Q#R![#R#S![#S#T![#T#o![#o#p![#p#q![#q#r*X#r#s![#s#y&P#y#z![#z$f&P$f$g![$g#BY&P#BY#BZ![#BZ$IS&P$IS$I_![$I_$I|&P$I|$JO![$JO$JT&P$JT$JU![$JU$KV&P$KV$KW![$KW&FU&P&FU&FV![&FV~&P~*^RS~O#q*g#q#r0s#r~*g~*j}X^*gpq*gqr*grs*gst*gtu*guv*gvw*gwx*gxy*gyz*gz{*g{|*g|}*g}!O*g!O!P*g!P!Q*g!Q![*g![!]*g!]!^*g!^!_*g!_!`*g!`!a*g!a!b*g!b!c*g!c!}*g!}#O*g#O#P-g#P#Q*g#Q#R*g#R#S*g#S#T*g#T#o*g#o#p*g#p#q*g#q#r3u#r#s*g#y#z*g$f$g*g#BY#BZ*g$IS$I_*g$I|$JO*g$JT$JU*g$KV$KW*g&FU&FV*g~-j}X^*gpq*gqr*grs*gst*gtu*guv*gvw*gwx*gxy*gyz*gz{*g{|*g|}*g}!O*g!O!P*g!P!Q*g!Q![*g![!]*g!]!^*g!^!_*g!_!`*g!`!a*g!a!b*g!b!c*g!c!}*g!}#O*g#O#P-g#P#Q*g#Q#R*g#R#S*g#S#T*g#T#o*g#o#p*g#p#q*g#q#r0g#r#s*g#y#z*g$f$g*g#BY#BZ*g$IS$I_*g$I|$JO*g$JT$JU*g$KV$KW*g&FU&FV*g~0jRO#q*g#q#r0s#r~*g~0x}R~X^*gpq*gqr*grs*gst*gtu*guv*gvw*gwx*gxy*gyz*gz{*g{|*g|}*g}!O*g!O!P*g!P!Q*g!Q![*g![!]*g!]!^*g!^!_*g!_!`*g!`!a*g!a!b*g!b!c*g!c!}*g!}#O*g#O#P-g#P#Q*g#Q#R*g#R#S*g#S#T*g#T#o*g#o#p*g#p#q*g#q#r3u#r#s*g#y#z*g$f$g*g#BY#BZ*g$IS$I_*g$I|$JO*g$JT$JU*g$KV$KW*g&FU&FV*g~3xRO#q*g#q#r4R#r~*g~4WOR~~4]RS~O#q*g#q#r4R#r~*g',
topRules: {"Program":[0,1]}, tokenizers: [0],
tokenPrec: 0 topRules: { Program: [0, 1] },
tokenPrec: 0,
}); });
const parserWithMetaData = parser.configure({ const parserWithMetaData = parser.configure({
props: [ props: [
language.foldNodeProp.add({ language.foldNodeProp.add({
Application: language.foldInside, Application: language.foldInside,
}), }),
highlight.styleTags({ highlight.styleTags({
OpenMarker: highlight.tags.brace, OpenMarker: highlight.tags.brace,
CloseMarker: highlight.tags.brace, CloseMarker: highlight.tags.brace,
Plaintext: highlight.tags.content, Plaintext: highlight.tags.content,
Resolvable: highlight.tags.string, Resolvable: highlight.tags.string,
BrokenResolvable: highlight.tags.className, BrokenResolvable: highlight.tags.className,
}), }),
], ],
}); });
const n8nExpressionLanguage = language.LRLanguage.define({ const n8nExpressionLanguage = language.LRLanguage.define({
parser: parserWithMetaData, parser: parserWithMetaData,
languageData: { languageData: {
commentTokens: { line: ";" }, commentTokens: { line: ';' },
}, },
}); });
const completions = n8nExpressionLanguage.data.of({ const completions = n8nExpressionLanguage.data.of({
autocomplete: autocomplete.completeFromList([ autocomplete: autocomplete.completeFromList([{ label: 'abcdefg', type: 'keyword' }]),
{ label: "abcdefg", type: "keyword" },
]),
}); });
function n8nExpression() { function n8nExpression() {
return new language.LanguageSupport(n8nExpressionLanguage, [completions]); return new language.LanguageSupport(n8nExpressionLanguage, [completions]);
} }
exports.n8nExpression = n8nExpression; exports.n8nExpression = n8nExpression;

View File

@@ -1,5 +1,5 @@
import { LRLanguage, LanguageSupport } from "@codemirror/language"; import { LRLanguage, LanguageSupport } from '@codemirror/language';
declare const parserWithMetaData: import("@lezer/lr").LRParser; declare const parserWithMetaData: import('@lezer/lr').LRParser;
declare const n8nExpressionLanguage: LRLanguage; declare const n8nExpressionLanguage: LRLanguage;
declare function n8nExpression(): LanguageSupport; declare function n8nExpression(): LanguageSupport;
export { parserWithMetaData, n8nExpressionLanguage, n8nExpression }; export { parserWithMetaData, n8nExpressionLanguage, n8nExpression };

View File

@@ -1,5 +1,5 @@
import { LRLanguage, LanguageSupport } from "@codemirror/language"; import { LRLanguage, LanguageSupport } from '@codemirror/language';
declare const parserWithMetaData: import("@lezer/lr").LRParser; declare const parserWithMetaData: import('@lezer/lr').LRParser;
declare const n8nExpressionLanguage: LRLanguage; declare const n8nExpressionLanguage: LRLanguage;
declare function n8nExpression(): LanguageSupport; declare function n8nExpression(): LanguageSupport;
export { parserWithMetaData, n8nExpressionLanguage, n8nExpression }; export { parserWithMetaData, n8nExpressionLanguage, n8nExpression };

View File

@@ -5,47 +5,46 @@ import { styleTags, tags } from '@lezer/highlight';
// This file was generated by lezer-generator. You probably shouldn't edit it. // This file was generated by lezer-generator. You probably shouldn't edit it.
const parser = LRParser.deserialize({ const parser = LRParser.deserialize({
version: 14, version: 14,
states: "nQQOPOOOOOO'#Cc'#CcOOOO'#Ca'#CaQQOPOOOOOO-E6_-E6_", states: "nQQOPOOOOOO'#Cc'#CcOOOO'#Ca'#CaQQOPOOOOOO-E6_-E6_",
stateData: "]~OQPORPOSPO~O", stateData: ']~OQPORPOSPO~O',
goto: "cWPPPPPXP_QRORSRTQOR", goto: 'cWPPPPPXP_QRORSRTQOR',
nodeNames: "⚠ Program Plaintext Resolvable BrokenResolvable", nodeNames: '⚠ Program Plaintext Resolvable BrokenResolvable',
maxTerm: 7, maxTerm: 7,
skippedNodes: [0], skippedNodes: [0],
repeatNodeCount: 1, repeatNodeCount: 1,
tokenData: "4f~RRO#o[#o#p{#p~[~aRQ~O#o[#o#pj#p~[~mRO#o[#p~[~~v~{OQ~~!OSO#o[#o#p![#p~[~~v~!a!YS~OX&PX^![^p&Ppq![qr![rs![st![tu![uv![vw![wx![xy![yz![z{![{|![|}![}!O![!O!P![!P!Q![!Q![![![!]![!]!^![!^!_![!_!`![!`!a![!a!b![!b!c![!c!}![!}#O![#O#P&d#P#Q![#Q#R![#R#S![#S#T![#T#o![#o#p![#p#q![#q#r4W#r#s![#s#y&P#y#z![#z$f&P$f$g![$g#BY&P#BY#BZ![#BZ$IS&P$IS$I_![$I_$I|&P$I|$JO![$JO$JT&P$JT$JU![$JU$KV&P$KV$KW![$KW&FU&P&FU&FV![&FV~&P~&URS~O#q&P#q#r&_#r~&P~&dOS~~&i!YS~OX&PX^![^p&Ppq![qr![rs![st![tu![uv![vw![wx![xy![yz![z{![{|![|}![}!O![!O!P![!P!Q![!Q![![![!]![!]!^![!^!_![!_!`![!`!a![!a!b![!b!c![!c!}![!}#O![#O#P&d#P#Q![#Q#R![#R#S![#S#T![#T#o![#o#p![#p#q![#q#r*X#r#s![#s#y&P#y#z![#z$f&P$f$g![$g#BY&P#BY#BZ![#BZ$IS&P$IS$I_![$I_$I|&P$I|$JO![$JO$JT&P$JT$JU![$JU$KV&P$KV$KW![$KW&FU&P&FU&FV![&FV~&P~*^RS~O#q*g#q#r0s#r~*g~*j}X^*gpq*gqr*grs*gst*gtu*guv*gvw*gwx*gxy*gyz*gz{*g{|*g|}*g}!O*g!O!P*g!P!Q*g!Q![*g![!]*g!]!^*g!^!_*g!_!`*g!`!a*g!a!b*g!b!c*g!c!}*g!}#O*g#O#P-g#P#Q*g#Q#R*g#R#S*g#S#T*g#T#o*g#o#p*g#p#q*g#q#r3u#r#s*g#y#z*g$f$g*g#BY#BZ*g$IS$I_*g$I|$JO*g$JT$JU*g$KV$KW*g&FU&FV*g~-j}X^*gpq*gqr*grs*gst*gtu*guv*gvw*gwx*gxy*gyz*gz{*g{|*g|}*g}!O*g!O!P*g!P!Q*g!Q![*g![!]*g!]!^*g!^!_*g!_!`*g!`!a*g!a!b*g!b!c*g!c!}*g!}#O*g#O#P-g#P#Q*g#Q#R*g#R#S*g#S#T*g#T#o*g#o#p*g#p#q*g#q#r0g#r#s*g#y#z*g$f$g*g#BY#BZ*g$IS$I_*g$I|$JO*g$JT$JU*g$KV$KW*g&FU&FV*g~0jRO#q*g#q#r0s#r~*g~0x}R~X^*gpq*gqr*grs*gst*gtu*guv*gvw*gwx*gxy*gyz*gz{*g{|*g|}*g}!O*g!O!P*g!P!Q*g!Q![*g![!]*g!]!^*g!^!_*g!_!`*g!`!a*g!a!b*g!b!c*g!c!}*g!}#O*g#O#P-g#P#Q*g#Q#R*g#R#S*g#S#T*g#T#o*g#o#p*g#p#q*g#q#r3u#r#s*g#y#z*g$f$g*g#BY#BZ*g$IS$I_*g$I|$JO*g$JT$JU*g$KV$KW*g&FU&FV*g~3xRO#q*g#q#r4R#r~*g~4WOR~~4]RS~O#q*g#q#r4R#r~*g", tokenData:
tokenizers: [0], '4f~RRO#o[#o#p{#p~[~aRQ~O#o[#o#pj#p~[~mRO#o[#p~[~~v~{OQ~~!OSO#o[#o#p![#p~[~~v~!a!YS~OX&PX^![^p&Ppq![qr![rs![st![tu![uv![vw![wx![xy![yz![z{![{|![|}![}!O![!O!P![!P!Q![!Q![![![!]![!]!^![!^!_![!_!`![!`!a![!a!b![!b!c![!c!}![!}#O![#O#P&d#P#Q![#Q#R![#R#S![#S#T![#T#o![#o#p![#p#q![#q#r4W#r#s![#s#y&P#y#z![#z$f&P$f$g![$g#BY&P#BY#BZ![#BZ$IS&P$IS$I_![$I_$I|&P$I|$JO![$JO$JT&P$JT$JU![$JU$KV&P$KV$KW![$KW&FU&P&FU&FV![&FV~&P~&URS~O#q&P#q#r&_#r~&P~&dOS~~&i!YS~OX&PX^![^p&Ppq![qr![rs![st![tu![uv![vw![wx![xy![yz![z{![{|![|}![}!O![!O!P![!P!Q![!Q![![![!]![!]!^![!^!_![!_!`![!`!a![!a!b![!b!c![!c!}![!}#O![#O#P&d#P#Q![#Q#R![#R#S![#S#T![#T#o![#o#p![#p#q![#q#r*X#r#s![#s#y&P#y#z![#z$f&P$f$g![$g#BY&P#BY#BZ![#BZ$IS&P$IS$I_![$I_$I|&P$I|$JO![$JO$JT&P$JT$JU![$JU$KV&P$KV$KW![$KW&FU&P&FU&FV![&FV~&P~*^RS~O#q*g#q#r0s#r~*g~*j}X^*gpq*gqr*grs*gst*gtu*guv*gvw*gwx*gxy*gyz*gz{*g{|*g|}*g}!O*g!O!P*g!P!Q*g!Q![*g![!]*g!]!^*g!^!_*g!_!`*g!`!a*g!a!b*g!b!c*g!c!}*g!}#O*g#O#P-g#P#Q*g#Q#R*g#R#S*g#S#T*g#T#o*g#o#p*g#p#q*g#q#r3u#r#s*g#y#z*g$f$g*g#BY#BZ*g$IS$I_*g$I|$JO*g$JT$JU*g$KV$KW*g&FU&FV*g~-j}X^*gpq*gqr*grs*gst*gtu*guv*gvw*gwx*gxy*gyz*gz{*g{|*g|}*g}!O*g!O!P*g!P!Q*g!Q![*g![!]*g!]!^*g!^!_*g!_!`*g!`!a*g!a!b*g!b!c*g!c!}*g!}#O*g#O#P-g#P#Q*g#Q#R*g#R#S*g#S#T*g#T#o*g#o#p*g#p#q*g#q#r0g#r#s*g#y#z*g$f$g*g#BY#BZ*g$IS$I_*g$I|$JO*g$JT$JU*g$KV$KW*g&FU&FV*g~0jRO#q*g#q#r0s#r~*g~0x}R~X^*gpq*gqr*grs*gst*gtu*guv*gvw*gwx*gxy*gyz*gz{*g{|*g|}*g}!O*g!O!P*g!P!Q*g!Q![*g![!]*g!]!^*g!^!_*g!_!`*g!`!a*g!a!b*g!b!c*g!c!}*g!}#O*g#O#P-g#P#Q*g#Q#R*g#R#S*g#S#T*g#T#o*g#o#p*g#p#q*g#q#r3u#r#s*g#y#z*g$f$g*g#BY#BZ*g$IS$I_*g$I|$JO*g$JT$JU*g$KV$KW*g&FU&FV*g~3xRO#q*g#q#r4R#r~*g~4WOR~~4]RS~O#q*g#q#r4R#r~*g',
topRules: {"Program":[0,1]}, tokenizers: [0],
tokenPrec: 0 topRules: { Program: [0, 1] },
tokenPrec: 0,
}); });
const parserWithMetaData = parser.configure({ const parserWithMetaData = parser.configure({
props: [ props: [
foldNodeProp.add({ foldNodeProp.add({
Application: foldInside, Application: foldInside,
}), }),
styleTags({ styleTags({
OpenMarker: tags.brace, OpenMarker: tags.brace,
CloseMarker: tags.brace, CloseMarker: tags.brace,
Plaintext: tags.content, Plaintext: tags.content,
Resolvable: tags.string, Resolvable: tags.string,
BrokenResolvable: tags.className, BrokenResolvable: tags.className,
}), }),
], ],
}); });
const n8nExpressionLanguage = LRLanguage.define({ const n8nExpressionLanguage = LRLanguage.define({
parser: parserWithMetaData, parser: parserWithMetaData,
languageData: { languageData: {
commentTokens: { line: ";" }, commentTokens: { line: ';' },
}, },
}); });
const completions = n8nExpressionLanguage.data.of({ const completions = n8nExpressionLanguage.data.of({
autocomplete: completeFromList([ autocomplete: completeFromList([{ label: 'abcdefg', type: 'keyword' }]),
{ label: "abcdefg", type: "keyword" },
]),
}); });
function n8nExpression() { function n8nExpression() {
return new LanguageSupport(n8nExpressionLanguage, [completions]); return new LanguageSupport(n8nExpressionLanguage, [completions]);
} }
export { n8nExpression, n8nExpressionLanguage, parserWithMetaData }; export { n8nExpression, n8nExpressionLanguage, parserWithMetaData };

View File

@@ -2,7 +2,7 @@
<div v-if="this.featureInfo" :class="[$style.container]"> <div v-if="this.featureInfo" :class="[$style.container]">
<div v-if="showTitle" class="mb-2xl"> <div v-if="showTitle" class="mb-2xl">
<n8n-heading size="2xlarge"> <n8n-heading size="2xlarge">
{{$locale.baseText(featureInfo.featureName)}} {{ $locale.baseText(featureInfo.featureName) }}
</n8n-heading> </n8n-heading>
</div> </div>
<div v-if="featureInfo.infoText" class="mb-l"> <div v-if="featureInfo.infoText" class="mb-l">
@@ -15,11 +15,13 @@
<div :class="$style.actionBoxContainer"> <div :class="$style.actionBoxContainer">
<n8n-action-box <n8n-action-box
:description="$locale.baseText(featureInfo.actionBoxDescription)" :description="$locale.baseText(featureInfo.actionBoxDescription)"
:buttonText="$locale.baseText(featureInfo.actionBoxButtonLabel || 'fakeDoor.actionBox.button.label')" :buttonText="
$locale.baseText(featureInfo.actionBoxButtonLabel || 'fakeDoor.actionBox.button.label')
"
@click="openLinkPage" @click="openLinkPage"
> >
<template #heading> <template #heading>
<span v-html="$locale.baseText(featureInfo.actionBoxTitle)"/> <span v-html="$locale.baseText(featureInfo.actionBoxTitle)" />
</template> </template>
</n8n-action-box> </n8n-action-box>
</div> </div>
@@ -27,7 +29,7 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import {IFakeDoor} from '@/Interface'; import { IFakeDoor } from '@/Interface';
import { useRootStore } from '@/stores/n8nRootStore'; import { useRootStore } from '@/stores/n8nRootStore';
import { useSettingsStore } from '@/stores/settings'; import { useSettingsStore } from '@/stores/settings';
import { useUIStore } from '@/stores/ui'; import { useUIStore } from '@/stores/ui';
@@ -48,12 +50,7 @@ export default Vue.extend({
}, },
}, },
computed: { computed: {
...mapStores( ...mapStores(useRootStore, useSettingsStore, useUIStore, useUsersStore),
useRootStore,
useSettingsStore,
useUIStore,
useUsersStore,
),
userId(): string { userId(): string {
return this.usersStore.currentUserId || ''; return this.usersStore.currentUserId || '';
}, },
@@ -67,8 +64,13 @@ export default Vue.extend({
methods: { methods: {
openLinkPage() { openLinkPage() {
if (this.featureInfo) { if (this.featureInfo) {
window.open(`${this.featureInfo.linkURL}&u=${this.instanceId}#${this.userId}&v=${this.rootStore.versionCli}`, '_blank'); window.open(
this.$telemetry.track('user clicked feature waiting list button', {feature: this.featureId}); `${this.featureInfo.linkURL}&u=${this.instanceId}#${this.userId}&v=${this.rootStore.versionCli}`,
'_blank',
);
this.$telemetry.track('user clicked feature waiting list button', {
feature: this.featureId,
});
} }
}, },
}, },
@@ -80,4 +82,3 @@ export default Vue.extend({
text-align: center; text-align: center;
} }
</style> </style>

View File

@@ -1,7 +1,9 @@
<template> <template>
<div @keydown.stop class="fixed-collection-parameter"> <div @keydown.stop class="fixed-collection-parameter">
<div v-if="getProperties.length === 0" class="no-items-exist"> <div v-if="getProperties.length === 0" class="no-items-exist">
<n8n-text size="small">{{ $locale.baseText('fixedCollectionParameter.currentlyNoItemsExist') }}</n8n-text> <n8n-text size="small">{{
$locale.baseText('fixedCollectionParameter.currentlyNoItemsExist')
}}</n8n-text>
</div> </div>
<div <div
@@ -10,7 +12,7 @@
class="fixed-collection-parameter-property" class="fixed-collection-parameter-property"
> >
<n8n-input-label <n8n-input-label
v-if="property.displayName !== '' && (parameter.options && parameter.options.length !== 1)" v-if="property.displayName !== '' && parameter.options && parameter.options.length !== 1"
:label="$locale.nodeText().inputLabelDisplayName(property, path)" :label="$locale.nodeText().inputLabelDisplayName(property, path)"
:underline="true" :underline="true"
size="small" size="small"
@@ -39,7 +41,7 @@
@click="moveOptionUp(property.name, index)" @click="moveOptionUp(property.name, index)"
/> />
<font-awesome-icon <font-awesome-icon
v-if="index !== (mutableValues[property.name].length - 1)" v-if="index !== mutableValues[property.name].length - 1"
icon="angle-down" icon="angle-down"
class="clickable" class="clickable"
:title="$locale.baseText('fixedCollectionParameter.moveDown')" :title="$locale.baseText('fixedCollectionParameter.moveDown')"
@@ -110,10 +112,8 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import Vue, { Component, PropType } from "vue"; import Vue, { Component, PropType } from 'vue';
import { import { IUpdateInformation } from '@/Interface';
IUpdateInformation,
} from '@/Interface';
import { import {
INodeParameters, INodeParameters,
@@ -127,187 +127,213 @@ import {
import { get } from 'lodash'; import { get } from 'lodash';
export default Vue.extend({ export default Vue.extend({
name: 'FixedCollectionParameter', name: 'FixedCollectionParameter',
props: { props: {
nodeValues: { nodeValues: {
type: Object as PropType<Record<string, INodeParameters[]>>, type: Object as PropType<Record<string, INodeParameters[]>>,
required: true, required: true,
},
parameter: {
type: Object as PropType<INodeProperties>,
required: true,
},
path: {
type: String,
required: true,
},
values: {
type: Object as PropType<Record<string, INodeParameters[]>>,
default: () => ({}),
},
isReadOnly: {
type: Boolean,
default: false,
},
}, },
components: { parameter: {
ParameterInputList: () => import('./ParameterInputList.vue') as Promise<Component>, type: Object as PropType<INodeProperties>,
required: true,
}, },
data() { path: {
return { type: String,
selectedOption: undefined, required: true,
mutableValues: {} as Record<string, INodeParameters[]>,
};
}, },
watch: { values: {
values: { type: Object as PropType<Record<string, INodeParameters[]>>,
handler(newValues: Record<string, INodeParameters[]>) { default: () => ({}),
this.mutableValues = deepCopy(newValues); },
}, isReadOnly: {
deep: true, type: Boolean,
default: false,
},
},
components: {
ParameterInputList: () => import('./ParameterInputList.vue') as Promise<Component>,
},
data() {
return {
selectedOption: undefined,
mutableValues: {} as Record<string, INodeParameters[]>,
};
},
watch: {
values: {
handler(newValues: Record<string, INodeParameters[]>) {
this.mutableValues = deepCopy(newValues);
}, },
deep: true,
}, },
created(){ },
this.mutableValues = deepCopy(this.values); created() {
this.mutableValues = deepCopy(this.values);
},
computed: {
getPlaceholderText(): string {
const placeholder = this.$locale.nodeText().placeholder(this.parameter, this.path);
return placeholder ? placeholder : this.$locale.baseText('fixedCollectionParameter.choose');
}, },
computed: { getProperties(): INodePropertyCollection[] {
getPlaceholderText(): string { const returnProperties = [];
const placeholder = this.$locale.nodeText().placeholder(this.parameter, this.path); let tempProperties;
return placeholder ? placeholder : this.$locale.baseText('fixedCollectionParameter.choose'); for (const name of this.propertyNames) {
}, tempProperties = this.getOptionProperties(name);
getProperties(): INodePropertyCollection[] { if (tempProperties !== undefined) {
const returnProperties = []; returnProperties.push(tempProperties);
let tempProperties;
for (const name of this.propertyNames) {
tempProperties = this.getOptionProperties(name);
if (tempProperties !== undefined) {
returnProperties.push(tempProperties);
}
} }
return returnProperties; }
}, return returnProperties;
multipleValues(): boolean { },
return !!this.parameter.typeOptions?.multipleValues; multipleValues(): boolean {
}, return !!this.parameter.typeOptions?.multipleValues;
},
parameterOptions(): INodePropertyCollection[] { parameterOptions(): INodePropertyCollection[] {
if (this.multipleValues && isINodePropertyCollectionList(this.parameter.options)) { if (this.multipleValues && isINodePropertyCollectionList(this.parameter.options)) {
return this.parameter.options; return this.parameter.options;
} }
return (this.parameter.options as INodePropertyCollection[]).filter((option) => { return (this.parameter.options as INodePropertyCollection[]).filter((option) => {
return !this.propertyNames.includes(option.name); return !this.propertyNames.includes(option.name);
});
},
propertyNames(): string[] {
return Object.keys(this.mutableValues || {});
},
sortable(): boolean {
return !!this.parameter.typeOptions?.sortable;
},
},
methods: {
deleteOption(optionName: string, index?: number) {
const currentOptionsOfSameType = this.mutableValues[optionName];
if (!currentOptionsOfSameType || currentOptionsOfSameType.length > 1) {
// it's not the only option of this type, so just remove it.
this.$emit('valueChanged', {
name: this.getPropertyPath(optionName, index),
value: undefined,
}); });
}, } else {
propertyNames(): string[] { // it's the only option, so remove the whole type
return Object.keys(this.mutableValues || {}); this.$emit('valueChanged', {
}, name: this.getPropertyPath(optionName),
sortable(): boolean { value: undefined,
return !!this.parameter.typeOptions?.sortable; });
}, }
}, },
methods: { getPropertyPath(name: string, index?: number) {
deleteOption(optionName: string, index?: number) { return `${this.path}.${name}` + (index !== undefined ? `[${index}]` : '');
const currentOptionsOfSameType = this.mutableValues[optionName]; },
if (!currentOptionsOfSameType || currentOptionsOfSameType.length > 1) { getOptionProperties(optionName: string): INodePropertyCollection | undefined {
// it's not the only option of this type, so just remove it. if (isINodePropertyCollectionList(this.parameter.options)) {
this.$emit('valueChanged', { for (const option of this.parameter.options) {
name: this.getPropertyPath(optionName, index), if (option.name === optionName) {
value: undefined, return option;
});
} else {
// it's the only option, so remove the whole type
this.$emit('valueChanged', {
name: this.getPropertyPath(optionName),
value: undefined,
});
}
},
getPropertyPath(name: string, index?: number) {
return `${this.path}.${name}` + (index !== undefined ? `[${index}]` : '');
},
getOptionProperties(optionName: string): INodePropertyCollection | undefined {
if(isINodePropertyCollectionList(this.parameter.options)){
for (const option of this.parameter.options) {
if (option.name === optionName) {
return option;
}
} }
} }
return undefined; }
}, return undefined;
moveOptionDown(optionName: string, index: number) {
if(Array.isArray(this.mutableValues[optionName])){
this.mutableValues[optionName].splice(index + 1, 0, this.mutableValues[optionName].splice(index, 1)[0]);
}
const parameterData = {
name: this.getPropertyPath(optionName),
value: this.mutableValues[optionName],
};
this.$emit('valueChanged', parameterData);
},
moveOptionUp(optionName: string, index: number) {
if(Array.isArray(this.mutableValues[optionName])) {
this.mutableValues?.[optionName].splice(index - 1, 0, this.mutableValues[optionName].splice(index, 1)[0]);
}
const parameterData = {
name: this.getPropertyPath(optionName),
value: this.mutableValues[optionName],
};
this.$emit('valueChanged', parameterData);
},
optionSelected(optionName: string) {
const option = this.getOptionProperties(optionName);
if (option === undefined) {
return;
}
const name = `${this.path}.${option.name}`;
const newParameterValue: INodeParameters = {};
for (const optionParameter of option.values) {
if (optionParameter.type === 'fixedCollection' && optionParameter.typeOptions !== undefined && optionParameter.typeOptions.multipleValues === true) {
newParameterValue[optionParameter.name] = {};
} else if (optionParameter.typeOptions !== undefined && optionParameter.typeOptions.multipleValues === true) {
// Multiple values are allowed so append option to array
newParameterValue[optionParameter.name] = get(this.nodeValues, `${this.path}.${optionParameter.name}`, []);
if (Array.isArray(optionParameter.default)) {
(newParameterValue[optionParameter.name] as INodeParameters[]).push(...deepCopy(optionParameter.default as INodeParameters[]));
} else if (optionParameter.default !== '' && typeof optionParameter.default !== 'object') {
(newParameterValue[optionParameter.name] as NodeParameterValue[]).push(deepCopy(optionParameter.default));
}
} else {
// Add a new option
newParameterValue[optionParameter.name] = deepCopy(optionParameter.default);
}
}
let newValue;
if (this.multipleValues) {
newValue = get(this.nodeValues, name, [] as INodeParameters[]);
newValue.push(newParameterValue);
} else {
newValue = newParameterValue;
}
const parameterData = {
name,
value: newValue,
};
this.$emit('valueChanged', parameterData);
this.selectedOption = undefined;
},
valueChanged(parameterData: IUpdateInformation) {
this.$emit('valueChanged', parameterData);
},
}, },
}); moveOptionDown(optionName: string, index: number) {
if (Array.isArray(this.mutableValues[optionName])) {
this.mutableValues[optionName].splice(
index + 1,
0,
this.mutableValues[optionName].splice(index, 1)[0],
);
}
const parameterData = {
name: this.getPropertyPath(optionName),
value: this.mutableValues[optionName],
};
this.$emit('valueChanged', parameterData);
},
moveOptionUp(optionName: string, index: number) {
if (Array.isArray(this.mutableValues[optionName])) {
this.mutableValues?.[optionName].splice(
index - 1,
0,
this.mutableValues[optionName].splice(index, 1)[0],
);
}
const parameterData = {
name: this.getPropertyPath(optionName),
value: this.mutableValues[optionName],
};
this.$emit('valueChanged', parameterData);
},
optionSelected(optionName: string) {
const option = this.getOptionProperties(optionName);
if (option === undefined) {
return;
}
const name = `${this.path}.${option.name}`;
const newParameterValue: INodeParameters = {};
for (const optionParameter of option.values) {
if (
optionParameter.type === 'fixedCollection' &&
optionParameter.typeOptions !== undefined &&
optionParameter.typeOptions.multipleValues === true
) {
newParameterValue[optionParameter.name] = {};
} else if (
optionParameter.typeOptions !== undefined &&
optionParameter.typeOptions.multipleValues === true
) {
// Multiple values are allowed so append option to array
newParameterValue[optionParameter.name] = get(
this.nodeValues,
`${this.path}.${optionParameter.name}`,
[],
);
if (Array.isArray(optionParameter.default)) {
(newParameterValue[optionParameter.name] as INodeParameters[]).push(
...deepCopy(optionParameter.default as INodeParameters[]),
);
} else if (
optionParameter.default !== '' &&
typeof optionParameter.default !== 'object'
) {
(newParameterValue[optionParameter.name] as NodeParameterValue[]).push(
deepCopy(optionParameter.default),
);
}
} else {
// Add a new option
newParameterValue[optionParameter.name] = deepCopy(optionParameter.default);
}
}
let newValue;
if (this.multipleValues) {
newValue = get(this.nodeValues, name, [] as INodeParameters[]);
newValue.push(newParameterValue);
} else {
newValue = newParameterValue;
}
const parameterData = {
name,
value: newValue,
};
this.$emit('valueChanged', parameterData);
this.selectedOption = undefined;
},
valueChanged(parameterData: IUpdateInformation) {
this.$emit('valueChanged', parameterData);
},
},
});
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">

View File

@@ -1,41 +1,40 @@
<template> <template>
<div :class="$style['gift-icon']"> <div :class="$style['gift-icon']">
<font-awesome-icon icon="gift" /> <font-awesome-icon icon="gift" />
<div :class="$style['notification']"> <div :class="$style['notification']">
<div></div> <div></div>
</div> </div>
</div> </div>
</template> </template>
<style lang="scss" module> <style lang="scss" module>
.gift-icon { .gift-icon {
display: flex; display: flex;
position: relative; position: relative;
svg { svg {
margin-right: 0 !important; margin-right: 0 !important;
} }
.notification { .notification {
height: .47em; height: 0.47em;
width: .47em; width: 0.47em;
border-radius: 50%; border-radius: 50%;
color: $gift-notification-active-color; color: $gift-notification-active-color;
position: absolute; position: absolute;
background-color: $gift-notification-outer-color; background-color: $gift-notification-outer-color;
right: -.3em; right: -0.3em;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
top: -.148em; top: -0.148em;
div { div {
height: .36em; height: 0.36em;
width: .36em; width: 0.36em;
background-color: $gift-notification-inner-color; background-color: $gift-notification-inner-color;
border-radius: 50%; border-radius: 50%;
}
} }
}
} }
</style> </style>

View File

@@ -23,7 +23,7 @@ export default Vue.extend({
}, },
}, },
mounted() { mounted() {
window.history.state ? this.routeHasHistory = true : this.routeHasHistory = false; window.history.state ? (this.routeHasHistory = true) : (this.routeHasHistory = false);
}, },
}); });
</script> </script>

View File

@@ -76,9 +76,7 @@ export default Vue.extend({
}, },
}, },
computed: { computed: {
...mapStores( ...mapStores(useRootStore),
useRootStore,
),
fontStyleData(): object { fontStyleData(): object {
return { return {
'max-width': this.size + 'px', 'max-width': this.size + 'px',

View File

@@ -67,10 +67,7 @@ export default mixins(showMessage).extend({
}; };
}, },
computed: { computed: {
...mapStores( ...mapStores(useNDVStore, useUIStore),
useNDVStore,
useUIStore,
),
node(): INodeUi | null { node(): INodeUi | null {
return this.ndvStore.activeNode; return this.ndvStore.activeNode;
}, },

View File

@@ -1,9 +1,6 @@
<template> <template>
<div class='ph-no-capture' :class="$style.container"> <div class="ph-no-capture" :class="$style.container">
<span <span v-if="readonly" :class="$style.headline">
v-if="readonly"
:class="$style.headline"
>
{{ name }} {{ name }}
</span> </span>
<div <div
@@ -91,7 +88,6 @@ export default mixins(showMessage).extend({
}); });
</script> </script>
<style module lang="scss"> <style module lang="scss">
.container { .container {
display: flex; display: flex;
@@ -146,5 +142,4 @@ export default mixins(showMessage).extend({
margin-left: 4px; margin-left: 4px;
font-weight: 400; font-weight: 400;
} }
</style> </style>

View File

@@ -1,5 +1,5 @@
<template> <template>
<span @keydown.stop class="inline-edit" > <span @keydown.stop class="inline-edit">
<span v-if="isEditEnabled"> <span v-if="isEditEnabled">
<ExpandableInputEdit <ExpandableInputEdit
:placeholder="placeholder" :placeholder="placeholder"
@@ -14,21 +14,19 @@
/> />
</span> </span>
<span @click="onClick" class="preview" v-else> <span @click="onClick" class="preview" v-else>
<ExpandableInputPreview <ExpandableInputPreview :value="previewValue || value" />
:value="previewValue || value"
/>
</span> </span>
</span> </span>
</template> </template>
<script lang="ts"> <script lang="ts">
import Vue from "vue"; import Vue from 'vue';
import ExpandableInputEdit from "@/components/ExpandableInput/ExpandableInputEdit.vue"; import ExpandableInputEdit from '@/components/ExpandableInput/ExpandableInputEdit.vue';
import ExpandableInputPreview from "@/components/ExpandableInput/ExpandableInputPreview.vue"; import ExpandableInputPreview from '@/components/ExpandableInput/ExpandableInputPreview.vue';
export default Vue.extend({ export default Vue.extend({
name: "InlineTextEdit", name: 'InlineTextEdit',
components: { ExpandableInputEdit, ExpandableInputPreview }, components: { ExpandableInputEdit, ExpandableInputPreview },
props: ['isEditEnabled', 'value', 'placeholder', 'maxLength', 'previewValue'], props: ['isEditEnabled', 'value', 'placeholder', 'maxLength', 'previewValue'],
data() { data() {

View File

@@ -21,17 +21,38 @@
@runChange="onRunIndexChange" @runChange="onRunIndexChange"
@tableMounted="$emit('tableMounted', $event)" @tableMounted="$emit('tableMounted', $event)"
data-test-id="ndv-input-panel" data-test-id="ndv-input-panel"
> >
<template #header> <template #header>
<div :class="$style.titleSection"> <div :class="$style.titleSection">
<n8n-select v-if="parentNodes.length" :popper-append-to-body="true" size="small" :value="currentNodeName" @input="onSelect" :no-data-text="$locale.baseText('ndv.input.noNodesFound')" :placeholder="$locale.baseText('ndv.input.parentNodes')" filterable data-test-id="ndv-input-select"> <n8n-select
v-if="parentNodes.length"
:popper-append-to-body="true"
size="small"
:value="currentNodeName"
@input="onSelect"
:no-data-text="$locale.baseText('ndv.input.noNodesFound')"
:placeholder="$locale.baseText('ndv.input.parentNodes')"
filterable
data-test-id="ndv-input-select"
>
<template #prepend> <template #prepend>
<span :class="$style.title">{{ $locale.baseText('ndv.input') }}</span> <span :class="$style.title">{{ $locale.baseText('ndv.input') }}</span>
</template> </template>
<n8n-option v-for="node of parentNodes" :value="node.name" :key="node.name" class="node-option" :label="`${truncate(node.name)} ${getMultipleNodesText(node.name)}`" data-test-id="ndv-input-option"> <n8n-option
v-for="node of parentNodes"
:value="node.name"
:key="node.name"
class="node-option"
:label="`${truncate(node.name)} ${getMultipleNodesText(node.name)}`"
data-test-id="ndv-input-option"
>
{{ truncate(node.name) }}&nbsp; {{ truncate(node.name) }}&nbsp;
<span v-if="getMultipleNodesText(node.name)">{{ getMultipleNodesText(node.name) }}</span> <span v-if="getMultipleNodesText(node.name)">{{
<span v-else>{{ $locale.baseText('ndv.input.nodeDistance', {adjustToNumber: node.depth}) }}</span> getMultipleNodesText(node.name)
}}</span>
<span v-else>{{
$locale.baseText('ndv.input.nodeDistance', { adjustToNumber: node.depth })
}}</span>
</n8n-option> </n8n-option>
</n8n-select> </n8n-select>
<span v-else :class="$style.title">{{ $locale.baseText('ndv.input') }}</span> <span v-else :class="$style.title">{{ $locale.baseText('ndv.input') }}</span>
@@ -40,12 +61,31 @@
<template #node-not-run> <template #node-not-run>
<div :class="$style.noOutputData" v-if="parentNodes.length"> <div :class="$style.noOutputData" v-if="parentNodes.length">
<n8n-text tag="div" :bold="true" color="text-dark" size="large">{{ $locale.baseText('ndv.input.noOutputData.title') }}</n8n-text> <n8n-text tag="div" :bold="true" color="text-dark" size="large">{{
<n8n-tooltip v-if="!readOnly" :manual="true" :value="showDraggableHint && showDraggableHintWithDelay"> $locale.baseText('ndv.input.noOutputData.title')
}}</n8n-text>
<n8n-tooltip
v-if="!readOnly"
:manual="true"
:value="showDraggableHint && showDraggableHintWithDelay"
>
<template #content> <template #content>
<div v-html="$locale.baseText('dataMapping.dragFromPreviousHint', { interpolate: { name: focusedMappableInput } })"></div> <div
v-html="
$locale.baseText('dataMapping.dragFromPreviousHint', {
interpolate: { name: focusedMappableInput },
})
"
></div>
</template> </template>
<NodeExecuteButton type="secondary" :transparent="true" :nodeName="currentNodeName" :label="$locale.baseText('ndv.input.noOutputData.executePrevious')" @execute="onNodeExecute" telemetrySource="inputs" /> <NodeExecuteButton
type="secondary"
:transparent="true"
:nodeName="currentNodeName"
:label="$locale.baseText('ndv.input.noOutputData.executePrevious')"
@execute="onNodeExecute"
telemetrySource="inputs"
/>
</n8n-tooltip> </n8n-tooltip>
<n8n-text v-if="!readOnly" tag="div" size="small"> <n8n-text v-if="!readOnly" tag="div" size="small">
{{ $locale.baseText('ndv.input.noOutputData.hint') }} {{ $locale.baseText('ndv.input.noOutputData.hint') }}
@@ -55,18 +95,26 @@
<div> <div>
<WireMeUp /> <WireMeUp />
</div> </div>
<n8n-text tag="div" :bold="true" color="text-dark" size="large">{{ $locale.baseText('ndv.input.notConnected.title') }}</n8n-text> <n8n-text tag="div" :bold="true" color="text-dark" size="large">{{
$locale.baseText('ndv.input.notConnected.title')
}}</n8n-text>
<n8n-text tag="div"> <n8n-text tag="div">
{{ $locale.baseText('ndv.input.notConnected.message') }} {{ $locale.baseText('ndv.input.notConnected.message') }}
<a href="https://docs.n8n.io/workflows/connections/" target="_blank" @click="onConnectionHelpClick"> <a
{{$locale.baseText('ndv.input.notConnected.learnMore')}} href="https://docs.n8n.io/workflows/connections/"
target="_blank"
@click="onConnectionHelpClick"
>
{{ $locale.baseText('ndv.input.notConnected.learnMore') }}
</a> </a>
</n8n-text> </n8n-text>
</div> </div>
</template> </template>
<template #no-output-data> <template #no-output-data>
<n8n-text tag="div" :bold="true" color="text-dark" size="large">{{ $locale.baseText('ndv.input.noOutputData') }}</n8n-text> <n8n-text tag="div" :bold="true" color="text-dark" size="large">{{
$locale.baseText('ndv.input.noOutputData')
}}</n8n-text>
</template> </template>
</RunData> </RunData>
</template> </template>
@@ -79,15 +127,20 @@ import { workflowHelpers } from '@/mixins/workflowHelpers';
import mixins from 'vue-typed-mixins'; import mixins from 'vue-typed-mixins';
import NodeExecuteButton from './NodeExecuteButton.vue'; import NodeExecuteButton from './NodeExecuteButton.vue';
import WireMeUp from './WireMeUp.vue'; import WireMeUp from './WireMeUp.vue';
import { CRON_NODE_TYPE, INTERVAL_NODE_TYPE, LOCAL_STORAGE_MAPPING_FLAG, MANUAL_TRIGGER_NODE_TYPE, SCHEDULE_TRIGGER_NODE_TYPE, START_NODE_TYPE } from '@/constants'; import {
CRON_NODE_TYPE,
INTERVAL_NODE_TYPE,
LOCAL_STORAGE_MAPPING_FLAG,
MANUAL_TRIGGER_NODE_TYPE,
SCHEDULE_TRIGGER_NODE_TYPE,
START_NODE_TYPE,
} from '@/constants';
import { mapStores } from 'pinia'; import { mapStores } from 'pinia';
import { useWorkflowsStore } from '@/stores/workflows'; import { useWorkflowsStore } from '@/stores/workflows';
import { useNDVStore } from '@/stores/ndv'; import { useNDVStore } from '@/stores/ndv';
import { useNodeTypesStore } from '@/stores/nodeTypes'; import { useNodeTypesStore } from '@/stores/nodeTypes';
export default mixins( export default mixins(workflowHelpers).extend({
workflowHelpers,
).extend({
name: 'InputPanel', name: 'InputPanel',
components: { RunData, NodeExecuteButton, WireMeUp }, components: { RunData, NodeExecuteButton, WireMeUp },
props: { props: {
@@ -100,8 +153,7 @@ export default mixins(
linkedRuns: { linkedRuns: {
type: Boolean, type: Boolean,
}, },
workflow: { workflow: {},
},
canLinkRuns: { canLinkRuns: {
type: Boolean, type: Boolean,
}, },
@@ -123,11 +175,7 @@ export default mixins(
}; };
}, },
computed: { computed: {
...mapStores( ...mapStores(useNodeTypesStore, useNDVStore, useWorkflowsStore),
useNodeTypesStore,
useNDVStore,
useWorkflowsStore,
),
focusedMappableInput(): string { focusedMappableInput(): string {
return this.ndvStore.focusedMappableInput; return this.ndvStore.focusedMappableInput;
}, },
@@ -135,7 +183,12 @@ export default mixins(
return window.localStorage.getItem(LOCAL_STORAGE_MAPPING_FLAG) === 'true'; return window.localStorage.getItem(LOCAL_STORAGE_MAPPING_FLAG) === 'true';
}, },
showDraggableHint(): boolean { showDraggableHint(): boolean {
const toIgnore = [START_NODE_TYPE, MANUAL_TRIGGER_NODE_TYPE, CRON_NODE_TYPE, INTERVAL_NODE_TYPE]; const toIgnore = [
START_NODE_TYPE,
MANUAL_TRIGGER_NODE_TYPE,
CRON_NODE_TYPE,
INTERVAL_NODE_TYPE,
];
if (!this.currentNode || toIgnore.includes(this.currentNode.type)) { if (!this.currentNode || toIgnore.includes(this.currentNode.type)) {
return false; return false;
} }
@@ -148,69 +201,86 @@ export default mixins(
} }
const triggeredNode = this.workflowsStore.executedNode; const triggeredNode = this.workflowsStore.executedNode;
const executingNode = this.workflowsStore.executingNode; const executingNode = this.workflowsStore.executingNode;
if (this.activeNode && triggeredNode === this.activeNode.name && this.activeNode.name !== executingNode) { if (
this.activeNode &&
triggeredNode === this.activeNode.name &&
this.activeNode.name !== executingNode
) {
return true; return true;
} }
if (executingNode || triggeredNode) { if (executingNode || triggeredNode) {
return !!this.parentNodes.find((node) => node.name === executingNode || node.name === triggeredNode); return !!this.parentNodes.find(
(node) => node.name === executingNode || node.name === triggeredNode,
);
} }
return false; return false;
}, },
workflowRunning (): boolean { workflowRunning(): boolean {
return this.uiStore.isActionActive('workflowRunning'); return this.uiStore.isActionActive('workflowRunning');
}, },
currentWorkflow(): Workflow { currentWorkflow(): Workflow {
return this.workflow as Workflow; return this.workflow as Workflow;
}, },
activeNode (): INodeUi | null { activeNode(): INodeUi | null {
return this.ndvStore.activeNode; return this.ndvStore.activeNode;
}, },
currentNode (): INodeUi | null { currentNode(): INodeUi | null {
return this.workflowsStore.getNodeByName(this.currentNodeName); return this.workflowsStore.getNodeByName(this.currentNodeName);
}, },
connectedCurrentNodeOutputs(): number[] | undefined { connectedCurrentNodeOutputs(): number[] | undefined {
const search = this.parentNodes.find(({name}) => name === this.currentNodeName); const search = this.parentNodes.find(({ name }) => name === this.currentNodeName);
if (search) { if (search) {
return search.indicies; return search.indicies;
} }
return undefined; return undefined;
}, },
parentNodes (): IConnectedNode[] { parentNodes(): IConnectedNode[] {
if (!this.activeNode) { if (!this.activeNode) {
return []; return [];
} }
const nodes: IConnectedNode[] = (this.workflow as Workflow).getParentNodesByDepth(this.activeNode.name); const nodes: IConnectedNode[] = (this.workflow as Workflow).getParentNodesByDepth(
this.activeNode.name,
);
return nodes.filter(({name}, i) => (this.activeNode && (name !== this.activeNode.name)) && nodes.findIndex((node) => node.name === name) === i); return nodes.filter(
({ name }, i) =>
this.activeNode &&
name !== this.activeNode.name &&
nodes.findIndex((node) => node.name === name) === i,
);
}, },
currentNodeDepth (): number { currentNodeDepth(): number {
const node = this.parentNodes.find((node) => this.currentNode && node.name === this.currentNode.name); const node = this.parentNodes.find(
return node ? node.depth: -1; (node) => this.currentNode && node.name === this.currentNode.name,
);
return node ? node.depth : -1;
}, },
activeNodeType () : INodeTypeDescription | null { activeNodeType(): INodeTypeDescription | null {
if (!this.activeNode) return null; if (!this.activeNode) return null;
return this.nodeTypesStore.getNodeType(this.activeNode.type, this.activeNode.typeVersion); return this.nodeTypesStore.getNodeType(this.activeNode.type, this.activeNode.typeVersion);
}, },
isMultiInputNode (): boolean { isMultiInputNode(): boolean {
return this.activeNodeType !== null && this.activeNodeType.inputs.length > 1; return this.activeNodeType !== null && this.activeNodeType.inputs.length > 1;
}, },
}, },
methods: { methods: {
getMultipleNodesText(nodeName?: string):string { getMultipleNodesText(nodeName?: string): string {
if( if (
!nodeName || !nodeName ||
!this.isMultiInputNode || !this.isMultiInputNode ||
!this.activeNode || !this.activeNode ||
this.activeNodeType === null || this.activeNodeType === null ||
this.activeNodeType.inputNames === undefined this.activeNodeType.inputNames === undefined
) return ''; )
return '';
const activeNodeConnections = this.currentWorkflow.connectionsByDestinationNode[this.activeNode.name].main || []; const activeNodeConnections =
this.currentWorkflow.connectionsByDestinationNode[this.activeNode.name].main || [];
// Collect indexes of connected nodes // Collect indexes of connected nodes
const connectedInputIndexes = activeNodeConnections.reduce((acc: number[], node, index) => { const connectedInputIndexes = activeNodeConnections.reduce((acc: number[], node, index) => {
if(node[0] && node[0].node === nodeName) return [...acc, index]; if (node[0] && node[0].node === nodeName) return [...acc, index];
return acc; return acc;
}, []); }, []);
@@ -222,7 +292,7 @@ export default mixins(
this.activeNodeType.inputNames[inputIndex], this.activeNodeType.inputNames[inputIndex],
); );
if(connectedInputs.length === 0) return ''; if (connectedInputs.length === 0) return '';
return `(${connectedInputs.join(' & ')})`; return `(${connectedInputs.join(' & ')})`;
}, },
@@ -281,11 +351,12 @@ export default mixins(
if (this.showDraggableHintWithDelay) { if (this.showDraggableHintWithDelay) {
this.draggableHintShown = true; this.draggableHintShown = true;
this.$telemetry.track('User viewed data mapping tooltip', { type: 'unexecuted input pane' }); this.$telemetry.track('User viewed data mapping tooltip', {
type: 'unexecuted input pane',
});
} }
}, 1000); }, 1000);
} } else if (!curr) {
else if (!curr) {
this.showDraggableHintWithDelay = false; this.showDraggableHintWithDelay = false;
} }
}, },

View File

@@ -5,7 +5,6 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import mixins from 'vue-typed-mixins'; import mixins from 'vue-typed-mixins';
import emitter from '@/mixins/emitter'; import emitter from '@/mixins/emitter';

View File

@@ -1,12 +1,10 @@
<template> <template>
<div ref="root"> <div ref="root">
<slot></slot> <slot></slot>
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts">
import Vue from 'vue'; import Vue from 'vue';
export default Vue.extend({ export default Vue.extend({
@@ -29,7 +27,7 @@ export default Vue.extend({
}; };
const observer = new IntersectionObserver((entries) => { const observer = new IntersectionObserver((entries) => {
entries.forEach(({target, isIntersecting}) => { entries.forEach(({ target, isIntersecting }) => {
this.$emit('observed', { this.$emit('observed', {
el: target, el: target,
isIntersecting, isIntersecting,

View File

@@ -17,23 +17,28 @@
/> />
</template> </template>
<template #footer> <template #footer>
<n8n-button :loading="loading" :disabled="!enabledButton" :label="buttonLabel" @click="onSubmitClick" float="right" /> <n8n-button
:loading="loading"
:disabled="!enabledButton"
:label="buttonLabel"
@click="onSubmitClick"
float="right"
/>
</template> </template>
</Modal> </Modal>
</template> </template>
<script lang="ts"> <script lang="ts">
import mixins from "vue-typed-mixins"; import mixins from 'vue-typed-mixins';
import { showMessage } from "@/mixins/showMessage"; import { showMessage } from '@/mixins/showMessage';
import Modal from "./Modal.vue"; import Modal from './Modal.vue';
import Vue from "vue"; import Vue from 'vue';
import { IFormInputs, IInviteResponse } from "@/Interface"; import { IFormInputs, IInviteResponse } from '@/Interface';
import { VALID_EMAIL_REGEX, INVITE_USER_MODAL_KEY } from "@/constants"; import { VALID_EMAIL_REGEX, INVITE_USER_MODAL_KEY } from '@/constants';
import { ROLE } from '@/utils'; import { ROLE } from '@/utils';
import { mapStores } from "pinia"; import { mapStores } from 'pinia';
import { useUsersStore } from "@/stores/users"; import { useUsersStore } from '@/stores/users';
const NAME_EMAIL_FORMAT_REGEX = /^.* <(.*)>$/; const NAME_EMAIL_FORMAT_REGEX = /^.* <(.*)>$/;
@@ -50,7 +55,7 @@ function getEmail(email: string): string {
export default mixins(showMessage).extend({ export default mixins(showMessage).extend({
components: { Modal }, components: { Modal },
name: "InviteUsersModal", name: 'InviteUsersModal',
props: { props: {
modalName: { modalName: {
type: String, type: String,
@@ -73,7 +78,7 @@ export default mixins(showMessage).extend({
properties: { properties: {
label: this.$locale.baseText('settings.users.newEmailsToInvite'), label: this.$locale.baseText('settings.users.newEmailsToInvite'),
required: true, required: true,
validationRules: [{name: 'VALID_EMAILS'}], validationRules: [{ name: 'VALID_EMAILS' }],
validators: { validators: {
VALID_EMAILS: { VALID_EMAILS: {
validate: this.validateEmails, validate: this.validateEmails,
@@ -109,10 +114,9 @@ export default mixins(showMessage).extend({
}, },
buttonLabel(): string { buttonLabel(): string {
if (this.emailsCount > 1) { if (this.emailsCount > 1) {
return this.$locale.baseText( return this.$locale.baseText('settings.users.inviteXUser', {
'settings.users.inviteXUser', interpolate: { count: this.emailsCount.toString() },
{ interpolate: { count: this.emailsCount.toString() }}, });
);
} }
return this.$locale.baseText('settings.users.inviteUser'); return this.$locale.baseText('settings.users.inviteUser');
@@ -135,14 +139,14 @@ export default mixins(showMessage).extend({
if (!!parsed.trim() && !VALID_EMAIL_REGEX.test(String(parsed).trim().toLowerCase())) { if (!!parsed.trim() && !VALID_EMAIL_REGEX.test(String(parsed).trim().toLowerCase())) {
return { return {
messageKey: 'settings.users.invalidEmailError', messageKey: 'settings.users.invalidEmailError',
options: { interpolate: { email: parsed }}, options: { interpolate: { email: parsed } },
}; };
} }
} }
return false; return false;
}, },
onInput(e: {name: string, value: string}) { onInput(e: { name: string; value: string }) {
if (e.name === 'emails') { if (e.name === 'emails') {
this.emails = e.value; this.emails = e.value;
} }
@@ -151,8 +155,9 @@ export default mixins(showMessage).extend({
try { try {
this.loading = true; this.loading = true;
const emails = this.emails.split(',') const emails = this.emails
.map((email) => ({email: getEmail(email)})) .split(',')
.map((email) => ({ email: getEmail(email) }))
.filter((invite) => !!invite.email); .filter((invite) => !!invite.email);
if (emails.length === 0) { if (emails.length === 0) {
@@ -160,24 +165,32 @@ export default mixins(showMessage).extend({
} }
const invited: IInviteResponse[] = await this.usersStore.inviteUsers(emails); const invited: IInviteResponse[] = await this.usersStore.inviteUsers(emails);
const invitedEmails = invited.reduce((accu, {user, error}) => { const invitedEmails = invited.reduce(
if (error) { (accu, { user, error }) => {
accu.error.push(user.email); if (error) {
} accu.error.push(user.email);
else { } else {
accu.success.push(user.email); accu.success.push(user.email);
} }
return accu; return accu;
}, { },
success: [] as string[], {
error: [] as string[], success: [] as string[],
}); error: [] as string[],
},
);
if (invitedEmails.success.length) { if (invitedEmails.success.length) {
this.$showMessage({ this.$showMessage({
type: 'success', type: 'success',
title: this.$locale.baseText(invitedEmails.success.length > 1 ? 'settings.users.usersInvited' : 'settings.users.userInvited'), title: this.$locale.baseText(
message: this.$locale.baseText('settings.users.emailInvitesSent', { interpolate: { emails: invitedEmails.success.join(', ') }}), invitedEmails.success.length > 1
? 'settings.users.usersInvited'
: 'settings.users.userInvited',
),
message: this.$locale.baseText('settings.users.emailInvitesSent', {
interpolate: { emails: invitedEmails.success.join(', ') },
}),
}); });
} }
@@ -186,13 +199,14 @@ export default mixins(showMessage).extend({
this.$showMessage({ this.$showMessage({
type: 'error', type: 'error',
title: this.$locale.baseText('settings.users.usersEmailedError'), title: this.$locale.baseText('settings.users.usersEmailedError'),
message: this.$locale.baseText('settings.users.emailInvitesSentError', { interpolate: { emails: invitedEmails.error.join(', ') }}), message: this.$locale.baseText('settings.users.emailInvitesSentError', {
interpolate: { emails: invitedEmails.error.join(', ') },
}),
}); });
}, 0); // notifications stack on top of each other otherwise }, 0); // notifications stack on top of each other otherwise
} }
this.modalBus.$emit('close'); this.modalBus.$emit('close');
} catch (error) { } catch (error) {
this.$showError(error, this.$locale.baseText('settings.users.usersInvitedError')); this.$showError(error, this.$locale.baseText('settings.users.usersInvitedError'));
} }
@@ -203,5 +217,4 @@ export default mixins(showMessage).extend({
}, },
}, },
}); });
</script> </script>

View File

@@ -1,9 +1,5 @@
<template> <template>
<img <img :src="basePath + 'n8n-logo-expanded.svg'" :class="$style.img" alt="n8n.io" />
:src="basePath + 'n8n-logo-expanded.svg'"
:class="$style.img"
alt="n8n.io"
/>
</template> </template>
<script lang="ts"> <script lang="ts">
@@ -13,9 +9,7 @@ import Vue from 'vue';
export default Vue.extend({ export default Vue.extend({
computed: { computed: {
...mapStores( ...mapStores(useRootStore),
useRootStore,
),
basePath(): string { basePath(): string {
return this.rootStore.baseUrl; return this.rootStore.baseUrl;
}, },

View File

@@ -25,12 +25,13 @@
/> />
</span> </span>
{{ $locale.baseText('executionDetails.of') }} {{ $locale.baseText('executionDetails.of') }}
<span class="primary-color clickable" :title="$locale.baseText('executionDetails.openWorkflow')"> <span
class="primary-color clickable"
:title="$locale.baseText('executionDetails.openWorkflow')"
>
<ShortenName :name="workflowName"> <ShortenName :name="workflowName">
<template #default="{ shortenedName }"> <template #default="{ shortenedName }">
<span @click="openWorkflow(workflowExecution.workflowId)"> <span @click="openWorkflow(workflowExecution.workflowId)"> "{{ shortenedName }}" </span>
"{{ shortenedName }}"
</span>
</template> </template>
</ShortenName> </ShortenName>
</span> </span>
@@ -41,27 +42,25 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import mixins from "vue-typed-mixins"; import mixins from 'vue-typed-mixins';
import { IExecutionResponse, IExecutionsSummary } from "../../../Interface"; import { IExecutionResponse, IExecutionsSummary } from '../../../Interface';
import { titleChange } from "@/mixins/titleChange"; import { titleChange } from '@/mixins/titleChange';
import ShortenName from "@/components/ShortenName.vue"; import ShortenName from '@/components/ShortenName.vue';
import ReadOnly from "@/components/MainHeader/ExecutionDetails/ReadOnly.vue"; import ReadOnly from '@/components/MainHeader/ExecutionDetails/ReadOnly.vue';
import { mapStores } from "pinia"; import { mapStores } from 'pinia';
import { useWorkflowsStore } from "@/stores/workflows"; import { useWorkflowsStore } from '@/stores/workflows';
export default mixins(titleChange).extend({ export default mixins(titleChange).extend({
name: "ExecutionDetails", name: 'ExecutionDetails',
components: { components: {
ShortenName, ShortenName,
ReadOnly, ReadOnly,
}, },
computed: { computed: {
...mapStores( ...mapStores(useWorkflowsStore),
useWorkflowsStore,
),
executionId(): string | undefined { executionId(): string | undefined {
return this.$route.params.id; return this.$route.params.id;
}, },
@@ -84,10 +83,10 @@ export default mixins(titleChange).extend({
}, },
methods: { methods: {
async openWorkflow(workflowId: string) { async openWorkflow(workflowId: string) {
this.$titleSet(this.workflowName, "IDLE"); this.$titleSet(this.workflowName, 'IDLE');
// Change to other workflow // Change to other workflow
this.$router.push({ this.$router.push({
name: "NodeViewExisting", name: 'NodeViewExisting',
params: { name: workflowId }, params: { name: workflowId },
}); });
}, },
@@ -101,12 +100,12 @@ export default mixins(titleChange).extend({
} }
.execution-icon { .execution-icon {
&.success { &.success {
color: var(--color-success); color: var(--color-success);
} }
&.warning { &.warning {
color: var(--color-warning); color: var(--color-warning);
} }
} }
.container { .container {

View File

@@ -1,5 +1,5 @@
<template> <template>
<n8n-tooltip class="primary-color" placement="bottom-end" > <n8n-tooltip class="primary-color" placement="bottom-end">
<template #content> <template #content>
<div> <div>
<span v-html="$locale.baseText('executionDetails.readOnly.youreViewingTheLogOf')"></span> <span v-html="$locale.baseText('executionDetails.readOnly.youreViewingTheLogOf')"></span>
@@ -16,7 +16,7 @@
import Vue from 'vue'; import Vue from 'vue';
export default Vue.extend({ export default Vue.extend({
name: "ReadOnly", name: 'ReadOnly',
}); });
</script> </script>

View File

@@ -1,10 +1,15 @@
<template> <template>
<div> <div>
<div :class="{'main-header': true, expanded: !this.uiStore.sidebarMenuCollapsed}"> <div :class="{ 'main-header': true, expanded: !this.uiStore.sidebarMenuCollapsed }">
<div v-show="!hideMenuBar" class="top-menu"> <div v-show="!hideMenuBar" class="top-menu">
<ExecutionDetails v-if="isExecutionPage" /> <ExecutionDetails v-if="isExecutionPage" />
<WorkflowDetails v-else /> <WorkflowDetails v-else />
<tab-bar v-if="onWorkflowPage && !isExecutionPage" :items="tabBarItems" :activeTab="activeHeaderTab" @select="onTabSelected"/> <tab-bar
v-if="onWorkflowPage && !isExecutionPage"
:items="tabBarItems"
:activeTab="activeHeaderTab"
@select="onTabSelected"
/>
</div> </div>
</div> </div>
</div> </div>
@@ -16,7 +21,12 @@ import { pushConnection } from '@/mixins/pushConnection';
import WorkflowDetails from '@/components/MainHeader/WorkflowDetails.vue'; import WorkflowDetails from '@/components/MainHeader/WorkflowDetails.vue';
import ExecutionDetails from '@/components/MainHeader/ExecutionDetails/ExecutionDetails.vue'; import ExecutionDetails from '@/components/MainHeader/ExecutionDetails/ExecutionDetails.vue';
import TabBar from '@/components/MainHeader/TabBar.vue'; import TabBar from '@/components/MainHeader/TabBar.vue';
import { MAIN_HEADER_TABS, PLACEHOLDER_EMPTY_WORKFLOW_ID, STICKY_NODE_TYPE, VIEWS } from '@/constants'; import {
MAIN_HEADER_TABS,
PLACEHOLDER_EMPTY_WORKFLOW_ID,
STICKY_NODE_TYPE,
VIEWS,
} from '@/constants';
import { IExecutionsSummary, INodeUi, ITabBarItem } from '@/Interface'; import { IExecutionsSummary, INodeUi, ITabBarItem } from '@/Interface';
import { workflowHelpers } from '@/mixins/workflowHelpers'; import { workflowHelpers } from '@/mixins/workflowHelpers';
import { Route } from 'vue-router'; import { Route } from 'vue-router';
@@ -24,121 +34,125 @@ import { mapStores } from 'pinia';
import { useUIStore } from '@/stores/ui'; import { useUIStore } from '@/stores/ui';
import { useNDVStore } from '@/stores/ndv'; import { useNDVStore } from '@/stores/ndv';
export default mixins( export default mixins(pushConnection, workflowHelpers).extend({
pushConnection, name: 'MainHeader',
workflowHelpers, components: {
).extend({ WorkflowDetails,
name: 'MainHeader', ExecutionDetails,
components: { TabBar,
WorkflowDetails, },
ExecutionDetails, data() {
TabBar, return {
activeHeaderTab: MAIN_HEADER_TABS.WORKFLOW,
workflowToReturnTo: '',
dirtyState: false,
};
},
computed: {
...mapStores(useNDVStore, useUIStore),
tabBarItems(): ITabBarItem[] {
return [
{ value: MAIN_HEADER_TABS.WORKFLOW, label: this.$locale.baseText('generic.workflow') },
{ value: MAIN_HEADER_TABS.EXECUTIONS, label: this.$locale.baseText('generic.executions') },
];
}, },
data() { isExecutionPage(): boolean {
return { return this.$route.name === VIEWS.EXECUTION;
activeHeaderTab: MAIN_HEADER_TABS.WORKFLOW,
workflowToReturnTo: '',
dirtyState: false,
};
}, },
computed: { activeNode(): INodeUi | null {
...mapStores( return this.ndvStore.activeNode;
useNDVStore,
useUIStore,
),
tabBarItems(): ITabBarItem[] {
return [
{ value: MAIN_HEADER_TABS.WORKFLOW, label: this.$locale.baseText('generic.workflow') },
{ value: MAIN_HEADER_TABS.EXECUTIONS, label: this.$locale.baseText('generic.executions') },
];
},
isExecutionPage (): boolean {
return this.$route.name === VIEWS.EXECUTION;
},
activeNode (): INodeUi | null {
return this.ndvStore.activeNode;
},
hideMenuBar(): boolean {
return Boolean(this.activeNode && this.activeNode.type !== STICKY_NODE_TYPE);
},
workflowName (): string {
return this.workflowsStore.workflowName;
},
currentWorkflow (): string {
return this.$route.params.name || this.workflowsStore.workflowId;
},
onWorkflowPage(): boolean {
return this.$route.meta && (this.$route.meta.nodeView || this.$route.meta.keepWorkflowAlive === true);
},
activeExecution(): IExecutionsSummary {
return this.workflowsStore.activeWorkflowExecution as IExecutionsSummary;
},
}, },
mounted() { hideMenuBar(): boolean {
this.dirtyState = this.uiStore.stateIsDirty; return Boolean(this.activeNode && this.activeNode.type !== STICKY_NODE_TYPE);
this.syncTabsWithRoute(this.$route);
// Initialize the push connection
this.pushConnect();
}, },
beforeDestroy() { workflowName(): string {
this.pushDisconnect(); return this.workflowsStore.workflowName;
}, },
watch: { currentWorkflow(): string {
$route (to, from){ return this.$route.params.name || this.workflowsStore.workflowId;
this.syncTabsWithRoute(to);
},
}, },
methods: { onWorkflowPage(): boolean {
syncTabsWithRoute(route: Route): void { return (
if (route.name === VIEWS.EXECUTION_HOME || route.name === VIEWS.EXECUTIONS || route.name === VIEWS.EXECUTION_PREVIEW) { this.$route.meta &&
this.activeHeaderTab = MAIN_HEADER_TABS.EXECUTIONS; (this.$route.meta.nodeView || this.$route.meta.keepWorkflowAlive === true)
} else if (route.name === VIEWS.WORKFLOW || route.name === VIEWS.NEW_WORKFLOW) { );
this.activeHeaderTab = MAIN_HEADER_TABS.WORKFLOW; },
} activeExecution(): IExecutionsSummary {
const workflowName = route.params.name; return this.workflowsStore.activeWorkflowExecution as IExecutionsSummary;
if (workflowName !== 'new') { },
this.workflowToReturnTo = workflowName; },
} mounted() {
}, this.dirtyState = this.uiStore.stateIsDirty;
onTabSelected(tab: string, event: MouseEvent) { this.syncTabsWithRoute(this.$route);
switch (tab) { // Initialize the push connection
case MAIN_HEADER_TABS.WORKFLOW: this.pushConnect();
if (!['', 'new', PLACEHOLDER_EMPTY_WORKFLOW_ID].includes(this.workflowToReturnTo)) { },
if (this.$route.name !== VIEWS.WORKFLOW) { beforeDestroy() {
this.$router.push({ this.pushDisconnect();
},
watch: {
$route(to, from) {
this.syncTabsWithRoute(to);
},
},
methods: {
syncTabsWithRoute(route: Route): void {
if (
route.name === VIEWS.EXECUTION_HOME ||
route.name === VIEWS.EXECUTIONS ||
route.name === VIEWS.EXECUTION_PREVIEW
) {
this.activeHeaderTab = MAIN_HEADER_TABS.EXECUTIONS;
} else if (route.name === VIEWS.WORKFLOW || route.name === VIEWS.NEW_WORKFLOW) {
this.activeHeaderTab = MAIN_HEADER_TABS.WORKFLOW;
}
const workflowName = route.params.name;
if (workflowName !== 'new') {
this.workflowToReturnTo = workflowName;
}
},
onTabSelected(tab: string, event: MouseEvent) {
switch (tab) {
case MAIN_HEADER_TABS.WORKFLOW:
if (!['', 'new', PLACEHOLDER_EMPTY_WORKFLOW_ID].includes(this.workflowToReturnTo)) {
if (this.$route.name !== VIEWS.WORKFLOW) {
this.$router.push({
name: VIEWS.WORKFLOW, name: VIEWS.WORKFLOW,
params: { name: this.workflowToReturnTo }, params: { name: this.workflowToReturnTo },
}); });
}
} else {
if (this.$route.name !== VIEWS.NEW_WORKFLOW) {
this.$router.push({ name: VIEWS.NEW_WORKFLOW });
this.uiStore.stateIsDirty = this.dirtyState;
}
} }
this.activeHeaderTab = MAIN_HEADER_TABS.WORKFLOW; } else {
break; if (this.$route.name !== VIEWS.NEW_WORKFLOW) {
case MAIN_HEADER_TABS.EXECUTIONS: this.$router.push({ name: VIEWS.NEW_WORKFLOW });
this.dirtyState = this.uiStore.stateIsDirty; this.uiStore.stateIsDirty = this.dirtyState;
this.workflowToReturnTo = this.currentWorkflow; }
const routeWorkflowId = this.currentWorkflow === PLACEHOLDER_EMPTY_WORKFLOW_ID ? 'new' : this.currentWorkflow; }
if (this.activeExecution) { this.activeHeaderTab = MAIN_HEADER_TABS.WORKFLOW;
this.$router.push({ break;
case MAIN_HEADER_TABS.EXECUTIONS:
this.dirtyState = this.uiStore.stateIsDirty;
this.workflowToReturnTo = this.currentWorkflow;
const routeWorkflowId =
this.currentWorkflow === PLACEHOLDER_EMPTY_WORKFLOW_ID ? 'new' : this.currentWorkflow;
if (this.activeExecution) {
this.$router
.push({
name: VIEWS.EXECUTION_PREVIEW, name: VIEWS.EXECUTION_PREVIEW,
params: { name: routeWorkflowId, executionId: this.activeExecution.id }, params: { name: routeWorkflowId, executionId: this.activeExecution.id },
}).catch(()=>{});; })
} else { .catch(() => {});
this.$router.push({ name: VIEWS.EXECUTION_HOME, params: { name: routeWorkflowId } }); } else {
} this.$router.push({ name: VIEWS.EXECUTION_HOME, params: { name: routeWorkflowId } });
// this.modalBus.$emit('closeAll'); }
this.activeHeaderTab = MAIN_HEADER_TABS.EXECUTIONS; // this.modalBus.$emit('closeAll');
break; this.activeHeaderTab = MAIN_HEADER_TABS.EXECUTIONS;
default: break;
break; default:
} break;
}, }
}, },
}); },
});
</script> </script>
<style lang="scss"> <style lang="scss">

View File

@@ -1,10 +1,13 @@
<template> <template>
<div v-if="items" :class="{[$style.container]: true, ['tab-bar-container']: true, [$style.menuCollapsed]: mainSidebarCollapsed}"> <div
<n8n-radio-buttons v-if="items"
:value="activeTab" :class="{
:options="items" [$style.container]: true,
@input="onSelect" ['tab-bar-container']: true,
/> [$style.menuCollapsed]: mainSidebarCollapsed,
}"
>
<n8n-radio-buttons :value="activeTab" :options="items" @input="onSelect" />
</div> </div>
</template> </template>
@@ -33,9 +36,7 @@ export default Vue.extend({
}, },
}, },
computed: { computed: {
...mapStores( ...mapStores(useUIStore),
useUIStore,
),
mainSidebarCollapsed(): boolean { mainSidebarCollapsed(): boolean {
return this.uiStore.sidebarMenuCollapsed; return this.uiStore.sidebarMenuCollapsed;
}, },
@@ -49,7 +50,6 @@ export default Vue.extend({
</script> </script>
<style module lang="scss"> <style module lang="scss">
.container { .container {
position: absolute; position: absolute;
top: 47px; top: 47px;

View File

@@ -39,14 +39,8 @@
data-test-id="workflow-tags-dropdown" data-test-id="workflow-tags-dropdown"
/> />
</div> </div>
<div <div v-else-if="currentWorkflowTagIds.length === 0">
v-else-if="currentWorkflowTagIds.length === 0" <span class="add-tag clickable" data-test-id="new-tag-link" @click="onTagsEditEnable">
>
<span
class="add-tag clickable"
data-test-id="new-tag-link"
@click="onTagsEditEnable"
>
+ {{ $locale.baseText('workflowDetails.addTag') }} + {{ $locale.baseText('workflowDetails.addTag') }}
</span> </span>
</div> </div>
@@ -68,26 +62,27 @@
<WorkflowActivator :workflow-active="isWorkflowActive" :workflow-id="currentWorkflowId" /> <WorkflowActivator :workflow-active="isWorkflowActive" :workflow-id="currentWorkflowId" />
</span> </span>
<enterprise-edition :features="[EnterpriseEditionFeature.WorkflowSharing]"> <enterprise-edition :features="[EnterpriseEditionFeature.WorkflowSharing]">
<n8n-button <n8n-button type="secondary" class="mr-2xs" @click="onShareButtonClick">
type="secondary"
class="mr-2xs"
@click="onShareButtonClick"
>
{{ $locale.baseText('workflowDetails.share') }} {{ $locale.baseText('workflowDetails.share') }}
</n8n-button> </n8n-button>
<template #fallback> <template #fallback>
<n8n-tooltip> <n8n-tooltip>
<n8n-button <n8n-button type="secondary" :class="['mr-2xs', $style.disabledShareButton]">
type="secondary"
:class="['mr-2xs', $style.disabledShareButton]"
>
{{ $locale.baseText('workflowDetails.share') }} {{ $locale.baseText('workflowDetails.share') }}
</n8n-button> </n8n-button>
<template #content> <template #content>
<i18n :path="dynamicTranslations.workflows.sharing.unavailable.description" tag="span"> <i18n
:path="dynamicTranslations.workflows.sharing.unavailable.description"
tag="span"
>
<template #action> <template #action>
<a :href="dynamicTranslations.workflows.sharing.unavailable.linkURL" target="_blank"> <a
{{ $locale.baseText(dynamicTranslations.workflows.sharing.unavailable.action) }} :href="dynamicTranslations.workflows.sharing.unavailable.linkURL"
target="_blank"
>
{{
$locale.baseText(dynamicTranslations.workflows.sharing.unavailable.action)
}}
</a> </a>
</template> </template>
</i18n> </i18n>
@@ -103,8 +98,18 @@
@click="onSaveButtonClick" @click="onSaveButtonClick"
/> />
<div :class="$style.workflowMenuContainer"> <div :class="$style.workflowMenuContainer">
<input :class="$style.hiddenInput" type="file" ref="importFile" data-test-id="workflow-import-input" @change="handleFileImport()"> <input
<n8n-action-dropdown :items="workflowMenuItems" data-test-id="workflow-menu" @select="onWorkflowMenuSelect" /> :class="$style.hiddenInput"
type="file"
ref="importFile"
data-test-id="workflow-import-input"
@change="handleFileImport()"
/>
<n8n-action-dropdown
:items="workflowMenuItems"
data-test-id="workflow-menu"
@select="onWorkflowMenuSelect"
/>
</div> </div>
</template> </template>
</PushConnectionTracker> </PushConnectionTracker>
@@ -112,40 +117,41 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import Vue from "vue"; import Vue from 'vue';
import mixins from "vue-typed-mixins"; import mixins from 'vue-typed-mixins';
import { import {
DUPLICATE_MODAL_KEY, DUPLICATE_MODAL_KEY,
EnterpriseEditionFeature, EnterpriseEditionFeature,
MAX_WORKFLOW_NAME_LENGTH, MAX_WORKFLOW_NAME_LENGTH,
PLACEHOLDER_EMPTY_WORKFLOW_ID, PLACEHOLDER_EMPTY_WORKFLOW_ID,
VIEWS, WORKFLOW_MENU_ACTIONS, VIEWS,
WORKFLOW_MENU_ACTIONS,
WORKFLOW_SETTINGS_MODAL_KEY, WORKFLOW_SETTINGS_MODAL_KEY,
WORKFLOW_SHARE_MODAL_KEY, WORKFLOW_SHARE_MODAL_KEY,
} from "@/constants"; } from '@/constants';
import ShortenName from "@/components/ShortenName.vue"; import ShortenName from '@/components/ShortenName.vue';
import TagsContainer from "@/components/TagsContainer.vue"; import TagsContainer from '@/components/TagsContainer.vue';
import PushConnectionTracker from "@/components/PushConnectionTracker.vue"; import PushConnectionTracker from '@/components/PushConnectionTracker.vue';
import WorkflowActivator from "@/components/WorkflowActivator.vue"; import WorkflowActivator from '@/components/WorkflowActivator.vue';
import { workflowHelpers } from "@/mixins/workflowHelpers"; import { workflowHelpers } from '@/mixins/workflowHelpers';
import SaveButton from "@/components/SaveButton.vue"; import SaveButton from '@/components/SaveButton.vue';
import TagsDropdown from "@/components/TagsDropdown.vue"; import TagsDropdown from '@/components/TagsDropdown.vue';
import InlineTextEdit from "@/components/InlineTextEdit.vue"; import InlineTextEdit from '@/components/InlineTextEdit.vue';
import BreakpointsObserver from "@/components/BreakpointsObserver.vue"; import BreakpointsObserver from '@/components/BreakpointsObserver.vue';
import {IWorkflowDataUpdate, IWorkflowDb, IWorkflowToShare, NestedRecord} from "@/Interface"; import { IWorkflowDataUpdate, IWorkflowDb, IWorkflowToShare, NestedRecord } from '@/Interface';
import { saveAs } from 'file-saver'; import { saveAs } from 'file-saver';
import { titleChange } from "@/mixins/titleChange"; import { titleChange } from '@/mixins/titleChange';
import type { MessageBoxInputData } from 'element-ui/types/message-box'; import type { MessageBoxInputData } from 'element-ui/types/message-box';
import { mapStores } from "pinia"; import { mapStores } from 'pinia';
import { useUIStore } from "@/stores/ui"; import { useUIStore } from '@/stores/ui';
import { useSettingsStore } from "@/stores/settings"; import { useSettingsStore } from '@/stores/settings';
import { useWorkflowsStore } from "@/stores/workflows"; import { useWorkflowsStore } from '@/stores/workflows';
import { useRootStore } from "@/stores/n8nRootStore"; import { useRootStore } from '@/stores/n8nRootStore';
import { useTagsStore } from "@/stores/tags"; import { useTagsStore } from '@/stores/tags';
import {getWorkflowPermissions, IPermissions} from "@/permissions"; import { getWorkflowPermissions, IPermissions } from '@/permissions';
import {useUsersStore} from "@/stores/users"; import { useUsersStore } from '@/stores/users';
const hasChanged = (prev: string[], curr: string[]) => { const hasChanged = (prev: string[], curr: string[]) => {
if (prev.length !== curr.length) { if (prev.length !== curr.length) {
@@ -157,7 +163,7 @@ const hasChanged = (prev: string[], curr: string[]) => {
}; };
export default mixins(workflowHelpers, titleChange).extend({ export default mixins(workflowHelpers, titleChange).extend({
name: "WorkflowDetails", name: 'WorkflowDetails',
components: { components: {
TagsContainer, TagsContainer,
PushConnectionTracker, PushConnectionTracker,
@@ -204,7 +210,11 @@ export default mixins(workflowHelpers, titleChange).extend({
return this.workflowsStore.workflowTags; return this.workflowsStore.workflowTags;
}, },
isNewWorkflow(): boolean { isNewWorkflow(): boolean {
return !this.currentWorkflowId || (this.currentWorkflowId === PLACEHOLDER_EMPTY_WORKFLOW_ID || this.currentWorkflowId === 'new'); return (
!this.currentWorkflowId ||
this.currentWorkflowId === PLACEHOLDER_EMPTY_WORKFLOW_ID ||
this.currentWorkflowId === 'new'
);
}, },
isWorkflowSaving(): boolean { isWorkflowSaving(): boolean {
return this.uiStore.isActionActive('workflowSaving'); return this.uiStore.isActionActive('workflowSaving');
@@ -216,10 +226,17 @@ export default mixins(workflowHelpers, titleChange).extend({
return this.workflowsStore.workflowId; return this.workflowsStore.workflowId;
}, },
onWorkflowPage(): boolean { onWorkflowPage(): boolean {
return this.$route.meta && (this.$route.meta.nodeView || this.$route.meta.keepWorkflowAlive === true); return (
this.$route.meta &&
(this.$route.meta.nodeView || this.$route.meta.keepWorkflowAlive === true)
);
}, },
onExecutionsTab(): boolean { onExecutionsTab(): boolean {
return [ VIEWS.EXECUTION_HOME.toString(), VIEWS.EXECUTIONS.toString(), VIEWS.EXECUTION_PREVIEW ].includes(this.$route.name || ''); return [
VIEWS.EXECUTION_HOME.toString(),
VIEWS.EXECUTIONS.toString(),
VIEWS.EXECUTION_PREVIEW,
].includes(this.$route.name || '');
}, },
workflowPermissions(): IPermissions { workflowPermissions(): IPermissions {
return getWorkflowPermissions(this.usersStore.currentUser, this.workflow); return getWorkflowPermissions(this.usersStore.currentUser, this.workflow);
@@ -251,31 +268,40 @@ export default mixins(workflowHelpers, titleChange).extend({
label: this.$locale.baseText('generic.settings'), label: this.$locale.baseText('generic.settings'),
disabled: !this.onWorkflowPage || this.isNewWorkflow, disabled: !this.onWorkflowPage || this.isNewWorkflow,
}, },
...(this.workflowPermissions.delete ? [ ...(this.workflowPermissions.delete
{ ? [
id: WORKFLOW_MENU_ACTIONS.DELETE, {
label: this.$locale.baseText('menuActions.delete'), id: WORKFLOW_MENU_ACTIONS.DELETE,
disabled: !this.onWorkflowPage || this.isNewWorkflow, label: this.$locale.baseText('menuActions.delete'),
customClass: this.$style.deleteItem, disabled: !this.onWorkflowPage || this.isNewWorkflow,
divided: true, customClass: this.$style.deleteItem,
}, divided: true,
] : []), },
]
: []),
]; ];
}, },
}, },
methods: { methods: {
async onSaveButtonClick () { async onSaveButtonClick() {
let currentId = undefined; let currentId = undefined;
if (this.currentWorkflowId !== PLACEHOLDER_EMPTY_WORKFLOW_ID) { if (this.currentWorkflowId !== PLACEHOLDER_EMPTY_WORKFLOW_ID) {
currentId = this.currentWorkflowId; currentId = this.currentWorkflowId;
} else if (this.$route.params.name && this.$route.params.name !== 'new') { } else if (this.$route.params.name && this.$route.params.name !== 'new') {
currentId = this.$route.params.name; currentId = this.$route.params.name;
} }
const saved = await this.saveCurrentWorkflow({ id: currentId, name: this.workflowName, tags: this.currentWorkflowTagIds }); const saved = await this.saveCurrentWorkflow({
id: currentId,
name: this.workflowName,
tags: this.currentWorkflowTagIds,
});
if (saved) await this.settingsStore.fetchPromptsData(); if (saved) await this.settingsStore.fetchPromptsData();
}, },
onShareButtonClick() { onShareButtonClick() {
this.uiStore.openModalWithData({ name: WORKFLOW_SHARE_MODAL_KEY, data: { id: this.currentWorkflowId } }); this.uiStore.openModalWithData({
name: WORKFLOW_SHARE_MODAL_KEY,
data: { id: this.currentWorkflowId },
});
}, },
onTagsEditEnable() { onTagsEditEnable() {
this.$data.appliedTagIds = this.currentWorkflowTagIds; this.$data.appliedTagIds = this.currentWorkflowTagIds;
@@ -305,7 +331,10 @@ export default mixins(workflowHelpers, titleChange).extend({
this.$data.tagsSaving = true; this.$data.tagsSaving = true;
const saved = await this.saveCurrentWorkflow({ tags }); const saved = await this.saveCurrentWorkflow({ tags });
this.$telemetry.track('User edited workflow tags', { workflow_id: this.currentWorkflowId as string, new_tag_count: tags.length }); this.$telemetry.track('User edited workflow tags', {
workflow_id: this.currentWorkflowId as string,
new_tag_count: tags.length,
});
this.$data.tagsSaving = false; this.$data.tagsSaving = false;
if (saved) { if (saved) {
@@ -332,7 +361,7 @@ export default mixins(workflowHelpers, titleChange).extend({
this.$showMessage({ this.$showMessage({
title: this.$locale.baseText('workflowDetails.showMessage.title'), title: this.$locale.baseText('workflowDetails.showMessage.title'),
message: this.$locale.baseText('workflowDetails.showMessage.message'), message: this.$locale.baseText('workflowDetails.showMessage.message'),
type: "error", type: 'error',
}); });
cb(false); cb(false);
@@ -392,7 +421,7 @@ export default mixins(workflowHelpers, titleChange).extend({
} }
case WORKFLOW_MENU_ACTIONS.DOWNLOAD: { case WORKFLOW_MENU_ACTIONS.DOWNLOAD: {
const workflowData = await this.getWorkflowDataToSave(); const workflowData = await this.getWorkflowDataToSave();
const {tags, ...data} = workflowData; const { tags, ...data } = workflowData;
if (data.id && typeof data.id === 'string') { if (data.id && typeof data.id === 'string') {
data.id = parseInt(data.id, 10); data.id = parseInt(data.id, 10);
} }
@@ -402,8 +431,8 @@ export default mixins(workflowHelpers, titleChange).extend({
meta: { meta: {
instanceId: this.rootStore.instanceId, instanceId: this.rootStore.instanceId,
}, },
tags: (tags || []).map(tagId => { tags: (tags || []).map((tagId) => {
const {usageCount, ...tag} = this.tagsStore.getTagById(tagId); const { usageCount, ...tag } = this.tagsStore.getTagById(tagId);
return tag; return tag;
}), }),
@@ -422,16 +451,16 @@ export default mixins(workflowHelpers, titleChange).extend({
} }
case WORKFLOW_MENU_ACTIONS.IMPORT_FROM_URL: { case WORKFLOW_MENU_ACTIONS.IMPORT_FROM_URL: {
try { try {
const promptResponse = await this.$prompt( const promptResponse = (await this.$prompt(
this.$locale.baseText('mainSidebar.prompt.workflowUrl') + ':', this.$locale.baseText('mainSidebar.prompt.workflowUrl') + ':',
this.$locale.baseText('mainSidebar.prompt.importWorkflowFromUrl') + ':', this.$locale.baseText('mainSidebar.prompt.importWorkflowFromUrl') + ':',
{ {
confirmButtonText: this.$locale.baseText('mainSidebar.prompt.import'), confirmButtonText: this.$locale.baseText('mainSidebar.prompt.import'),
cancelButtonText: this.$locale.baseText('mainSidebar.prompt.cancel'), cancelButtonText: this.$locale.baseText('mainSidebar.prompt.cancel'),
inputErrorMessage: this.$locale.baseText('mainSidebar.prompt.invalidUrl'), inputErrorMessage: this.$locale.baseText('mainSidebar.prompt.invalidUrl'),
inputPattern: /^http[s]?:\/\/.*\.json$/i, inputPattern: /^http[s]?:\/\/.*\.json$/i,
}, },
) as MessageBoxInputData; )) as MessageBoxInputData;
this.$root.$emit('importWorkflowUrl', { url: promptResponse.value }); this.$root.$emit('importWorkflowUrl', { url: promptResponse.value });
} catch (e) {} } catch (e) {}
@@ -447,10 +476,9 @@ export default mixins(workflowHelpers, titleChange).extend({
} }
case WORKFLOW_MENU_ACTIONS.DELETE: { case WORKFLOW_MENU_ACTIONS.DELETE: {
const deleteConfirmed = await this.confirmMessage( const deleteConfirmed = await this.confirmMessage(
this.$locale.baseText( this.$locale.baseText('mainSidebar.confirmMessage.workflowDelete.message', {
'mainSidebar.confirmMessage.workflowDelete.message', interpolate: { workflowName: this.workflowName },
{ interpolate: { workflowName: this.workflowName } }, }),
),
this.$locale.baseText('mainSidebar.confirmMessage.workflowDelete.headline'), this.$locale.baseText('mainSidebar.confirmMessage.workflowDelete.headline'),
'warning', 'warning',
this.$locale.baseText('mainSidebar.confirmMessage.workflowDelete.confirmButtonText'), this.$locale.baseText('mainSidebar.confirmMessage.workflowDelete.confirmButtonText'),

View File

@@ -1,51 +1,88 @@
<template> <template>
<div id="side-menu" :class="{ <div
['side-menu']: true, id="side-menu"
[$style.sideMenu]: true, :class="{
[$style.sideMenuCollapsed]: isCollapsed ['side-menu']: true,
}"> [$style.sideMenu]: true,
[$style.sideMenuCollapsed]: isCollapsed,
}"
>
<div <div
id="collapse-change-button" id="collapse-change-button"
:class="{ ['clickable']: true, [$style.sideMenuCollapseButton]: true, [$style.expandedButton]: !isCollapsed }" :class="{
@click="toggleCollapse"> ['clickable']: true,
</div> [$style.sideMenuCollapseButton]: true,
[$style.expandedButton]: !isCollapsed,
}"
@click="toggleCollapse"
></div>
<n8n-menu :items="mainMenuItems" :collapsed="isCollapsed" @select="handleSelect"> <n8n-menu :items="mainMenuItems" :collapsed="isCollapsed" @select="handleSelect">
<template #header> <template #header>
<div :class="$style.logo"> <div :class="$style.logo">
<img :src="basePath + (isCollapsed ? 'n8n-logo-collapsed.svg' : 'n8n-logo-expanded.svg')" :class="$style.icon" alt="n8n"/> <img
:src="basePath + (isCollapsed ? 'n8n-logo-collapsed.svg' : 'n8n-logo-expanded.svg')"
:class="$style.icon"
alt="n8n"
/>
</div> </div>
</template> </template>
<template #menuSuffix v-if="hasVersionUpdates"> <template #menuSuffix v-if="hasVersionUpdates">
<div :class="$style.updates" @click="openUpdatesPanel"> <div :class="$style.updates" @click="openUpdatesPanel">
<div :class="$style.giftContainer"> <div :class="$style.giftContainer">
<GiftNotificationIcon /> <GiftNotificationIcon />
</div> </div>
<n8n-text :class="{['ml-xs']: true, [$style.expanded]: fullyExpanded }" color="text-base"> <n8n-text
{{ nextVersions.length > 99 ? '99+' : nextVersions.length}} update{{nextVersions.length > 1 ? 's' : '' }} :class="{ ['ml-xs']: true, [$style.expanded]: fullyExpanded }"
</n8n-text> color="text-base"
>
{{ nextVersions.length > 99 ? '99+' : nextVersions.length }} update{{
nextVersions.length > 1 ? 's' : ''
}}
</n8n-text>
</div> </div>
</template> </template>
<template #footer v-if="showUserArea"> <template #footer v-if="showUserArea">
<div :class="$style.userArea"> <div :class="$style.userArea">
<div class="ml-3xs"> <div class="ml-3xs">
<!-- This dropdown is only enabled when sidebar is collapsed --> <!-- This dropdown is only enabled when sidebar is collapsed -->
<el-dropdown :disabled="!isCollapsed" placement="right-end" trigger="click" @command="onUserActionToggle"> <el-dropdown
<div :class="{[$style.avatar]: true, ['clickable']: isCollapsed }"> :disabled="!isCollapsed"
<n8n-avatar :firstName="usersStore.currentUser.firstName" :lastName="usersStore.currentUser.lastName" size="small" /> placement="right-end"
trigger="click"
@command="onUserActionToggle"
>
<div :class="{ [$style.avatar]: true, ['clickable']: isCollapsed }">
<n8n-avatar
:firstName="usersStore.currentUser.firstName"
:lastName="usersStore.currentUser.lastName"
size="small"
/>
</div> </div>
<template #dropdown> <template #dropdown>
<el-dropdown-menu> <el-dropdown-menu>
<el-dropdown-item command="settings">{{ $locale.baseText('settings') }}</el-dropdown-item> <el-dropdown-item command="settings">{{
<el-dropdown-item command="logout">{{ $locale.baseText('auth.signout') }}</el-dropdown-item> $locale.baseText('settings')
}}</el-dropdown-item>
<el-dropdown-item command="logout">{{
$locale.baseText('auth.signout')
}}</el-dropdown-item>
</el-dropdown-menu> </el-dropdown-menu>
</template> </template>
</el-dropdown> </el-dropdown>
</div> </div>
<div :class="{ ['ml-2xs']: true, [$style.userName]: true, [$style.expanded]: fullyExpanded }"> <div
<n8n-text size="small" :bold="true" color="text-dark">{{usersStore.currentUser.fullName}}</n8n-text> :class="{ ['ml-2xs']: true, [$style.userName]: true, [$style.expanded]: fullyExpanded }"
>
<n8n-text size="small" :bold="true" color="text-dark">{{
usersStore.currentUser.fullName
}}</n8n-text>
</div> </div>
<div :class="{ [$style.userActions]: true, [$style.expanded]: fullyExpanded }"> <div :class="{ [$style.userActions]: true, [$style.expanded]: fullyExpanded }">
<n8n-action-dropdown :items="userMenuItems" placement="top-start" @select="onUserActionToggle" /> <n8n-action-dropdown
:items="userMenuItems"
placement="top-start"
@select="onUserActionToggle"
/>
</div> </div>
</div> </div>
</template> </template>
@@ -54,11 +91,7 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { import { IExecutionResponse, IMenuItem, IVersion } from '../Interface';
IExecutionResponse,
IMenuItem,
IVersion,
} from '../Interface';
import ExecutionsList from '@/components/ExecutionsList.vue'; import ExecutionsList from '@/components/ExecutionsList.vue';
import GiftNotificationIcon from './GiftNotificationIcon.vue'; import GiftNotificationIcon from './GiftNotificationIcon.vue';
@@ -102,372 +135,379 @@ export default mixins(
workflowRun, workflowRun,
userHelpers, userHelpers,
debounceHelper, debounceHelper,
) ).extend({
.extend({ name: 'MainSidebar',
name: 'MainSidebar', components: {
components: { ExecutionsList,
ExecutionsList, GiftNotificationIcon,
GiftNotificationIcon, WorkflowSettings,
WorkflowSettings, },
data() {
return {
// @ts-ignore
basePath: '',
fullyExpanded: false,
};
},
computed: {
...mapStores(
useRootStore,
useSettingsStore,
useUIStore,
useUsersStore,
useVersionsStore,
useWorkflowsStore,
),
hasVersionUpdates(): boolean {
return this.versionsStore.hasVersionUpdates;
}, },
data () { nextVersions(): IVersion[] {
return { return this.versionsStore.nextVersions;
// @ts-ignore
basePath: '',
fullyExpanded: false,
};
}, },
computed: { isCollapsed(): boolean {
...mapStores( return this.uiStore.sidebarMenuCollapsed;
useRootStore, },
useSettingsStore, canUserAccessSettings(): boolean {
useUIStore, const accessibleRoute = this.findFirstAccessibleSettingsRoute();
useUsersStore, return accessibleRoute !== null;
useVersionsStore, },
useWorkflowsStore, showUserArea(): boolean {
), return (
hasVersionUpdates(): boolean { this.settingsStore.isUserManagementEnabled &&
return this.versionsStore.hasVersionUpdates; this.usersStore.canUserAccessSidebarUserInfo &&
}, this.usersStore.currentUser !== null
nextVersions(): IVersion[] { );
return this.versionsStore.nextVersions; },
}, workflowExecution(): IExecutionResponse | null {
isCollapsed(): boolean { return this.workflowsStore.getWorkflowExecution;
return this.uiStore.sidebarMenuCollapsed; },
}, userMenuItems(): object[] {
canUserAccessSettings(): boolean { return [
const accessibleRoute = this.findFirstAccessibleSettingsRoute(); {
return accessibleRoute !== null; id: 'settings',
}, label: this.$locale.baseText('settings'),
showUserArea(): boolean { },
return this.settingsStore.isUserManagementEnabled && this.usersStore.canUserAccessSidebarUserInfo && this.usersStore.currentUser !== null; {
}, id: 'logout',
workflowExecution (): IExecutionResponse | null { label: this.$locale.baseText('auth.signout'),
return this.workflowsStore.getWorkflowExecution; },
}, ];
userMenuItems (): object[] { },
return [ mainMenuItems(): IMenuItem[] {
{ const items: IMenuItem[] = [];
id: 'settings', const injectedItems = this.uiStore.sidebarMenuItems;
label: this.$locale.baseText('settings'),
},
{
id: 'logout',
label: this.$locale.baseText('auth.signout'),
},
];
},
mainMenuItems (): IMenuItem[] {
const items: IMenuItem[] = [];
const injectedItems = this.uiStore.sidebarMenuItems;
if (injectedItems && injectedItems.length > 0) { if (injectedItems && injectedItems.length > 0) {
for(const item of injectedItems) { for (const item of injectedItems) {
items.push( items.push({
{ id: item.id,
id: item.id, // @ts-ignore
// @ts-ignore icon: item.properties ? item.properties.icon : '',
icon: item.properties ? item.properties.icon : '', // @ts-ignore
// @ts-ignore label: item.properties ? item.properties.title : '',
label: item.properties ? item.properties.title : '', position: item.position,
position: item.position, type: item.properties?.href ? 'link' : 'regular',
type: item.properties?.href ? 'link' : 'regular', properties: item.properties,
properties: item.properties, } as IMenuItem);
} as IMenuItem, }
); }
}
};
const regularItems: IMenuItem[] = [ const regularItems: IMenuItem[] = [
{ {
id: 'workflows', id: 'workflows',
icon: 'network-wired', icon: 'network-wired',
label: this.$locale.baseText('mainSidebar.workflows'), label: this.$locale.baseText('mainSidebar.workflows'),
position: 'top', position: 'top',
activateOnRouteNames: [ VIEWS.WORKFLOWS ], activateOnRouteNames: [VIEWS.WORKFLOWS],
}, },
{ {
id: 'templates', id: 'templates',
icon: 'box-open', icon: 'box-open',
label: this.$locale.baseText('mainSidebar.templates'), label: this.$locale.baseText('mainSidebar.templates'),
position: 'top', position: 'top',
available: this.settingsStore.isTemplatesEnabled, available: this.settingsStore.isTemplatesEnabled,
activateOnRouteNames: [ VIEWS.TEMPLATES ], activateOnRouteNames: [VIEWS.TEMPLATES],
}, },
{ {
id: 'credentials', id: 'credentials',
icon: 'key', icon: 'key',
label: this.$locale.baseText('mainSidebar.credentials'), label: this.$locale.baseText('mainSidebar.credentials'),
customIconSize: 'medium', customIconSize: 'medium',
position: 'top', position: 'top',
activateOnRouteNames: [ VIEWS.CREDENTIALS ], activateOnRouteNames: [VIEWS.CREDENTIALS],
}, },
{ {
id: 'executions', id: 'executions',
icon: 'tasks', icon: 'tasks',
label: this.$locale.baseText('generic.executions'), label: this.$locale.baseText('generic.executions'),
position: 'top', position: 'top',
}, },
{ {
id: 'settings', id: 'settings',
icon: 'cog', icon: 'cog',
label: this.$locale.baseText('settings'), label: this.$locale.baseText('settings'),
position: 'bottom', position: 'bottom',
available: this.canUserAccessSettings && this.usersStore.currentUser !== null, available: this.canUserAccessSettings && this.usersStore.currentUser !== null,
activateOnRouteNames: [ VIEWS.USERS_SETTINGS, VIEWS.API_SETTINGS, VIEWS.PERSONAL_SETTINGS ], activateOnRouteNames: [VIEWS.USERS_SETTINGS, VIEWS.API_SETTINGS, VIEWS.PERSONAL_SETTINGS],
}, },
{ {
id: 'help', id: 'help',
icon: 'question', icon: 'question',
label: 'Help', label: 'Help',
position: 'bottom', position: 'bottom',
children: [ children: [
{ {
id: 'quickstart', id: 'quickstart',
icon: 'video', icon: 'video',
label: this.$locale.baseText('mainSidebar.helpMenuItems.quickstart'), label: this.$locale.baseText('mainSidebar.helpMenuItems.quickstart'),
type: 'link', type: 'link',
properties: { properties: {
href: 'https://www.youtube.com/watch?v=RpjQTGKm-ok', href: 'https://www.youtube.com/watch?v=RpjQTGKm-ok',
newWindow: true, newWindow: true,
},
}, },
{ },
id: 'docs', {
icon: 'book', id: 'docs',
label: this.$locale.baseText('mainSidebar.helpMenuItems.documentation'), icon: 'book',
type: 'link', label: this.$locale.baseText('mainSidebar.helpMenuItems.documentation'),
properties: { type: 'link',
href: 'https://docs.n8n.io', properties: {
newWindow: true, href: 'https://docs.n8n.io',
}, newWindow: true,
}, },
{ },
id: 'forum', {
icon: 'users', id: 'forum',
label: this.$locale.baseText('mainSidebar.helpMenuItems.forum'), icon: 'users',
type: 'link', label: this.$locale.baseText('mainSidebar.helpMenuItems.forum'),
properties: { type: 'link',
href: 'https://community.n8n.io', properties: {
newWindow: true, href: 'https://community.n8n.io',
}, newWindow: true,
}, },
{ },
id: 'examples', {
icon: 'graduation-cap', id: 'examples',
label: this.$locale.baseText('mainSidebar.helpMenuItems.course'), icon: 'graduation-cap',
type: 'link', label: this.$locale.baseText('mainSidebar.helpMenuItems.course'),
properties: { type: 'link',
href: 'https://www.youtube.com/watch?v=RpjQTGKm-ok', properties: {
newWindow: true, href: 'https://www.youtube.com/watch?v=RpjQTGKm-ok',
}, newWindow: true,
}, },
{ },
id: 'about', {
icon: 'info', id: 'about',
label: this.$locale.baseText('mainSidebar.aboutN8n'), icon: 'info',
position: 'bottom', label: this.$locale.baseText('mainSidebar.aboutN8n'),
}, position: 'bottom',
], },
}, ],
]; },
return [ ...items, ...regularItems ]; ];
}, return [...items, ...regularItems];
}, },
async mounted() { },
this.basePath = this.rootStore.baseUrl; async mounted() {
if (this.$refs.user) { this.basePath = this.rootStore.baseUrl;
this.$externalHooks().run('mainSidebar.mounted', { userRef: this.$refs.user }); if (this.$refs.user) {
this.$externalHooks().run('mainSidebar.mounted', { userRef: this.$refs.user });
}
if (window.innerWidth < 900 || this.uiStore.isNodeView) {
this.uiStore.sidebarMenuCollapsed = true;
} else {
this.uiStore.sidebarMenuCollapsed = false;
}
await Vue.nextTick();
this.fullyExpanded = !this.isCollapsed;
},
created() {
window.addEventListener('resize', this.onResize);
},
destroyed() {
window.removeEventListener('resize', this.onResize);
},
methods: {
trackHelpItemClick(itemType: string) {
this.$telemetry.track('User clicked help resource', {
type: itemType,
workflow_id: this.workflowsStore.workflowId,
});
},
async onUserActionToggle(action: string) {
switch (action) {
case 'logout':
this.onLogout();
break;
case 'settings':
this.$router.push({ name: VIEWS.PERSONAL_SETTINGS });
break;
default:
break;
} }
if (window.innerWidth < 900 || this.uiStore.isNodeView) { },
this.uiStore.sidebarMenuCollapsed = true; async onLogout() {
} else { try {
this.uiStore.sidebarMenuCollapsed = false; await this.usersStore.logout();
const route = this.$router.resolve({ name: VIEWS.SIGNIN });
window.open(route.href, '_self');
} catch (e) {
this.$showError(e, this.$locale.baseText('auth.signout.error'));
} }
await Vue.nextTick();
this.fullyExpanded = !this.isCollapsed;
}, },
created() { toggleCollapse() {
window.addEventListener("resize", this.onResize); this.uiStore.toggleSidebarMenuCollapse();
}, // When expanding, delay showing some element to ensure smooth animation
destroyed() { if (!this.isCollapsed) {
window.removeEventListener("resize", this.onResize); setTimeout(() => {
},
methods: {
trackHelpItemClick (itemType: string) {
this.$telemetry.track('User clicked help resource', { type: itemType, workflow_id: this.workflowsStore.workflowId });
},
async onUserActionToggle(action: string) {
switch (action) {
case 'logout':
this.onLogout();
break;
case 'settings':
this.$router.push({name: VIEWS.PERSONAL_SETTINGS});
break;
default:
break;
}
},
async onLogout() {
try {
await this.usersStore.logout();
const route = this.$router.resolve({ name: VIEWS.SIGNIN });
window.open(route.href, '_self');
} catch (e) {
this.$showError(e, this.$locale.baseText('auth.signout.error'));
}
},
toggleCollapse () {
this.uiStore.toggleSidebarMenuCollapse();
// When expanding, delay showing some element to ensure smooth animation
if (!this.isCollapsed) {
setTimeout(() => {
this.fullyExpanded = !this.isCollapsed;
}, 300);
} else {
this.fullyExpanded = !this.isCollapsed; this.fullyExpanded = !this.isCollapsed;
}, 300);
} else {
this.fullyExpanded = !this.isCollapsed;
}
},
openUpdatesPanel() {
this.uiStore.openModal(VERSIONS_MODAL_KEY);
},
async handleSelect(key: string) {
switch (key) {
case 'workflows': {
if (this.$router.currentRoute.name !== VIEWS.WORKFLOWS) {
this.$router.push({ name: VIEWS.WORKFLOWS });
}
break;
} }
}, case 'templates': {
openUpdatesPanel() { if (this.$router.currentRoute.name !== VIEWS.TEMPLATES) {
this.uiStore.openModal(VERSIONS_MODAL_KEY); this.$router.push({ name: VIEWS.TEMPLATES });
},
async handleSelect (key: string) {
switch (key) {
case 'workflows': {
if (this.$router.currentRoute.name !== VIEWS.WORKFLOWS) {
this.$router.push({name: VIEWS.WORKFLOWS});
}
break;
} }
case 'templates': { break;
if (this.$router.currentRoute.name !== VIEWS.TEMPLATES) {
this.$router.push({ name: VIEWS.TEMPLATES });
}
break;
}
case 'credentials': {
if (this.$router.currentRoute.name !== VIEWS.CREDENTIALS) {
this.$router.push({name: VIEWS.CREDENTIALS});
}
break;
}
case 'executions': {
this.uiStore.openModal(EXECUTIONS_MODAL_KEY);
break;
}
case 'settings': {
const defaultRoute = this.findFirstAccessibleSettingsRoute();
if (defaultRoute) {
const routeProps = this.$router.resolve({ name: defaultRoute });
if (this.$router.currentRoute.name !== defaultRoute) {
this.$router.push(routeProps.route.path);
}
}
break;
}
case 'about': {
this.trackHelpItemClick('about');
this.uiStore.openModal(ABOUT_MODAL_KEY);
break;
}
case 'quickstart':
case 'docs':
case 'forum':
case 'examples' : {
this.trackHelpItemClick(key);
break;
}
default: break;
} }
}, case 'credentials': {
async createNewWorkflow (): Promise<void> { if (this.$router.currentRoute.name !== VIEWS.CREDENTIALS) {
const result = this.uiStore.stateIsDirty; this.$router.push({ name: VIEWS.CREDENTIALS });
if(result) {
const confirmModal = await this.confirmModal(
this.$locale.baseText('generic.unsavedWork.confirmMessage.message'),
this.$locale.baseText('generic.unsavedWork.confirmMessage.headline'),
'warning',
this.$locale.baseText('generic.unsavedWork.confirmMessage.confirmButtonText'),
this.$locale.baseText('generic.unsavedWork.confirmMessage.cancelButtonText'),
true,
);
if (confirmModal === MODAL_CONFIRMED) {
const saved = await this.saveCurrentWorkflow({}, false);
if (saved) await this.settingsStore.fetchPromptsData();
if (this.$router.currentRoute.name === VIEWS.NEW_WORKFLOW) {
this.$root.$emit('newWorkflow');
} else {
this.$router.push({ name: VIEWS.NEW_WORKFLOW });
}
this.$showMessage({
title: this.$locale.baseText('mainSidebar.showMessage.handleSelect2.title'),
type: 'success',
});
} else if (confirmModal === MODAL_CANCEL) {
this.uiStore.stateIsDirty = false;
if (this.$router.currentRoute.name === VIEWS.NEW_WORKFLOW) {
this.$root.$emit('newWorkflow');
} else {
this.workflowsStore.setWorkflowId(PLACEHOLDER_EMPTY_WORKFLOW_ID);
this.$router.push({ name: VIEWS.NEW_WORKFLOW });
}
this.$showMessage({
title: this.$locale.baseText('mainSidebar.showMessage.handleSelect2.title'),
type: 'success',
});
} else if (confirmModal === MODAL_CLOSE) {
return;
} }
} else { break;
if (this.$router.currentRoute.name !== VIEWS.NEW_WORKFLOW) { }
case 'executions': {
this.uiStore.openModal(EXECUTIONS_MODAL_KEY);
break;
}
case 'settings': {
const defaultRoute = this.findFirstAccessibleSettingsRoute();
if (defaultRoute) {
const routeProps = this.$router.resolve({ name: defaultRoute });
if (this.$router.currentRoute.name !== defaultRoute) {
this.$router.push(routeProps.route.path);
}
}
break;
}
case 'about': {
this.trackHelpItemClick('about');
this.uiStore.openModal(ABOUT_MODAL_KEY);
break;
}
case 'quickstart':
case 'docs':
case 'forum':
case 'examples': {
this.trackHelpItemClick(key);
break;
}
default:
break;
}
},
async createNewWorkflow(): Promise<void> {
const result = this.uiStore.stateIsDirty;
if (result) {
const confirmModal = await this.confirmModal(
this.$locale.baseText('generic.unsavedWork.confirmMessage.message'),
this.$locale.baseText('generic.unsavedWork.confirmMessage.headline'),
'warning',
this.$locale.baseText('generic.unsavedWork.confirmMessage.confirmButtonText'),
this.$locale.baseText('generic.unsavedWork.confirmMessage.cancelButtonText'),
true,
);
if (confirmModal === MODAL_CONFIRMED) {
const saved = await this.saveCurrentWorkflow({}, false);
if (saved) await this.settingsStore.fetchPromptsData();
if (this.$router.currentRoute.name === VIEWS.NEW_WORKFLOW) {
this.$root.$emit('newWorkflow');
} else {
this.$router.push({ name: VIEWS.NEW_WORKFLOW });
}
this.$showMessage({
title: this.$locale.baseText('mainSidebar.showMessage.handleSelect2.title'),
type: 'success',
});
} else if (confirmModal === MODAL_CANCEL) {
this.uiStore.stateIsDirty = false;
if (this.$router.currentRoute.name === VIEWS.NEW_WORKFLOW) {
this.$root.$emit('newWorkflow');
} else {
this.workflowsStore.setWorkflowId(PLACEHOLDER_EMPTY_WORKFLOW_ID); this.workflowsStore.setWorkflowId(PLACEHOLDER_EMPTY_WORKFLOW_ID);
this.$router.push({ name: VIEWS.NEW_WORKFLOW }); this.$router.push({ name: VIEWS.NEW_WORKFLOW });
} }
this.$showMessage({ this.$showMessage({
title: this.$locale.baseText('mainSidebar.showMessage.handleSelect3.title'), title: this.$locale.baseText('mainSidebar.showMessage.handleSelect2.title'),
type: 'success', type: 'success',
}); });
} else if (confirmModal === MODAL_CLOSE) {
return;
} }
this.$titleReset(); } else {
}, if (this.$router.currentRoute.name !== VIEWS.NEW_WORKFLOW) {
findFirstAccessibleSettingsRoute () { this.workflowsStore.setWorkflowId(PLACEHOLDER_EMPTY_WORKFLOW_ID);
// Get all settings rotes by filtering them by pageCategory property this.$router.push({ name: VIEWS.NEW_WORKFLOW });
const settingsRoutes = this.$router.getRoutes().filter(
category => category.meta.telemetry &&
category.meta.telemetry.pageCategory === 'settings',
).map(route => route.name || '');
let defaultSettingsRoute = null;
for (const route of settingsRoutes) {
if (this.canUserAccessRouteByName(route)) {
defaultSettingsRoute = route;
break;
}
} }
return defaultSettingsRoute; this.$showMessage({
}, title: this.$locale.baseText('mainSidebar.showMessage.handleSelect3.title'),
onResize (event: UIEvent) { type: 'success',
this.callDebounced("onResizeEnd", { debounceTime: 100 }, event); });
}, }
onResizeEnd (event: UIEvent) { this.$titleReset();
const browserWidth = (event.target as Window).outerWidth;
this.checkWidthAndAdjustSidebar(browserWidth);
},
checkWidthAndAdjustSidebar (width: number) {
if (width < 900) {
this.uiStore.sidebarMenuCollapsed = true;
Vue.nextTick(() => {
this.fullyExpanded = !this.isCollapsed;
});
}
},
}, },
}); findFirstAccessibleSettingsRoute() {
// Get all settings rotes by filtering them by pageCategory property
const settingsRoutes = this.$router
.getRoutes()
.filter(
(category) =>
category.meta.telemetry && category.meta.telemetry.pageCategory === 'settings',
)
.map((route) => route.name || '');
let defaultSettingsRoute = null;
for (const route of settingsRoutes) {
if (this.canUserAccessRouteByName(route)) {
defaultSettingsRoute = route;
break;
}
}
return defaultSettingsRoute;
},
onResize(event: UIEvent) {
this.callDebounced('onResizeEnd', { debounceTime: 100 }, event);
},
onResizeEnd(event: UIEvent) {
const browserWidth = (event.target as Window).outerWidth;
this.checkWidthAndAdjustSidebar(browserWidth);
},
checkWidthAndAdjustSidebar(width: number) {
if (width < 900) {
this.uiStore.sidebarMenuCollapsed = true;
Vue.nextTick(() => {
this.fullyExpanded = !this.isCollapsed;
});
}
},
},
});
</script> </script>
<style lang="scss" module> <style lang="scss" module>
.sideMenu { .sideMenu {
position: relative; position: relative;
height: 100%; height: 100%;
@@ -518,7 +558,7 @@ export default mixins(
left: px; left: px;
top: -2.5px; top: -2.5px;
transform: rotate(270deg); transform: rotate(270deg);
content: "\e6df"; content: '\e6df';
font-family: element-icons; font-family: element-icons;
font-size: var(--font-size-2xs); font-size: var(--font-size-2xs);
font-weight: bold; font-weight: bold;
@@ -545,14 +585,19 @@ export default mixins(
height: 26px; height: 26px;
cursor: pointer; cursor: pointer;
svg { color: var(--color-text-base) !important; } svg {
color: var(--color-text-base) !important;
}
span { span {
display: none; display: none;
&.expanded { display: initial; } &.expanded {
display: initial;
}
} }
&:hover { &:hover {
&, & svg { &,
& svg {
color: var(--color-text-dark) !important; color: var(--color-text-dark) !important;
} }
} }
@@ -591,8 +636,9 @@ export default mixins(
} }
} }
@media screen and (max-height: 470px) { @media screen and (max-height: 470px) {
:global(#help) { display: none; } :global(#help) {
display: none;
}
} }
</style> </style>

View File

@@ -2,7 +2,7 @@
<el-dialog <el-dialog
:visible="uiStore.isModalOpen(this.$props.name)" :visible="uiStore.isModalOpen(this.$props.name)"
:before-close="closeDialog" :before-close="closeDialog"
:class="{'dialog-wrapper': true, [$style.center]: center, scrollable: scrollable}" :class="{ 'dialog-wrapper': true, [$style.center]: center, scrollable: scrollable }"
:width="width" :width="width"
:show-close="showClose" :show-close="showClose"
:custom-class="getCustomClass()" :custom-class="getCustomClass()"
@@ -18,15 +18,20 @@
<template #title v-else-if="title"> <template #title v-else-if="title">
<div :class="centerTitle ? $style.centerTitle : ''"> <div :class="centerTitle ? $style.centerTitle : ''">
<div v-if="title"> <div v-if="title">
<n8n-heading tag="h1" size="xlarge">{{title}}</n8n-heading> <n8n-heading tag="h1" size="xlarge">{{ title }}</n8n-heading>
</div> </div>
<div v-if="subtitle" :class="$style.subtitle"> <div v-if="subtitle" :class="$style.subtitle">
<n8n-heading tag="h3" size="small" color="text-light">{{subtitle}}</n8n-heading> <n8n-heading tag="h3" size="small" color="text-light">{{ subtitle }}</n8n-heading>
</div> </div>
</div> </div>
</template> </template>
<div class="modal-content" @keydown.stop @keydown.enter="handleEnter" @keydown.esc="closeDialog"> <div
<slot v-if="!loading" name="content"/> class="modal-content"
@keydown.stop
@keydown.enter="handleEnter"
@keydown.esc="closeDialog"
>
<slot v-if="!loading" name="content" />
<div :class="$style.loader" v-else> <div :class="$style.loader" v-else>
<n8n-spinner /> <n8n-spinner />
</div> </div>
@@ -38,12 +43,12 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import Vue from "vue"; import Vue from 'vue';
import { useUIStore } from '@/stores/ui'; import { useUIStore } from '@/stores/ui';
import { mapStores } from "pinia"; import { mapStores } from 'pinia';
export default Vue.extend({ export default Vue.extend({
name: "Modal", name: 'Modal',
props: { props: {
name: { name: {
type: String, type: String,
@@ -136,7 +141,7 @@ export default Vue.extend({
computed: { computed: {
...mapStores(useUIStore), ...mapStores(useUIStore),
styles() { styles() {
const styles: {[prop: string]: string} = {}; const styles: { [prop: string]: string } = {};
if (this.height) { if (this.height) {
styles['--dialog-height'] = this.height; styles['--dialog-height'] = this.height;
} }
@@ -173,7 +178,8 @@ export default Vue.extend({
async closeDialog() { async closeDialog() {
if (this.beforeClose) { if (this.beforeClose) {
const shouldClose = await this.beforeClose(); const shouldClose = await this.beforeClose();
if (shouldClose === false) { // must be strictly false to stop modal from closing if (shouldClose === false) {
// must be strictly false to stop modal from closing
return; return;
} }
} }
@@ -224,9 +230,9 @@ export default Vue.extend({
<style lang="scss" module> <style lang="scss" module>
.center { .center {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
} }
.loader { .loader {

View File

@@ -6,25 +6,25 @@
:before-close="close" :before-close="close"
:modal="modal" :modal="modal"
:wrapperClosable="wrapperClosable" :wrapperClosable="wrapperClosable"
> >
<template #title> <template #title>
<slot name="header" /> <slot name="header" />
</template> </template>
<template> <template>
<span @keydown.stop> <span @keydown.stop>
<slot name="content"/> <slot name="content" />
</span> </span>
</template> </template>
</el-drawer> </el-drawer>
</template> </template>
<script lang="ts"> <script lang="ts">
import { useUIStore } from "@/stores/ui"; import { useUIStore } from '@/stores/ui';
import { mapStores } from "pinia"; import { mapStores } from 'pinia';
import Vue from "vue"; import Vue from 'vue';
export default Vue.extend({ export default Vue.extend({
name: "ModalDrawer", name: 'ModalDrawer',
props: { props: {
name: { name: {
type: String, type: String,
@@ -88,7 +88,8 @@ export default Vue.extend({
async close() { async close() {
if (this.beforeClose) { if (this.beforeClose) {
const shouldClose = await this.beforeClose(); const shouldClose = await this.beforeClose();
if (shouldClose === false) { // must be strictly false to stop modal from closing if (shouldClose === false) {
// must be strictly false to stop modal from closing
return; return;
} }
} }

Some files were not shown because too many files have changed in this diff Show More