mirror of
https://github.com/n8n-io/n8n.git
synced 2024-11-09 22:24:05 -08:00
refactor(editor): Apply Prettier (no-changelog) (#4920)
* ⚡ Adjust `format` script * 🔥 Remove exemption for `editor-ui` * 🎨 Prettify * 👕 Fix lint
This commit is contained in:
parent
bcde07e032
commit
5ca2148c7e
|
@ -1,5 +1,4 @@
|
|||
coverage
|
||||
dist
|
||||
packages/editor-ui
|
||||
package.json
|
||||
.pnpm-lock.yml
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 };
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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));
|
||||
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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 });
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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 });
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
|
|
|
@ -73,10 +73,7 @@ export default Vue.extend({
|
|||
};
|
||||
},
|
||||
computed: {
|
||||
...mapStores(
|
||||
useRootStore,
|
||||
useSettingsStore,
|
||||
),
|
||||
...mapStores(useRootStore, useSettingsStore),
|
||||
},
|
||||
methods: {
|
||||
closeDialog() {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 }}
|
||||
</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,
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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]);
|
||||
});
|
||||
|
|
|
@ -49,9 +49,7 @@ export default mixins(linterExtension, completerExtension, workflowHelpers).exte
|
|||
},
|
||||
},
|
||||
computed: {
|
||||
...mapStores(
|
||||
useRootStore,
|
||||
),
|
||||
...mapStores(useRootStore),
|
||||
content(): string {
|
||||
if (!this.editor) return '';
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -9,10 +9,7 @@ import { useNDVStore } from '@/stores/ndv';
|
|||
|
||||
export const jsonFieldCompletions = (Vue as CodeNodeEditorMixin).extend({
|
||||
computed: {
|
||||
...mapStores(
|
||||
useNDVStore,
|
||||
useWorkflowsStore,
|
||||
),
|
||||
...mapStores(useNDVStore, useWorkflowsStore),
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)',
|
||||
},
|
||||
]),
|
||||
),
|
||||
];
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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', {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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;
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -28,9 +28,7 @@ export default Vue.extend({
|
|||
},
|
||||
},
|
||||
computed: {
|
||||
...mapStores(
|
||||
useRootStore,
|
||||
),
|
||||
...mapStores(useRootStore),
|
||||
basePath(): string {
|
||||
return this.rootStore.baseUrl;
|
||||
},
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 };
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -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');
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
},
|
||||
},
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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%;
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 };
|
||||
|
|
|
@ -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 };
|
||||
|
|
|
@ -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 };
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -76,9 +76,7 @@ export default Vue.extend({
|
|||
},
|
||||
},
|
||||
computed: {
|
||||
...mapStores(
|
||||
useRootStore,
|
||||
),
|
||||
...mapStores(useRootStore),
|
||||
fontStyleData(): object {
|
||||
return {
|
||||
'max-width': this.size + 'px',
|
||||
|
|
|
@ -67,10 +67,7 @@ export default mixins(showMessage).extend({
|
|||
};
|
||||
},
|
||||
computed: {
|
||||
...mapStores(
|
||||
useNDVStore,
|
||||
useUIStore,
|
||||
),
|
||||
...mapStores(useNDVStore, useUIStore),
|
||||
node(): INodeUi | null {
|
||||
return this.ndvStore.activeNode;
|
||||
},
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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) }}
|
||||
<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;
|
||||
}
|
||||
},
|
||||
|
|
|
@ -5,7 +5,6 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
|
||||
import mixins from 'vue-typed-mixins';
|
||||
import emitter from '@/mixins/emitter';
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
},
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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'),
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
Loading…
Reference in a new issue