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
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
284 changed files with 19247 additions and 15540 deletions

View file

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

View file

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

View file

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

View file

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

View file

@ -20,7 +20,7 @@
"dev": "pnpm serve",
"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",
"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",
"test": "vitest run",
"test:ci": "vitest run --coverage",

View file

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

View file

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

View file

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

View file

@ -2,12 +2,17 @@ import { IRestApiContext } from '@/Interface';
import { PublicInstalledPackage } from 'n8n-workflow';
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');
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 });
}
@ -15,6 +20,9 @@ export async function uninstallPackage(context: IRestApiContext, name: string):
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 });
}

View file

@ -1,13 +1,16 @@
import {
ICredentialsResponse,
IRestApiContext,
IShareCredentialsPayload,
} from '@/Interface';
import { ICredentialsResponse, IRestApiContext, IShareCredentialsPayload } from '@/Interface';
import { makeRestApiRequest } from '@/utils';
import {
IDataObject,
} from 'n8n-workflow';
import { IDataObject } from 'n8n-workflow';
export async function setCredentialSharedWith(context: IRestApiContext, id: string, data: IShareCredentialsPayload): Promise<ICredentialsResponse> {
return makeRestApiRequest(context, 'PUT', `/credentials/${id}/share`, data as unknown as IDataObject);
export async function setCredentialSharedWith(
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;
}
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 } : {});
}
@ -22,7 +25,10 @@ export async function getAllCredentials(context: IRestApiContext): Promise<ICred
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);
}
@ -30,26 +36,52 @@ export async function deleteCredential(context: IRestApiContext, id: string): Pr
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);
}
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}`, {
includeData: true,
});
}
// Get OAuth1 Authorization URL using the stored credentials
export async function oAuth1CredentialAuthorize(context: IRestApiContext, data: ICredentialsResponse): Promise<string> {
return makeRestApiRequest(context, 'GET', `/oauth1-credential/auth`, data as unknown as IDataObject);
export async function oAuth1CredentialAuthorize(
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
export async function oAuth2CredentialAuthorize(context: IRestApiContext, data: ICredentialsResponse): Promise<string> {
return makeRestApiRequest(context, 'GET', `/oauth2-credential/auth`, data as unknown as IDataObject);
export async function oAuth2CredentialAuthorize(
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);
}

View file

@ -1,6 +1,9 @@
import {CurlToJSONResponse, IRestApiContext} from "@/Interface";
import {makeRestApiRequest} from "@/utils";
import { CurlToJSONResponse, IRestApiContext } from '@/Interface';
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 });
}

View file

@ -37,12 +37,12 @@ export async function getNodesInformation(
export async function getNodeParameterOptions(
context: IRestApiContext,
sendData: {
nodeTypeAndVersion: INodeTypeNameVersion,
path: string,
methodName?: string,
loadOptions?: ILoadOptions,
currentNodeParameters: INodeParameters,
credentials?: INodeCredentials,
nodeTypeAndVersion: INodeTypeNameVersion;
path: string;
methodName?: string;
loadOptions?: ILoadOptions;
currentNodeParameters: INodeParameters;
credentials?: INodeCredentials;
},
): Promise<INodePropertyOptions[]> {
return makeRestApiRequest(context, 'GET', '/node-parameter-options', sendData);
@ -52,5 +52,10 @@ export async function getResourceLocatorResults(
context: IRestApiContext,
sendData: IResourceLocatorReqParams,
): 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 { 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> {
return makeRestApiRequest(context, 'GET', '/settings');
}
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> {
return await post(N8N_IO_BASE_URL, '/prompt', { email }, {'n8n-instance-id': instanceId, 'n8n-user-id': userId});
export async function submitContactInfo(
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> {
return await post(N8N_IO_BASE_URL, '/value-survey', params, {'n8n-instance-id': instanceId, 'n8n-user-id': userId});
export async function submitValueSurvey(
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> {
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;
}

View file

@ -9,7 +9,11 @@ export async function createTag(context: IRestApiContext, params: { name: string
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);
}

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 { get } from '@/utils';
@ -10,30 +18,64 @@ export function testHealthEndpoint(apiEndpoint: string) {
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);
}
export async function getCollections(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 getCollections(
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(
apiEndpoint: string,
query: {skip: number, limit: number, categories: number[], search: string},
query: { skip: number; limit: number; categories: number[]; search: string },
headers?: IDataObject,
): Promise<{totalWorkflows: number, workflows: ITemplatesWorkflow[]}> {
return get(apiEndpoint, '/templates/workflows', {skip: query.skip, rows: query.limit, category: stringifyArray(query.categories), search: query.search}, headers);
): Promise<{ totalWorkflows: number; workflows: ITemplatesWorkflow[] }> {
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);
}
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);
}
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);
}

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 { makeRestApiRequest } from '@/utils';
@ -10,7 +15,10 @@ export function getCurrentUser(context: IRestApiContext): Promise<IUserResponse
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);
}
@ -18,7 +26,10 @@ export async function logout(context: IRestApiContext): Promise<void> {
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);
}
@ -26,36 +37,71 @@ export function skipOwnerSetup(context: IRestApiContext): Promise<void> {
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);
}
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;
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);
}
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);
}
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);
}
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);
}
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);
}
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 } : {});
}
@ -63,14 +109,20 @@ export function getUsers(context: IRestApiContext): Promise<IUserResponse[]> {
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);
}
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`);
}
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);
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -13,9 +13,8 @@
<div v-if="!binaryData">
{{ $locale.baseText('binaryDataDisplay.noDataFoundToDisplay') }}
</div>
<BinaryDataDisplayEmbed v-else :binaryData="binaryData"/>
<BinaryDataDisplayEmbed v-else :binaryData="binaryData" />
</div>
</div>
</template>
@ -31,62 +30,62 @@ import { restApi } from '@/mixins/restApi';
import { mapStores } from 'pinia';
import { useWorkflowsStore } from '@/stores/workflows';
export default mixins(
nodeHelpers,
restApi,
)
.extend({
name: 'BinaryDataDisplay',
components: {
BinaryDataDisplayEmbed,
export default mixins(nodeHelpers, restApi).extend({
name: 'BinaryDataDisplay',
components: {
BinaryDataDisplayEmbed,
},
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;
},
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 () {
// Handle the close externally as the visible parameter is an external prop
// and is so not allowed to be changed here.
this.$emit('close');
return false;
},
},
methods: {
closeWindow() {
// Handle the close externally as the visible parameter is an external prop
// and is so not allowed to be changed here.
this.$emit('close');
return false;
},
});
},
});
</script>
<style lang="scss">
.binary-data-window {
position: absolute;
top: 50px;
@ -103,7 +102,7 @@ export default mixins(
}
.binary-data-window-wrapper {
margin-top: .5em;
margin-top: 0.5em;
padding: 0 1em;
height: calc(100% - 50px);
@ -126,7 +125,5 @@ export default mixins(
width: calc(100% - 1em);
}
}
}
</style>

View file

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

View file

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

View file

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

View file

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

View file

@ -4,11 +4,18 @@
append-to-body
:close-on-click-modal="false"
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"
>
<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>
</el-dialog>
</template>
@ -28,30 +35,21 @@ import {
WorkflowDataProxy,
} from 'n8n-workflow';
import {
PLACEHOLDER_FILLED_AT_EXECUTION_TIME,
} from '@/constants';
import { PLACEHOLDER_FILLED_AT_EXECUTION_TIME } from '@/constants';
import { CodeEditor } from './forms';
import { mapStores } from 'pinia';
import { useWorkflowsStore } from '@/stores/workflows';
import { useRootStore } from '@/stores/n8nRootStore';
import { useNDVStore } from '@/stores/ndv';
export default mixins(
genericHelpers,
workflowHelpers,
).extend({
export default mixins(genericHelpers, workflowHelpers).extend({
name: 'CodeEdit',
components: {
CodeEditor,
},
props: ['codeAutocomplete', 'parameter', 'path', 'type', 'value', 'readonly'],
computed: {
...mapStores(
useNDVStore,
useRootStore,
useWorkflowsStore,
),
...mapStores(useNDVStore, useRootStore, useWorkflowsStore),
},
methods: {
loadAutocompleteData(): string[] {
@ -65,7 +63,10 @@ export default mixins(
const workflow = this.getCurrentWorkflow();
const activeNode: INodeUi | null = this.ndvStore.activeNode;
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,
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 = {
$execution: {
@ -100,7 +107,18 @@ export default mixins(
$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 autoCompleteItems = [
@ -126,13 +144,7 @@ export default mixins(
'Interval',
];
const functionItemKeys = [
'$json',
'$binary',
'$position',
'$thisItem',
'$thisItemIndex',
];
const functionItemKeys = ['$json', '$binary', '$position', '$thisItem', '$thisItemIndex'];
const additionalKeys: string[] = [];
if (this.codeAutocomplete === 'functionItem') {
@ -142,13 +154,15 @@ export default mixins(
if (executedWorkflow && connectionInputData && connectionInputData.length) {
baseKeys.push(...additionalKeys);
} else {
additionalKeys.forEach(key => {
additionalKeys.forEach((key) => {
autoCompleteItems.push(`const ${key} = {}`);
});
}
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
@ -159,24 +173,36 @@ export default mixins(
// To not load to much data create a simple representation.
nodes[nodeName] = {
json: {} as IDataObject,
parameter: this.createSimpleRepresentation(proxy.$node[nodeName].parameter) as IDataObject,
parameter: this.createSimpleRepresentation(
proxy.$node[nodeName].parameter,
) as IDataObject,
};
try {
nodes[nodeName]!.json = this.createSimpleRepresentation(proxy.$node[nodeName].json) as IDataObject;
nodes[nodeName]!.context = this.createSimpleRepresentation(proxy.$node[nodeName].context) as IDataObject;
nodes[nodeName]!.json = this.createSimpleRepresentation(
proxy.$node[nodeName].json,
) as IDataObject;
nodes[nodeName]!.context = this.createSimpleRepresentation(
proxy.$node[nodeName].context,
) as IDataObject;
nodes[nodeName]!.runIndex = proxy.$node[nodeName].runIndex;
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(`function $jmespath(jsonDoc: object, query: string): {};`);
if (this.codeAutocomplete === 'function') {
if (connectionInputData) {
autoCompleteItems.push(`const items = ${JSON.stringify(this.createSimpleRepresentation(connectionInputData))}`);
autoCompleteItems.push(
`const items = ${JSON.stringify(
this.createSimpleRepresentation(connectionInputData),
)}`,
);
} else {
autoCompleteItems.push(`const items: {json: {[key: string]: any}}[] = []`);
}
@ -200,7 +226,29 @@ export default mixins(
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) {
return inputData;
} else if (typeof inputData === 'string') {
@ -210,10 +258,10 @@ export default mixins(
} else if (typeof inputData === 'number') {
return 1;
} else if (Array.isArray(inputData)) {
return inputData.map(value => this.createSimpleRepresentation(value));
return inputData.map((value) => this.createSimpleRepresentation(value));
} else if (typeof inputData === 'object') {
const returnData: { [key: string]: object } = {};
Object.keys(inputData).forEach(key => {
Object.keys(inputData).forEach((key) => {
// @ts-ignore
returnData[key] = this.createSimpleRepresentation(inputData[key]);
});

View file

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

View file

@ -9,7 +9,12 @@ import {
} from '@codemirror/view';
import { bracketMatching, foldGutter, indentOnInput } from '@codemirror/language';
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 type { Extension } from '@codemirror/state';

View file

@ -1,5 +1,5 @@
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

View file

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

View file

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

View file

@ -75,34 +75,47 @@ export const CODE_NODE_EDITOR_THEME = [
cursor: BASE_STYLING.diagnosticButton.cursor,
},
}),
syntaxHighlighting(HighlightStyle.define([
{
tag: tags.comment,
color: 'var(--color-code-tags-comment)',
},
{
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.keyword,
color: 'var(--color-code-tags-keyword)',
},
{
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.definition(tags.typeName), tags.definition(tags.propertyName), tags.function(tags.variableName)],
color: 'var(--color-code-tags-definition)',
},
])),
syntaxHighlighting(
HighlightStyle.define([
{
tag: tags.comment,
color: 'var(--color-code-tags-comment)',
},
{
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.keyword,
color: 'var(--color-code-tags-keyword)',
},
{
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.definition(tags.typeName),
tags.definition(tags.propertyName),
tags.function(tags.variableName),
],
color: 'var(--color-code-tags-definition)',
},
]),
),
];

View file

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

View file

@ -5,7 +5,15 @@
<n8n-text size="small">{{ $locale.baseText('collectionParameter.noProperties') }}</n8n-text>
</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">
<n8n-button
@ -16,183 +24,180 @@
:label="getPlaceholderText"
/>
<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
v-for="item in parameterOptions"
:key="item.name"
:label="$locale.nodeText().collectionOptionDisplayName(parameter, item, path)"
:value="item.name">
:value="item.name"
>
</n8n-option>
</n8n-select>
</div>
</div>
</div>
</div>
</template>
<script lang="ts">
import {
INodeUi,
IUpdateInformation,
} from '@/Interface';
import { INodeUi, IUpdateInformation } from '@/Interface';
import {
deepCopy,
INodeProperties,
INodePropertyOptions,
} from 'n8n-workflow';
import { deepCopy, INodeProperties, INodePropertyOptions } from 'n8n-workflow';
import { nodeHelpers } from '@/mixins/nodeHelpers';
import { get } from 'lodash';
import mixins from 'vue-typed-mixins';
import {Component} from "vue";
import { Component } from 'vue';
import { mapStores } from 'pinia';
import { useNDVStore } from '@/stores/ndv';
export default mixins(
nodeHelpers,
)
.extend({
name: 'CollectionParameter',
props: [
'hideDelete', // boolean
'nodeValues', // NodeParameters
'parameter', // INodeProperties
'path', // string
'values', // NodeParameters
'isReadOnly', // boolean
],
components: {
ParameterInputList: () => import('./ParameterInputList.vue') as Promise<Component>,
export default mixins(nodeHelpers).extend({
name: 'CollectionParameter',
props: [
'hideDelete', // boolean
'nodeValues', // NodeParameters
'parameter', // INodeProperties
'path', // string
'values', // NodeParameters
'isReadOnly', // boolean
],
components: {
ParameterInputList: () => import('./ParameterInputList.vue') as Promise<Component>,
},
data() {
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 () {
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');
},
getProperties (): INodeProperties[] {
const returnProperties = [];
let tempProperties;
for (const name of this.propertyNames) {
tempProperties = this.getOptionProperties(name);
if (tempProperties !== undefined) {
returnProperties.push(...tempProperties);
}
getProperties(): INodeProperties[] {
const returnProperties = [];
let tempProperties;
for (const name of this.propertyNames) {
tempProperties = this.getOptionProperties(name);
if (tempProperties !== undefined) {
returnProperties.push(...tempProperties);
}
return returnProperties;
},
// Returns all the options which should be displayed
filteredOptions (): Array<INodePropertyOptions | INodeProperties> {
return (this.parameter.options as Array<INodePropertyOptions | INodeProperties>).filter((option) => {
}
return returnProperties;
},
// Returns all the options which should be displayed
filteredOptions(): Array<INodePropertyOptions | INodeProperties> {
return (this.parameter.options as Array<INodePropertyOptions | INodeProperties>).filter(
(option) => {
return this.displayNodeParameter(option as INodeProperties);
});
},
node (): INodeUi | null {
return this.ndvStore.activeNode;
},
// Returns all the options which did not get added already
parameterOptions (): Array<INodePropertyOptions | INodeProperties> {
return (this.filteredOptions as Array<INodePropertyOptions | INodeProperties>).filter((option) => {
},
);
},
node(): INodeUi | null {
return this.ndvStore.activeNode;
},
// Returns all the options which did not get added already
parameterOptions(): Array<INodePropertyOptions | INodeProperties> {
return (this.filteredOptions as Array<INodePropertyOptions | INodeProperties>).filter(
(option) => {
return !this.propertyNames.includes(option.name);
});
},
propertyNames (): string[] {
if (this.values) {
return Object.keys(this.values);
}
return [];
},
},
);
},
methods: {
getArgument (argumentName: string): string | number | boolean | undefined {
if (this.parameter.typeOptions === undefined) {
return undefined;
propertyNames(): string[] {
if (this.values) {
return Object.keys(this.values);
}
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 undefined;
}
return properties;
},
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];
},
getOptionProperties (optionName: string): INodeProperties[] {
const properties: INodeProperties[] = [];
for (const option of this.parameter.options) {
if (option.name === optionName) {
properties.push(option);
}
}
const option = options[0];
const name = `${this.path}.${option.name}`;
return properties;
},
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;
}
let parameterData;
const option = options[0];
const name = `${this.path}.${option.name}`;
if (option.typeOptions !== undefined && option.typeOptions.multipleValues === true) {
// Multiple values are allowed
let parameterData;
if (option.typeOptions !== undefined && option.typeOptions.multipleValues === true) {
// Multiple values are allowed
let newValue;
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,
};
let newValue;
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 {
// Add a new option
parameterData = {
name,
value: deepCopy(option.default),
};
// Everything else saves them directly as an array.
newValue = get(this.nodeValues, `${this.path}.${optionName}`, []);
newValue.push(deepCopy(option.default));
}
this.$emit('valueChanged', parameterData);
this.selectedOption = undefined;
},
valueChanged (parameterData: IUpdateInformation) {
this.$emit('valueChanged', parameterData);
},
parameterData = {
name,
value: newValue,
};
} 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>
<style lang="scss">
.collection-parameter {
padding-left: var(--spacing-s);
@ -212,5 +217,4 @@ export default mixins(
padding: 0.25em 0 0.25em 1em;
}
}
</style>

View file

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

View file

@ -1,9 +1,16 @@
<template>
<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">
<Card v-for="n in (loading ? 4: 0)" :key="`loading-${n}`" :loading="loading" />
<agile
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
v-for="collection in (loading? []: collections)"
v-for="collection in loading ? [] : collections"
:key="collection.id"
:collection="collection"
@click="(e) => onCardClick(e, collection.id)"
@ -19,8 +26,8 @@
</template>
<script lang="ts">
import { PropType } from "vue";
import { ITemplatesCollection } from "@/Interface";
import { PropType } from 'vue';
import { ITemplatesCollection } from '@/Interface';
import Card from '@/components/CollectionWorkflowCard.vue';
import CollectionCard from '@/components/CollectionCard.vue';
import VueAgile from 'vue-agile';
@ -75,7 +82,7 @@ export default mixins(genericHelpers).extend({
}
},
onCardClick(event: MouseEvent, id: string) {
this.$emit('openCollection', {event, id});
this.$emit('openCollection', { event, id });
},
scrollLeft() {
if (this.listElement) {
@ -144,9 +151,21 @@ export default mixins(genericHelpers).extend({
&:after {
left: 27px;
background: linear-gradient(270deg,
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%));
background: linear-gradient(
270deg,
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;
&:after {
right: 27px;
background: linear-gradient(90deg,
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%));
background: linear-gradient(
90deg,
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>

View file

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

View file

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

View file

@ -10,7 +10,10 @@
>
<template #content>
<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">
<template>
<span v-text="getModalContent.description"></span>
@ -35,7 +38,10 @@
import Vue from 'vue';
import mixins from 'vue-typed-mixins';
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 { mapStores } from 'pinia';
import { useCommunityNodesStore } from '@/stores/communityNodes';
@ -80,8 +86,12 @@ export default mixins(showMessage).extend({
packageName: this.activePackageName,
},
}),
buttonLabel: this.$locale.baseText('settings.communityNodes.confirmModal.uninstall.buttonLabel'),
buttonLoadingLabel: this.$locale.baseText('settings.communityNodes.confirmModal.uninstall.buttonLoadingLabel'),
buttonLabel: this.$locale.baseText(
'settings.communityNodes.confirmModal.uninstall.buttonLabel',
),
buttonLoadingLabel: this.$locale.baseText(
'settings.communityNodes.confirmModal.uninstall.buttonLoadingLabel',
),
};
}
return {
@ -90,15 +100,21 @@ export default mixins(showMessage).extend({
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', {
interpolate: {
packageName: this.activePackageName,
version: this.activePackage.updateAvailable,
},
}),
buttonLabel: this.$locale.baseText('settings.communityNodes.confirmModal.update.buttonLabel'),
buttonLoadingLabel: this.$locale.baseText('settings.communityNodes.confirmModal.update.buttonLoadingLabel'),
buttonLabel: this.$locale.baseText(
'settings.communityNodes.confirmModal.update.buttonLabel',
),
buttonLoadingLabel: this.$locale.baseText(
'settings.communityNodes.confirmModal.update.buttonLoadingLabel',
),
};
},
},
@ -117,7 +133,7 @@ export default mixins(showMessage).extend({
try {
this.$telemetry.track('user started cnr package deletion', {
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_author: this.activePackage.authorName,
package_author_email: this.activePackage.authorEmail,
@ -129,7 +145,10 @@ export default mixins(showMessage).extend({
type: 'success',
});
} 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 {
this.loading = false;
this.modalBus.$emit('close');
@ -139,7 +158,7 @@ export default mixins(showMessage).extend({
try {
this.$telemetry.track('user started cnr package update', {
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_new: this.activePackage.updateAvailable,
package_author: this.activePackage.authorName,
@ -150,16 +169,22 @@ export default mixins(showMessage).extend({
await this.communityNodesStore.updatePackage(this.activePackageName);
this.$showMessage({
title: this.$locale.baseText('settings.communityNodes.messages.update.success.title'),
message: this.$locale.baseText('settings.communityNodes.messages.update.success.message', {
interpolate: {
packageName: this.activePackageName,
version: updatedVersion,
message: this.$locale.baseText(
'settings.communityNodes.messages.update.success.message',
{
interpolate: {
packageName: this.activePackageName,
version: updatedVersion,
},
},
}),
),
type: 'success',
});
} 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 {
this.loading = false;
this.modalBus.$emit('close');
@ -170,17 +195,17 @@ export default mixins(showMessage).extend({
</script>
<style module lang="scss">
.descriptionContainer {
display: flex;
margin: var(--spacing-s) 0;
}
.descriptionContainer {
display: flex;
margin: var(--spacing-s) 0;
}
.descriptionIcon {
align-self: center;
color: var(--color-text-lighter);
}
.descriptionIcon {
align-self: center;
color: var(--color-text-lighter);
}
.descriptionText {
padding: 0 var(--spacing-xs);
}
.descriptionText {
padding: 0 var(--spacing-xs);
}
</style>

View file

@ -55,10 +55,7 @@ export default mixins(workflowHelpers).extend({
};
},
computed: {
...mapStores(
useRootStore,
useSettingsStore,
),
...mapStores(useRootStore, useSettingsStore),
title(): string {
if (this.settingsStore.promptsData && this.settingsStore.promptsData.title) {
return this.settingsStore.promptsData.title;
@ -88,7 +85,9 @@ export default mixins(workflowHelpers).extend({
},
async send() {
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) {
this.$telemetry.track('User closed email modal', {

View file

@ -1,9 +1,15 @@
<template>
<div>
<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>
<div :class="$style.copyButton"><span>{{ copyButtonText }}</span></div>
<div :class="$style.copyButton">
<span>{{ copyButtonText }}</span>
</div>
</div>
</n8n-input-label>
<div v-if="hint" :class="$style.hint">{{ hint }}</div>
@ -103,7 +109,7 @@ export default mixins(copyPaste, showMessage).extend({
.collapsed {
white-space: nowrap;
overflow: hidden;
overflow: hidden;
}
.copyButton {
@ -129,5 +135,4 @@ export default mixins(copyPaste, showMessage).extend({
font-weight: var(--font-weight-regular);
word-break: normal;
}
</style>

View file

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

View file

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

View file

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

View file

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

View file

@ -1,11 +1,13 @@
<template>
<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 -->
<n8n-notice
v-if="parameter.type === 'notice'"
:content="parameter.displayName"
/>
<n8n-notice v-if="parameter.type === 'notice'" :content="parameter.displayName" />
<parameter-input-expanded
v-else
:parameter="parameter"

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -10,12 +10,20 @@
<template #content>
<div>
<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 :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')">
<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>
<div :class="$style.optionInput" v-if="operation === 'transfer'">
<n8n-input-label :label="$locale.baseText('settings.users.userToTransferTo')">
@ -29,38 +37,49 @@
</n8n-input-label>
</div>
<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>
<div :class="$style.optionInput" v-if="operation === 'delete'">
<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>
</div>
</div>
</div>
</template>
<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>
</Modal>
</template>
<script lang="ts">
import mixins from "vue-typed-mixins";
import mixins from 'vue-typed-mixins';
import { showMessage } from "@/mixins/showMessage";
import Modal from "./Modal.vue";
import Vue from "vue";
import { IUser } from "../Interface";
import { mapStores } from "pinia";
import { showMessage } from '@/mixins/showMessage';
import Modal from './Modal.vue';
import Vue from 'vue';
import { IUser } from '../Interface';
import { mapStores } from 'pinia';
import { useUsersStore } from '@/stores/users';
export default mixins(showMessage).extend({
components: {
Modal,
},
name: "DeleteUserModal",
name: 'DeleteUserModal',
props: {
modalName: {
type: String,
@ -88,17 +107,18 @@ export default mixins(showMessage).extend({
return this.userToDelete ? this.userToDelete && !this.userToDelete.firstName : false;
},
title(): string {
const user = this.userToDelete && (this.userToDelete.fullName || this.userToDelete.email) || '';
return this.$locale.baseText(
'settings.users.deleteUser',
{ interpolate: { user }},
);
const user =
(this.userToDelete && (this.userToDelete.fullName || this.userToDelete.email)) || '';
return this.$locale.baseText('settings.users.deleteUser', { interpolate: { user } });
},
enabled(): boolean {
if (this.isPending) {
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;
}
@ -128,7 +148,7 @@ export default mixins(showMessage).extend({
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') {
params.transferId = this.transferId;
}
@ -139,10 +159,9 @@ export default mixins(showMessage).extend({
if (this.transferId) {
const transferUser: IUser | null = this.usersStore.getUserById(this.transferId);
if (transferUser) {
message = this.$locale.baseText(
'settings.users.transferredToUser',
{ interpolate: { user: transferUser.fullName || '' }},
);
message = this.$locale.baseText('settings.users.transferredToUser', {
interpolate: { user: transferUser.fullName || '' },
});
}
}
@ -153,7 +172,6 @@ export default mixins(showMessage).extend({
});
this.modalBus.$emit('close');
} catch (error) {
this.$showError(error, this.$locale.baseText('settings.users.userDeletedError'));
}
@ -161,7 +179,6 @@ export default mixins(showMessage).extend({
},
},
});
</script>
<style lang="scss" module>

View file

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

View file

@ -39,9 +39,7 @@ export default Vue.extend({
window.removeEventListener('mouseup', this.onMouseUp);
},
computed: {
...mapStores(
useNDVStore,
),
...mapStores(useNDVStore),
isDragging(): boolean {
return this.ndvStore.isDraggableDragging;
},
@ -62,10 +60,17 @@ export default Vue.extend({
if (target && this.isDragging) {
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) {
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 #footer="{ close }">
<div :class="$style.footer">
<n8n-button @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" />
<n8n-button
@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>
</template>
</Modal>
</template>
<script lang="ts">
import Vue from "vue";
import mixins from "vue-typed-mixins";
import Vue from 'vue';
import mixins from 'vue-typed-mixins';
import { MAX_WORKFLOW_NAME_LENGTH, PLACEHOLDER_EMPTY_WORKFLOW_ID } from "@/constants";
import { workflowHelpers } from "@/mixins/workflowHelpers";
import { showMessage } from "@/mixins/showMessage";
import TagsDropdown from "@/components/TagsDropdown.vue";
import Modal from "./Modal.vue";
import {restApi} from "@/mixins/restApi";
import { mapStores } from "pinia";
import { useSettingsStore } from "@/stores/settings";
import { useWorkflowsStore } from "@/stores/workflows";
import { IWorkflowDataUpdate } from "@/Interface";
import { MAX_WORKFLOW_NAME_LENGTH, PLACEHOLDER_EMPTY_WORKFLOW_ID } from '@/constants';
import { workflowHelpers } from '@/mixins/workflowHelpers';
import { showMessage } from '@/mixins/showMessage';
import TagsDropdown from '@/components/TagsDropdown.vue';
import Modal from './Modal.vue';
import { restApi } from '@/mixins/restApi';
import { mapStores } from 'pinia';
import { useSettingsStore } from '@/stores/settings';
import { useWorkflowsStore } from '@/stores/workflows';
import { IWorkflowDataUpdate } from '@/Interface';
export default mixins(showMessage, workflowHelpers, restApi).extend({
components: { TagsDropdown, Modal },
name: "DuplicateWorkflow",
props: ["modalName", "isActive", "data"],
name: 'DuplicateWorkflow',
props: ['modalName', 'isActive', 'data'],
data() {
const currentTagIds = this.data.tags;
@ -74,10 +85,7 @@ export default mixins(showMessage, workflowHelpers, restApi).extend({
this.$nextTick(() => this.focusOnNameInput());
},
computed: {
...mapStores(
useSettingsStore,
useWorkflowsStore,
),
...mapStores(useSettingsStore, useWorkflowsStore),
},
watch: {
isActive(active) {
@ -112,7 +120,7 @@ export default mixins(showMessage, workflowHelpers, restApi).extend({
this.$showMessage({
title: this.$locale.baseText('duplicateWorkflowDialog.errors.missingName.title'),
message: this.$locale.baseText('duplicateWorkflowDialog.errors.missingName.message'),
type: "error",
type: 'error',
});
return;
@ -125,10 +133,14 @@ export default mixins(showMessage, workflowHelpers, restApi).extend({
try {
let workflowToUpdate: IWorkflowDataUpdate | undefined;
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;
this.removeForeignCredentialsFromWorkflow(workflowToUpdate, this.credentialsStore.allCredentials);
this.removeForeignCredentialsFromWorkflow(
workflowToUpdate,
this.credentialsStore.allCredentials,
);
}
const saved = await this.saveAsNewWorkflow({
@ -166,7 +178,7 @@ export default mixins(showMessage, workflowHelpers, restApi).extend({
}
},
closeDialog(): void {
this.modalBus.$emit("close");
this.modalBus.$emit('close');
},
},
});

View file

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

View file

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

View file

@ -1,62 +1,54 @@
<template>
<span>
{{time}}
</span>
<span>
{{ time }}
</span>
</template>
<script lang="ts">
import { genericHelpers } from '@/mixins/genericHelpers';
import mixins from 'vue-typed-mixins';
export default mixins(
genericHelpers,
)
.extend({
name: 'ExecutionTime',
props: [
'startTime',
],
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);
export default mixins(genericHelpers).extend({
name: 'ExecutionTime',
props: ['startTime'],
computed: {
time(): string {
if (!this.startTime) {
return '...';
}
const msPassed = this.nowTime - new Date(this.startTime).getTime();
return this.displayTimer(msPassed);
},
methods: {
setNow () {
this.nowTime = (new Date()).getTime();
},
},
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);
}
},
methods: {
setNow() {
this.nowTime = new Date().getTime();
},
});
},
});
</script>
<style lang="scss">
// .data-display-wrapper {
// }
</style>

View file

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

View file

@ -10,23 +10,48 @@
>
<router-link
: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">
<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">
<n8n-spinner v-if="executionUIDetails.name === 'running'" 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">
<n8n-spinner
v-if="executionUIDetails.name === 'running'"
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') }}
<execution-time :start-time="execution.startedAt"/>
<execution-time :start-time="execution.startedAt" />
</n8n-text>
<n8n-text 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
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>
</div>
<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 }}
</n8n-text>
</div>
@ -44,7 +69,7 @@
:class="[$style.icon, $style.manual]"
:title="$locale.baseText('executionsList.manual')"
icon="flask"
/>
/>
</div>
</router-link>
</div>
@ -59,11 +84,7 @@ import { showMessage } from '@/mixins/showMessage';
import { restApi } from '@/mixins/restApi';
import ExecutionTime from '@/components/ExecutionTime.vue';
export default mixins(
executionHelpers,
showMessage,
restApi,
).extend({
export default mixins(executionHelpers, showMessage, restApi).extend({
name: 'execution-card',
components: {
ExecutionTime,
@ -86,8 +107,14 @@ export default mixins(
computed: {
retryExecutionActions(): object[] {
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 {
@ -119,9 +146,12 @@ export default mixins(
}
}
& + &.active { padding-top: var(--spacing-2xs); }
& + &.active {
padding-top: var(--spacing-2xs);
}
&:hover, &.active {
&:hover,
&.active {
.executionLink {
background-color: var(--color-foreground-base);
}
@ -132,34 +162,47 @@ export default mixins(
position: relative;
top: 1px;
}
&, & .executionLink {
&,
& .executionLink {
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 {
&, & .executionLink {
&,
& .executionLink {
border-left: var(--spacing-4xs) var(--border-style-base) hsl(var(--color-success-h), 60%, 70%);
}
}
&.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 {
&, & .executionLink {
&,
& .executionLink {
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 {
&, & .executionLink {
&,
& .executionLink {
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);
border-radius: var(--border-radius-base);
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 {
.icon, .statusLabel {
color: var(--color-text-base);;
.icon,
.statusLabel {
color: var(--color-text-base);
}
}
}

View file

@ -1,5 +1,8 @@
<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">
<n8n-spinner type="ring" />
</div>
@ -11,32 +14,66 @@
</n8n-button>
</div>
<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>
<n8n-text size="large" color="text-base" :bold="true">{{ executionUIDetails.startTime }}</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 size="large" color="text-base" :bold="true">{{
executionUIDetails.startTime
}}</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">
{{ $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 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 v-else-if="executionUIDetails.name === 'waiting'" color="text-base" size="medium">
| ID#{{ activeExecution.id }}
</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') }}
<router-link
: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 }}
</router-link>
</n8n-text>
</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">
<n8n-icon-button
size="large"
@ -57,10 +94,21 @@
</el-dropdown-menu>
</template>
</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>
<workflow-preview mode="execution" loaderType="spinner" :executionId="executionId" :executionMode="executionMode"/>
<workflow-preview
mode="execution"
loaderType="spinner"
:executionId="executionId"
:executionMode="executionMode"
/>
</div>
</template>
@ -87,9 +135,7 @@ export default mixins(restApi, showMessage, executionHelpers).extend({
};
},
computed: {
...mapStores(
useUIStore,
),
...mapStores(useUIStore),
executionUIDetails(): IExecutionUIData | null {
return this.activeExecution ? this.getExecutionUIDetails(this.activeExecution) : null;
},
@ -122,7 +168,7 @@ export default mixins(restApi, showMessage, executionHelpers).extend({
},
onRetryButtonBlur(event: FocusEvent): void {
// 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) {
retryDropdown.hide();
}
@ -132,7 +178,6 @@ export default mixins(restApi, showMessage, executionHelpers).extend({
</script>
<style module lang="scss">
.previewContainer {
height: calc(100% - $header-height);
overflow: hidden;
@ -148,7 +193,9 @@ export default mixins(restApi, showMessage, executionHelpers).extend({
transition: all 150ms ease-in-out;
pointer-events: none;
& * { pointer-events: all; }
& * {
pointer-events: all;
}
&.sidebarCollapsed {
width: calc(100% - 375px);
@ -163,10 +210,19 @@ export default mixins(restApi, showMessage, executionHelpers).extend({
}
}
.running, .spinner { color: var(--color-warning); }
.waiting { color: var(--color-secondary); }
.success { color: var(--color-success); }
.error { color: var(--color-danger); }
.running,
.spinner {
color: var(--color-warning);
}
.waiting {
color: var(--color-secondary);
}
.success {
color: var(--color-success);
}
.error {
color: var(--color-danger);
}
.runningInfo {
display: flex;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,6 +1,13 @@
<template>
<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-col :span="8">
<div class="header-side-menu">
@ -58,10 +65,8 @@
/>
</div>
</div>
</el-col>
</el-row>
</el-dialog>
</div>
</template>
@ -87,25 +92,15 @@ import { useNDVStore } from '@/stores/ndv';
import type { Resolvable, Segment } from './ExpressionEditorModal/types';
export default mixins(
externalHooks,
genericHelpers,
debounceHelper,
).extend({
export default mixins(externalHooks, genericHelpers, debounceHelper).extend({
name: 'ExpressionEdit',
props: [
'dialogVisible',
'parameter',
'path',
'value',
'eventSource',
],
props: ['dialogVisible', 'parameter', 'path', 'value', 'eventSource'],
components: {
ExpressionModalInput,
ExpressionModalOutput,
VariableSelector,
},
data () {
data() {
return {
displayValue: '',
latestValue: '',
@ -114,13 +109,10 @@ export default mixins(
};
},
computed: {
...mapStores(
useNDVStore,
useWorkflowsStore,
),
...mapStores(useNDVStore, useWorkflowsStore),
},
methods: {
valueChanged ({ value, segments }: { value: string, segments: Segment[] }, forceUpdate = false) {
valueChanged({ value, segments }: { value: string; segments: Segment[] }, forceUpdate = false) {
this.latestValue = value;
this.segments = segments;
@ -132,11 +124,11 @@ export default mixins(
}
},
updateDisplayValue () {
updateDisplayValue() {
this.displayValue = this.latestValue;
},
closeDialog () {
closeDialog() {
if (this.latestValue !== this.value) {
// Handle the close externally as the visible parameter is an external prop
// and is so not allowed to be changed here.
@ -146,9 +138,13 @@ export default mixins(
return false;
},
itemSelected (eventData: IVariableItemSelected) {
itemSelected(eventData: IVariableItemSelected) {
(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: {
event_version: string;
@ -162,11 +158,11 @@ export default mixins(
node_name: string;
} = {
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,
is_immediate_input: false,
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) {
@ -184,47 +180,63 @@ export default mixins(
if (splitVar[0].startsWith('$node')) {
const sourceNodeName = splitVar[0].split('"')[1];
trackProperties.node_type_source = this.workflowsStore.getNodeByName(sourceNodeName)?.type;
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;
trackProperties.node_type_source =
this.workflowsStore.getNodeByName(sourceNodeName)?.type;
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')) {
trackProperties.parameter_name_source = splitVar[1].split('"')[1];
}
} else {
trackProperties.is_immediate_input = true;
if(splitVar[0].startsWith('$parameter')) {
if (splitVar[0].startsWith('$parameter')) {
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: {
dialogVisible (newValue) {
dialogVisible(newValue) {
this.displayValue = this.value;
this.latestValue = this.value;
const 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 });
const 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) {
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) => {
return Object.getOwnPropertyNames(error).reduce<Record<string, unknown>>((acc, key) => {
// @ts-ignore
return acc[key] = error[key], acc;
return (acc[key] = error[key]), acc;
}, {});
};
const telemetryPayload = {
empty_expression: (this.value === '=') || (this.value === '={{}}') || !this.value,
empty_expression: this.value === '=' || this.value === '={{}}' || !this.value,
workflow_id: this.workflowsStore.workflowId,
source: this.eventSource,
session_id: this.ndvStore.sessionId,
@ -233,12 +245,15 @@ export default mixins(
node_type: this.ndvStore.activeNode?.type ?? '',
handlebar_count: resolvables.length,
handlebar_error_count: errorResolvables.length,
full_errors: errorResolvables.map(errorResolvable => {
full_errors: errorResolvables.map((errorResolvable) => {
return errorResolvable.fullError
? { ...exposeErrorProperties(errorResolvable.fullError), stack: errorResolvable.fullError.stack }
? {
...exposeErrorProperties(errorResolvable.fullError),
stack: errorResolvable.fullError.stack,
}
: 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);
@ -255,7 +270,7 @@ export default mixins(
font-weight: bold;
padding: 0 0 0.5em 0.2em;
display: flex;
justify-content: space-between;
justify-content: space-between;
.hint {
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.
const parser = lr.LRParser.deserialize({
version: 14,
states: "nQQOPOOOOOO'#Cc'#CcOOOO'#Ca'#CaQQOPOOOOOO-E6_-E6_",
stateData: "]~OQPORPOSPO~O",
goto: "cWPPPPPXP_QRORSRTQOR",
nodeNames: "⚠ Program Plaintext Resolvable BrokenResolvable",
maxTerm: 7,
skippedNodes: [0],
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",
tokenizers: [0],
topRules: {"Program":[0,1]},
tokenPrec: 0
version: 14,
states: "nQQOPOOOOOO'#Cc'#CcOOOO'#Ca'#CaQQOPOOOOOO-E6_-E6_",
stateData: ']~OQPORPOSPO~O',
goto: 'cWPPPPPXP_QRORSRTQOR',
nodeNames: '⚠ Program Plaintext Resolvable BrokenResolvable',
maxTerm: 7,
skippedNodes: [0],
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',
tokenizers: [0],
topRules: { Program: [0, 1] },
tokenPrec: 0,
});
const parserWithMetaData = parser.configure({
props: [
language.foldNodeProp.add({
Application: language.foldInside,
}),
highlight.styleTags({
OpenMarker: highlight.tags.brace,
CloseMarker: highlight.tags.brace,
Plaintext: highlight.tags.content,
Resolvable: highlight.tags.string,
BrokenResolvable: highlight.tags.className,
}),
],
props: [
language.foldNodeProp.add({
Application: language.foldInside,
}),
highlight.styleTags({
OpenMarker: highlight.tags.brace,
CloseMarker: highlight.tags.brace,
Plaintext: highlight.tags.content,
Resolvable: highlight.tags.string,
BrokenResolvable: highlight.tags.className,
}),
],
});
const n8nExpressionLanguage = language.LRLanguage.define({
parser: parserWithMetaData,
languageData: {
commentTokens: { line: ";" },
},
parser: parserWithMetaData,
languageData: {
commentTokens: { line: ';' },
},
});
const completions = n8nExpressionLanguage.data.of({
autocomplete: autocomplete.completeFromList([
{ label: "abcdefg", type: "keyword" },
]),
autocomplete: autocomplete.completeFromList([{ label: 'abcdefg', type: 'keyword' }]),
});
function n8nExpression() {
return new language.LanguageSupport(n8nExpressionLanguage, [completions]);
return new language.LanguageSupport(n8nExpressionLanguage, [completions]);
}
exports.n8nExpression = n8nExpression;

View file

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

View file

@ -1,5 +1,5 @@
import { LRLanguage, LanguageSupport } from "@codemirror/language";
declare const parserWithMetaData: import("@lezer/lr").LRParser;
import { LRLanguage, LanguageSupport } from '@codemirror/language';
declare const parserWithMetaData: import('@lezer/lr').LRParser;
declare const n8nExpressionLanguage: LRLanguage;
declare function n8nExpression(): LanguageSupport;
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.
const parser = LRParser.deserialize({
version: 14,
states: "nQQOPOOOOOO'#Cc'#CcOOOO'#Ca'#CaQQOPOOOOOO-E6_-E6_",
stateData: "]~OQPORPOSPO~O",
goto: "cWPPPPPXP_QRORSRTQOR",
nodeNames: "⚠ Program Plaintext Resolvable BrokenResolvable",
maxTerm: 7,
skippedNodes: [0],
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",
tokenizers: [0],
topRules: {"Program":[0,1]},
tokenPrec: 0
version: 14,
states: "nQQOPOOOOOO'#Cc'#CcOOOO'#Ca'#CaQQOPOOOOOO-E6_-E6_",
stateData: ']~OQPORPOSPO~O',
goto: 'cWPPPPPXP_QRORSRTQOR',
nodeNames: '⚠ Program Plaintext Resolvable BrokenResolvable',
maxTerm: 7,
skippedNodes: [0],
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',
tokenizers: [0],
topRules: { Program: [0, 1] },
tokenPrec: 0,
});
const parserWithMetaData = parser.configure({
props: [
foldNodeProp.add({
Application: foldInside,
}),
styleTags({
OpenMarker: tags.brace,
CloseMarker: tags.brace,
Plaintext: tags.content,
Resolvable: tags.string,
BrokenResolvable: tags.className,
}),
],
props: [
foldNodeProp.add({
Application: foldInside,
}),
styleTags({
OpenMarker: tags.brace,
CloseMarker: tags.brace,
Plaintext: tags.content,
Resolvable: tags.string,
BrokenResolvable: tags.className,
}),
],
});
const n8nExpressionLanguage = LRLanguage.define({
parser: parserWithMetaData,
languageData: {
commentTokens: { line: ";" },
},
parser: parserWithMetaData,
languageData: {
commentTokens: { line: ';' },
},
});
const completions = n8nExpressionLanguage.data.of({
autocomplete: completeFromList([
{ label: "abcdefg", type: "keyword" },
]),
autocomplete: completeFromList([{ label: 'abcdefg', type: 'keyword' }]),
});
function n8nExpression() {
return new LanguageSupport(n8nExpressionLanguage, [completions]);
return new LanguageSupport(n8nExpressionLanguage, [completions]);
}
export { n8nExpression, n8nExpressionLanguage, parserWithMetaData };

View file

@ -2,7 +2,7 @@
<div v-if="this.featureInfo" :class="[$style.container]">
<div v-if="showTitle" class="mb-2xl">
<n8n-heading size="2xlarge">
{{$locale.baseText(featureInfo.featureName)}}
{{ $locale.baseText(featureInfo.featureName) }}
</n8n-heading>
</div>
<div v-if="featureInfo.infoText" class="mb-l">
@ -15,11 +15,13 @@
<div :class="$style.actionBoxContainer">
<n8n-action-box
: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"
>
<template #heading>
<span v-html="$locale.baseText(featureInfo.actionBoxTitle)"/>
<span v-html="$locale.baseText(featureInfo.actionBoxTitle)" />
</template>
</n8n-action-box>
</div>
@ -27,7 +29,7 @@
</template>
<script lang="ts">
import {IFakeDoor} from '@/Interface';
import { IFakeDoor } from '@/Interface';
import { useRootStore } from '@/stores/n8nRootStore';
import { useSettingsStore } from '@/stores/settings';
import { useUIStore } from '@/stores/ui';
@ -48,12 +50,7 @@ export default Vue.extend({
},
},
computed: {
...mapStores(
useRootStore,
useSettingsStore,
useUIStore,
useUsersStore,
),
...mapStores(useRootStore, useSettingsStore, useUIStore, useUsersStore),
userId(): string {
return this.usersStore.currentUserId || '';
},
@ -67,8 +64,13 @@ export default Vue.extend({
methods: {
openLinkPage() {
if (this.featureInfo) {
window.open(`${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});
window.open(
`${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;
}
</style>

View file

@ -1,7 +1,9 @@
<template>
<div @keydown.stop class="fixed-collection-parameter">
<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
@ -10,7 +12,7 @@
class="fixed-collection-parameter-property"
>
<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)"
:underline="true"
size="small"
@ -39,7 +41,7 @@
@click="moveOptionUp(property.name, index)"
/>
<font-awesome-icon
v-if="index !== (mutableValues[property.name].length - 1)"
v-if="index !== mutableValues[property.name].length - 1"
icon="angle-down"
class="clickable"
:title="$locale.baseText('fixedCollectionParameter.moveDown')"
@ -110,10 +112,8 @@
</template>
<script lang="ts">
import Vue, { Component, PropType } from "vue";
import {
IUpdateInformation,
} from '@/Interface';
import Vue, { Component, PropType } from 'vue';
import { IUpdateInformation } from '@/Interface';
import {
INodeParameters,
@ -127,187 +127,213 @@ import {
import { get } from 'lodash';
export default Vue.extend({
name: 'FixedCollectionParameter',
props: {
nodeValues: {
type: Object as PropType<Record<string, INodeParameters[]>>,
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,
},
name: 'FixedCollectionParameter',
props: {
nodeValues: {
type: Object as PropType<Record<string, INodeParameters[]>>,
required: true,
},
components: {
ParameterInputList: () => import('./ParameterInputList.vue') as Promise<Component>,
parameter: {
type: Object as PropType<INodeProperties>,
required: true,
},
data() {
return {
selectedOption: undefined,
mutableValues: {} as Record<string, INodeParameters[]>,
};
path: {
type: String,
required: true,
},
watch: {
values: {
handler(newValues: Record<string, INodeParameters[]>) {
this.mutableValues = deepCopy(newValues);
},
deep: true,
values: {
type: Object as PropType<Record<string, INodeParameters[]>>,
default: () => ({}),
},
isReadOnly: {
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: {
getPlaceholderText(): string {
const placeholder = this.$locale.nodeText().placeholder(this.parameter, this.path);
return placeholder ? placeholder : this.$locale.baseText('fixedCollectionParameter.choose');
},
getProperties(): INodePropertyCollection[] {
const returnProperties = [];
let tempProperties;
for (const name of this.propertyNames) {
tempProperties = this.getOptionProperties(name);
if (tempProperties !== undefined) {
returnProperties.push(tempProperties);
}
getProperties(): INodePropertyCollection[] {
const returnProperties = [];
let tempProperties;
for (const name of this.propertyNames) {
tempProperties = this.getOptionProperties(name);
if (tempProperties !== undefined) {
returnProperties.push(tempProperties);
}
return returnProperties;
},
multipleValues(): boolean {
return !!this.parameter.typeOptions?.multipleValues;
},
}
return returnProperties;
},
multipleValues(): boolean {
return !!this.parameter.typeOptions?.multipleValues;
},
parameterOptions(): INodePropertyCollection[] {
if (this.multipleValues && isINodePropertyCollectionList(this.parameter.options)) {
return this.parameter.options;
}
parameterOptions(): INodePropertyCollection[] {
if (this.multipleValues && isINodePropertyCollectionList(this.parameter.options)) {
return this.parameter.options;
}
return (this.parameter.options as INodePropertyCollection[]).filter((option) => {
return !this.propertyNames.includes(option.name);
return (this.parameter.options as INodePropertyCollection[]).filter((option) => {
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,
});
},
propertyNames(): string[] {
return Object.keys(this.mutableValues || {});
},
sortable(): boolean {
return !!this.parameter.typeOptions?.sortable;
},
} else {
// it's the only option, so remove the whole type
this.$emit('valueChanged', {
name: this.getPropertyPath(optionName),
value: undefined,
});
}
},
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 {
// 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;
}
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;
},
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);
},
}
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);
},
},
});
</script>
<style scoped lang="scss">

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -21,17 +21,38 @@
@runChange="onRunIndexChange"
@tableMounted="$emit('tableMounted', $event)"
data-test-id="ndv-input-panel"
>
>
<template #header>
<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>
<span :class="$style.title">{{ $locale.baseText('ndv.input') }}</span>
</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;
<span v-if="getMultipleNodesText(node.name)">{{ getMultipleNodesText(node.name) }}</span>
<span v-else>{{ $locale.baseText('ndv.input.nodeDistance', {adjustToNumber: node.depth}) }}</span>
<span v-if="getMultipleNodesText(node.name)">{{
getMultipleNodesText(node.name)
}}</span>
<span v-else>{{
$locale.baseText('ndv.input.nodeDistance', { adjustToNumber: node.depth })
}}</span>
</n8n-option>
</n8n-select>
<span v-else :class="$style.title">{{ $locale.baseText('ndv.input') }}</span>
@ -40,12 +61,31 @@
<template #node-not-run>
<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-tooltip v-if="!readOnly" :manual="true" :value="showDraggableHint && showDraggableHintWithDelay">
<n8n-text tag="div" :bold="true" color="text-dark" size="large">{{
$locale.baseText('ndv.input.noOutputData.title')
}}</n8n-text>
<n8n-tooltip
v-if="!readOnly"
:manual="true"
:value="showDraggableHint && showDraggableHintWithDelay"
>
<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>
<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-text v-if="!readOnly" tag="div" size="small">
{{ $locale.baseText('ndv.input.noOutputData.hint') }}
@ -55,18 +95,26 @@
<div>
<WireMeUp />
</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">
{{ $locale.baseText('ndv.input.notConnected.message') }}
<a href="https://docs.n8n.io/workflows/connections/" target="_blank" @click="onConnectionHelpClick">
{{$locale.baseText('ndv.input.notConnected.learnMore')}}
<a
href="https://docs.n8n.io/workflows/connections/"
target="_blank"
@click="onConnectionHelpClick"
>
{{ $locale.baseText('ndv.input.notConnected.learnMore') }}
</a>
</n8n-text>
</div>
</template>
<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>
</RunData>
</template>
@ -79,15 +127,20 @@ import { workflowHelpers } from '@/mixins/workflowHelpers';
import mixins from 'vue-typed-mixins';
import NodeExecuteButton from './NodeExecuteButton.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 { useWorkflowsStore } from '@/stores/workflows';
import { useNDVStore } from '@/stores/ndv';
import { useNodeTypesStore } from '@/stores/nodeTypes';
export default mixins(
workflowHelpers,
).extend({
export default mixins(workflowHelpers).extend({
name: 'InputPanel',
components: { RunData, NodeExecuteButton, WireMeUp },
props: {
@ -100,8 +153,7 @@ export default mixins(
linkedRuns: {
type: Boolean,
},
workflow: {
},
workflow: {},
canLinkRuns: {
type: Boolean,
},
@ -123,11 +175,7 @@ export default mixins(
};
},
computed: {
...mapStores(
useNodeTypesStore,
useNDVStore,
useWorkflowsStore,
),
...mapStores(useNodeTypesStore, useNDVStore, useWorkflowsStore),
focusedMappableInput(): string {
return this.ndvStore.focusedMappableInput;
},
@ -135,7 +183,12 @@ export default mixins(
return window.localStorage.getItem(LOCAL_STORAGE_MAPPING_FLAG) === 'true';
},
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)) {
return false;
}
@ -148,69 +201,86 @@ export default mixins(
}
const triggeredNode = this.workflowsStore.executedNode;
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;
}
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;
},
workflowRunning (): boolean {
workflowRunning(): boolean {
return this.uiStore.isActionActive('workflowRunning');
},
currentWorkflow(): Workflow {
return this.workflow as Workflow;
},
activeNode (): INodeUi | null {
activeNode(): INodeUi | null {
return this.ndvStore.activeNode;
},
currentNode (): INodeUi | null {
currentNode(): INodeUi | null {
return this.workflowsStore.getNodeByName(this.currentNodeName);
},
connectedCurrentNodeOutputs(): number[] | undefined {
const search = this.parentNodes.find(({name}) => name === this.currentNodeName);
const search = this.parentNodes.find(({ name }) => name === this.currentNodeName);
if (search) {
return search.indicies;
}
return undefined;
},
parentNodes (): IConnectedNode[] {
parentNodes(): IConnectedNode[] {
if (!this.activeNode) {
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 {
const node = this.parentNodes.find((node) => this.currentNode && node.name === this.currentNode.name);
return node ? node.depth: -1;
currentNodeDepth(): number {
const node = this.parentNodes.find(
(node) => this.currentNode && node.name === this.currentNode.name,
);
return node ? node.depth : -1;
},
activeNodeType () : INodeTypeDescription | null {
activeNodeType(): INodeTypeDescription | null {
if (!this.activeNode) return null;
return this.nodeTypesStore.getNodeType(this.activeNode.type, this.activeNode.typeVersion);
},
isMultiInputNode (): boolean {
isMultiInputNode(): boolean {
return this.activeNodeType !== null && this.activeNodeType.inputs.length > 1;
},
},
methods: {
getMultipleNodesText(nodeName?: string):string {
if(
getMultipleNodesText(nodeName?: string): string {
if (
!nodeName ||
!this.isMultiInputNode ||
!this.activeNode ||
this.activeNodeType === null ||
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
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;
}, []);
@ -222,7 +292,7 @@ export default mixins(
this.activeNodeType.inputNames[inputIndex],
);
if(connectedInputs.length === 0) return '';
if (connectedInputs.length === 0) return '';
return `(${connectedInputs.join(' & ')})`;
},
@ -281,11 +351,12 @@ export default mixins(
if (this.showDraggableHintWithDelay) {
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);
}
else if (!curr) {
} else if (!curr) {
this.showDraggableHintWithDelay = false;
}
},

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,10 +1,15 @@
<template>
<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">
<ExecutionDetails v-if="isExecutionPage" />
<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>
@ -16,7 +21,12 @@ import { pushConnection } from '@/mixins/pushConnection';
import WorkflowDetails from '@/components/MainHeader/WorkflowDetails.vue';
import ExecutionDetails from '@/components/MainHeader/ExecutionDetails/ExecutionDetails.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 { workflowHelpers } from '@/mixins/workflowHelpers';
import { Route } from 'vue-router';
@ -24,121 +34,125 @@ import { mapStores } from 'pinia';
import { useUIStore } from '@/stores/ui';
import { useNDVStore } from '@/stores/ndv';
export default mixins(
pushConnection,
workflowHelpers,
).extend({
name: 'MainHeader',
components: {
WorkflowDetails,
ExecutionDetails,
TabBar,
export default mixins(pushConnection, workflowHelpers).extend({
name: 'MainHeader',
components: {
WorkflowDetails,
ExecutionDetails,
TabBar,
},
data() {
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() {
return {
activeHeaderTab: MAIN_HEADER_TABS.WORKFLOW,
workflowToReturnTo: '',
dirtyState: false,
};
isExecutionPage(): boolean {
return this.$route.name === VIEWS.EXECUTION;
},
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') },
];
},
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;
},
activeNode(): INodeUi | null {
return this.ndvStore.activeNode;
},
mounted() {
this.dirtyState = this.uiStore.stateIsDirty;
this.syncTabsWithRoute(this.$route);
// Initialize the push connection
this.pushConnect();
hideMenuBar(): boolean {
return Boolean(this.activeNode && this.activeNode.type !== STICKY_NODE_TYPE);
},
beforeDestroy() {
this.pushDisconnect();
workflowName(): string {
return this.workflowsStore.workflowName;
},
watch: {
$route (to, from){
this.syncTabsWithRoute(to);
},
currentWorkflow(): string {
return this.$route.params.name || this.workflowsStore.workflowId;
},
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({
onWorkflowPage(): boolean {
return (
this.$route.meta &&
(this.$route.meta.nodeView || this.$route.meta.keepWorkflowAlive === true)
);
},
activeExecution(): IExecutionsSummary {
return this.workflowsStore.activeWorkflowExecution as IExecutionsSummary;
},
},
mounted() {
this.dirtyState = this.uiStore.stateIsDirty;
this.syncTabsWithRoute(this.$route);
// Initialize the push connection
this.pushConnect();
},
beforeDestroy() {
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,
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;
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({
} 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;
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,
params: { name: routeWorkflowId, executionId: this.activeExecution.id },
}).catch(()=>{});;
} else {
this.$router.push({ name: VIEWS.EXECUTION_HOME, params: { name: routeWorkflowId } });
}
// this.modalBus.$emit('closeAll');
this.activeHeaderTab = MAIN_HEADER_TABS.EXECUTIONS;
break;
default:
break;
}
},
})
.catch(() => {});
} else {
this.$router.push({ name: VIEWS.EXECUTION_HOME, params: { name: routeWorkflowId } });
}
// this.modalBus.$emit('closeAll');
this.activeHeaderTab = MAIN_HEADER_TABS.EXECUTIONS;
break;
default:
break;
}
},
});
},
});
</script>
<style lang="scss">

View file

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

View file

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

View file

@ -1,51 +1,88 @@
<template>
<div id="side-menu" :class="{
['side-menu']: true,
[$style.sideMenu]: true,
[$style.sideMenuCollapsed]: isCollapsed
}">
<div
id="side-menu"
:class="{
['side-menu']: true,
[$style.sideMenu]: true,
[$style.sideMenuCollapsed]: isCollapsed,
}"
>
<div
id="collapse-change-button"
:class="{ ['clickable']: true, [$style.sideMenuCollapseButton]: true, [$style.expandedButton]: !isCollapsed }"
@click="toggleCollapse">
</div>
:class="{
['clickable']: true,
[$style.sideMenuCollapseButton]: true,
[$style.expandedButton]: !isCollapsed,
}"
@click="toggleCollapse"
></div>
<n8n-menu :items="mainMenuItems" :collapsed="isCollapsed" @select="handleSelect">
<template #header>
<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>
</template>
<template #menuSuffix v-if="hasVersionUpdates">
<div :class="$style.updates" @click="openUpdatesPanel">
<div :class="$style.giftContainer">
<GiftNotificationIcon />
</div>
<n8n-text :class="{['ml-xs']: true, [$style.expanded]: fullyExpanded }" color="text-base">
{{ nextVersions.length > 99 ? '99+' : nextVersions.length}} update{{nextVersions.length > 1 ? 's' : '' }}
</n8n-text>
<div :class="$style.giftContainer">
<GiftNotificationIcon />
</div>
<n8n-text
:class="{ ['ml-xs']: true, [$style.expanded]: fullyExpanded }"
color="text-base"
>
{{ nextVersions.length > 99 ? '99+' : nextVersions.length }} update{{
nextVersions.length > 1 ? 's' : ''
}}
</n8n-text>
</div>
</template>
<template #footer v-if="showUserArea">
<div :class="$style.userArea">
<div class="ml-3xs">
<!-- This dropdown is only enabled when sidebar is collapsed -->
<el-dropdown :disabled="!isCollapsed" 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" />
<el-dropdown
:disabled="!isCollapsed"
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>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="settings">{{ $locale.baseText('settings') }}</el-dropdown-item>
<el-dropdown-item command="logout">{{ $locale.baseText('auth.signout') }}</el-dropdown-item>
<el-dropdown-item command="settings">{{
$locale.baseText('settings')
}}</el-dropdown-item>
<el-dropdown-item command="logout">{{
$locale.baseText('auth.signout')
}}</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
<div :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
: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 :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>
</template>
@ -54,11 +91,7 @@
</template>
<script lang="ts">
import {
IExecutionResponse,
IMenuItem,
IVersion,
} from '../Interface';
import { IExecutionResponse, IMenuItem, IVersion } from '../Interface';
import ExecutionsList from '@/components/ExecutionsList.vue';
import GiftNotificationIcon from './GiftNotificationIcon.vue';
@ -102,372 +135,379 @@ export default mixins(
workflowRun,
userHelpers,
debounceHelper,
)
.extend({
name: 'MainSidebar',
components: {
ExecutionsList,
GiftNotificationIcon,
WorkflowSettings,
).extend({
name: 'MainSidebar',
components: {
ExecutionsList,
GiftNotificationIcon,
WorkflowSettings,
},
data() {
return {
// @ts-ignore
basePath: '',
fullyExpanded: false,
};
},
computed: {
...mapStores(
useRootStore,
useSettingsStore,
useUIStore,
useUsersStore,
useVersionsStore,
useWorkflowsStore,
),
hasVersionUpdates(): boolean {
return this.versionsStore.hasVersionUpdates;
},
data () {
return {
// @ts-ignore
basePath: '',
fullyExpanded: false,
};
nextVersions(): IVersion[] {
return this.versionsStore.nextVersions;
},
computed: {
...mapStores(
useRootStore,
useSettingsStore,
useUIStore,
useUsersStore,
useVersionsStore,
useWorkflowsStore,
),
hasVersionUpdates(): boolean {
return this.versionsStore.hasVersionUpdates;
},
nextVersions(): IVersion[] {
return this.versionsStore.nextVersions;
},
isCollapsed(): boolean {
return this.uiStore.sidebarMenuCollapsed;
},
canUserAccessSettings(): boolean {
const accessibleRoute = this.findFirstAccessibleSettingsRoute();
return accessibleRoute !== null;
},
showUserArea(): boolean {
return this.settingsStore.isUserManagementEnabled && this.usersStore.canUserAccessSidebarUserInfo && this.usersStore.currentUser !== null;
},
workflowExecution (): IExecutionResponse | null {
return this.workflowsStore.getWorkflowExecution;
},
userMenuItems (): object[] {
return [
{
id: 'settings',
label: this.$locale.baseText('settings'),
},
{
id: 'logout',
label: this.$locale.baseText('auth.signout'),
},
];
},
mainMenuItems (): IMenuItem[] {
const items: IMenuItem[] = [];
const injectedItems = this.uiStore.sidebarMenuItems;
isCollapsed(): boolean {
return this.uiStore.sidebarMenuCollapsed;
},
canUserAccessSettings(): boolean {
const accessibleRoute = this.findFirstAccessibleSettingsRoute();
return accessibleRoute !== null;
},
showUserArea(): boolean {
return (
this.settingsStore.isUserManagementEnabled &&
this.usersStore.canUserAccessSidebarUserInfo &&
this.usersStore.currentUser !== null
);
},
workflowExecution(): IExecutionResponse | null {
return this.workflowsStore.getWorkflowExecution;
},
userMenuItems(): object[] {
return [
{
id: 'settings',
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) {
for(const item of injectedItems) {
items.push(
{
id: item.id,
// @ts-ignore
icon: item.properties ? item.properties.icon : '',
// @ts-ignore
label: item.properties ? item.properties.title : '',
position: item.position,
type: item.properties?.href ? 'link' : 'regular',
properties: item.properties,
} as IMenuItem,
);
}
};
if (injectedItems && injectedItems.length > 0) {
for (const item of injectedItems) {
items.push({
id: item.id,
// @ts-ignore
icon: item.properties ? item.properties.icon : '',
// @ts-ignore
label: item.properties ? item.properties.title : '',
position: item.position,
type: item.properties?.href ? 'link' : 'regular',
properties: item.properties,
} as IMenuItem);
}
}
const regularItems: IMenuItem[] = [
{
id: 'workflows',
icon: 'network-wired',
label: this.$locale.baseText('mainSidebar.workflows'),
position: 'top',
activateOnRouteNames: [ VIEWS.WORKFLOWS ],
},
{
id: 'templates',
icon: 'box-open',
label: this.$locale.baseText('mainSidebar.templates'),
position: 'top',
available: this.settingsStore.isTemplatesEnabled,
activateOnRouteNames: [ VIEWS.TEMPLATES ],
},
{
id: 'credentials',
icon: 'key',
label: this.$locale.baseText('mainSidebar.credentials'),
customIconSize: 'medium',
position: 'top',
activateOnRouteNames: [ VIEWS.CREDENTIALS ],
},
{
id: 'executions',
icon: 'tasks',
label: this.$locale.baseText('generic.executions'),
position: 'top',
},
{
id: 'settings',
icon: 'cog',
label: this.$locale.baseText('settings'),
position: 'bottom',
available: this.canUserAccessSettings && this.usersStore.currentUser !== null,
activateOnRouteNames: [ VIEWS.USERS_SETTINGS, VIEWS.API_SETTINGS, VIEWS.PERSONAL_SETTINGS ],
},
{
id: 'help',
icon: 'question',
label: 'Help',
position: 'bottom',
children: [
{
id: 'quickstart',
icon: 'video',
label: this.$locale.baseText('mainSidebar.helpMenuItems.quickstart'),
type: 'link',
properties: {
href: 'https://www.youtube.com/watch?v=RpjQTGKm-ok',
newWindow: true,
},
const regularItems: IMenuItem[] = [
{
id: 'workflows',
icon: 'network-wired',
label: this.$locale.baseText('mainSidebar.workflows'),
position: 'top',
activateOnRouteNames: [VIEWS.WORKFLOWS],
},
{
id: 'templates',
icon: 'box-open',
label: this.$locale.baseText('mainSidebar.templates'),
position: 'top',
available: this.settingsStore.isTemplatesEnabled,
activateOnRouteNames: [VIEWS.TEMPLATES],
},
{
id: 'credentials',
icon: 'key',
label: this.$locale.baseText('mainSidebar.credentials'),
customIconSize: 'medium',
position: 'top',
activateOnRouteNames: [VIEWS.CREDENTIALS],
},
{
id: 'executions',
icon: 'tasks',
label: this.$locale.baseText('generic.executions'),
position: 'top',
},
{
id: 'settings',
icon: 'cog',
label: this.$locale.baseText('settings'),
position: 'bottom',
available: this.canUserAccessSettings && this.usersStore.currentUser !== null,
activateOnRouteNames: [VIEWS.USERS_SETTINGS, VIEWS.API_SETTINGS, VIEWS.PERSONAL_SETTINGS],
},
{
id: 'help',
icon: 'question',
label: 'Help',
position: 'bottom',
children: [
{
id: 'quickstart',
icon: 'video',
label: this.$locale.baseText('mainSidebar.helpMenuItems.quickstart'),
type: 'link',
properties: {
href: 'https://www.youtube.com/watch?v=RpjQTGKm-ok',
newWindow: true,
},
{
id: 'docs',
icon: 'book',
label: this.$locale.baseText('mainSidebar.helpMenuItems.documentation'),
type: 'link',
properties: {
href: 'https://docs.n8n.io',
newWindow: true,
},
},
{
id: 'docs',
icon: 'book',
label: this.$locale.baseText('mainSidebar.helpMenuItems.documentation'),
type: 'link',
properties: {
href: 'https://docs.n8n.io',
newWindow: true,
},
{
id: 'forum',
icon: 'users',
label: this.$locale.baseText('mainSidebar.helpMenuItems.forum'),
type: 'link',
properties: {
href: 'https://community.n8n.io',
newWindow: true,
},
},
{
id: 'forum',
icon: 'users',
label: this.$locale.baseText('mainSidebar.helpMenuItems.forum'),
type: 'link',
properties: {
href: 'https://community.n8n.io',
newWindow: true,
},
{
id: 'examples',
icon: 'graduation-cap',
label: this.$locale.baseText('mainSidebar.helpMenuItems.course'),
type: 'link',
properties: {
href: 'https://www.youtube.com/watch?v=RpjQTGKm-ok',
newWindow: true,
},
},
{
id: 'examples',
icon: 'graduation-cap',
label: this.$locale.baseText('mainSidebar.helpMenuItems.course'),
type: 'link',
properties: {
href: 'https://www.youtube.com/watch?v=RpjQTGKm-ok',
newWindow: true,
},
{
id: 'about',
icon: 'info',
label: this.$locale.baseText('mainSidebar.aboutN8n'),
position: 'bottom',
},
],
},
];
return [ ...items, ...regularItems ];
},
},
{
id: 'about',
icon: 'info',
label: this.$locale.baseText('mainSidebar.aboutN8n'),
position: 'bottom',
},
],
},
];
return [...items, ...regularItems];
},
async mounted() {
this.basePath = this.rootStore.baseUrl;
if (this.$refs.user) {
this.$externalHooks().run('mainSidebar.mounted', { userRef: this.$refs.user });
},
async mounted() {
this.basePath = this.rootStore.baseUrl;
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;
} else {
this.uiStore.sidebarMenuCollapsed = false;
},
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'));
}
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;
}
},
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 {
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;
}
},
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;
}
},
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': {
if (this.$router.currentRoute.name !== VIEWS.TEMPLATES) {
this.$router.push({ name: VIEWS.TEMPLATES });
}
case 'templates': {
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;
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.$router.push({ name: VIEWS.NEW_WORKFLOW });
}
this.$showMessage({
title: this.$locale.baseText('mainSidebar.showMessage.handleSelect2.title'),
type: 'success',
});
} else if (confirmModal === MODAL_CLOSE) {
return;
case 'credentials': {
if (this.$router.currentRoute.name !== VIEWS.CREDENTIALS) {
this.$router.push({ name: VIEWS.CREDENTIALS });
}
} else {
if (this.$router.currentRoute.name !== VIEWS.NEW_WORKFLOW) {
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;
}
},
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.$router.push({ name: VIEWS.NEW_WORKFLOW });
}
this.$showMessage({
title: this.$locale.baseText('mainSidebar.showMessage.handleSelect3.title'),
title: this.$locale.baseText('mainSidebar.showMessage.handleSelect2.title'),
type: 'success',
});
} else if (confirmModal === MODAL_CLOSE) {
return;
}
this.$titleReset();
},
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;
}
} else {
if (this.$router.currentRoute.name !== VIEWS.NEW_WORKFLOW) {
this.workflowsStore.setWorkflowId(PLACEHOLDER_EMPTY_WORKFLOW_ID);
this.$router.push({ name: VIEWS.NEW_WORKFLOW });
}
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;
});
}
},
this.$showMessage({
title: this.$locale.baseText('mainSidebar.showMessage.handleSelect3.title'),
type: 'success',
});
}
this.$titleReset();
},
});
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>
<style lang="scss" module>
.sideMenu {
position: relative;
height: 100%;
@ -518,7 +558,7 @@ export default mixins(
left: px;
top: -2.5px;
transform: rotate(270deg);
content: "\e6df";
content: '\e6df';
font-family: element-icons;
font-size: var(--font-size-2xs);
font-weight: bold;
@ -545,14 +585,19 @@ export default mixins(
height: 26px;
cursor: pointer;
svg { color: var(--color-text-base) !important; }
svg {
color: var(--color-text-base) !important;
}
span {
display: none;
&.expanded { display: initial; }
&.expanded {
display: initial;
}
}
&:hover {
&, & svg {
&,
& svg {
color: var(--color-text-dark) !important;
}
}
@ -591,8 +636,9 @@ export default mixins(
}
}
@media screen and (max-height: 470px) {
:global(#help) { display: none; }
:global(#help) {
display: none;
}
}
</style>

View file

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

View file

@ -6,25 +6,25 @@
:before-close="close"
:modal="modal"
:wrapperClosable="wrapperClosable"
>
>
<template #title>
<slot name="header" />
</template>
<template>
<span @keydown.stop>
<slot name="content"/>
<slot name="content" />
</span>
</template>
</el-drawer>
</template>
<script lang="ts">
import { useUIStore } from "@/stores/ui";
import { mapStores } from "pinia";
import Vue from "vue";
import { useUIStore } from '@/stores/ui';
import { mapStores } from 'pinia';
import Vue from 'vue';
export default Vue.extend({
name: "ModalDrawer",
name: 'ModalDrawer',
props: {
name: {
type: String,
@ -88,7 +88,8 @@ export default Vue.extend({
async close() {
if (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;
}
}

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