mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 18:12:04 +00:00
feat(editor): Add focus panel component (no-changelog) (#16620)
This commit is contained in:
@@ -1473,6 +1473,9 @@
|
|||||||
"nodeView.couldntImportWorkflow": "Could not import workflow",
|
"nodeView.couldntImportWorkflow": "Could not import workflow",
|
||||||
"nodeView.couldntLoadWorkflow.invalidWorkflowObject": "Invalid workflow object",
|
"nodeView.couldntLoadWorkflow.invalidWorkflowObject": "Invalid workflow object",
|
||||||
"nodeView.deletesTheCurrentExecutionData": "Deletes the current execution data",
|
"nodeView.deletesTheCurrentExecutionData": "Deletes the current execution data",
|
||||||
|
"nodeView.focusPanel.executeButtonTooltip": "Execute AI Agent",
|
||||||
|
"nodeView.focusPanel.title": "Focus",
|
||||||
|
"nodeView.focusPanel.noParameters": "No parameters focused. Focus a parameter by clicking on the action dropdown in the node detail view.",
|
||||||
"nodeView.itLooksLikeYouHaveBeenEditingSomething": "It looks like you made some edits. If you leave before saving, your changes will be lost.",
|
"nodeView.itLooksLikeYouHaveBeenEditingSomething": "It looks like you made some edits. If you leave before saving, your changes will be lost.",
|
||||||
"nodeView.loadingTemplate": "Loading template",
|
"nodeView.loadingTemplate": "Loading template",
|
||||||
"nodeView.moreInfo": "More info",
|
"nodeView.moreInfo": "More info",
|
||||||
@@ -1610,6 +1613,7 @@
|
|||||||
"parameterInput.parameterHasIssuesAndExpression": "Parameter: \"{shortPath}\" has issues and an expression",
|
"parameterInput.parameterHasIssuesAndExpression": "Parameter: \"{shortPath}\" has issues and an expression",
|
||||||
"parameterInput.refreshList": "Refresh List",
|
"parameterInput.refreshList": "Refresh List",
|
||||||
"parameterInput.clearContents": "Clear Contents",
|
"parameterInput.clearContents": "Clear Contents",
|
||||||
|
"parameterInput.focusParameter": "Focus parameter",
|
||||||
"parameterInput.resetValue": "Reset Value",
|
"parameterInput.resetValue": "Reset Value",
|
||||||
"parameterInput.select": "Select",
|
"parameterInput.select": "Select",
|
||||||
"parameterInput.selectDateAndTime": "Select date and time",
|
"parameterInput.selectDateAndTime": "Select date and time",
|
||||||
|
|||||||
@@ -30,4 +30,5 @@ export const STORES = {
|
|||||||
EVALUATION: 'evaluation',
|
EVALUATION: 'evaluation',
|
||||||
FOLDERS: 'folders',
|
FOLDERS: 'folders',
|
||||||
MODULES: 'modules',
|
MODULES: 'modules',
|
||||||
|
FOCUS_PANEL: 'focusPanel',
|
||||||
} as const;
|
} as const;
|
||||||
|
|||||||
195
packages/frontend/editor-ui/src/components/FocusPanel.vue
Normal file
195
packages/frontend/editor-ui/src/components/FocusPanel.vue
Normal file
@@ -0,0 +1,195 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { useFocusPanelStore } from '@/stores/focusPanel.store';
|
||||||
|
import { N8nText, N8nInput } from '@n8n/design-system';
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import { useI18n } from '@n8n/i18n';
|
||||||
|
import { isValueExpression } from '@/utils/nodeTypesUtils';
|
||||||
|
|
||||||
|
defineOptions({ name: 'FocusPanel' });
|
||||||
|
|
||||||
|
const locale = useI18n();
|
||||||
|
|
||||||
|
const focusPanelStore = useFocusPanelStore();
|
||||||
|
|
||||||
|
const focusedNodeParameter = computed(() => focusPanelStore.focusedNodeParameters[0]);
|
||||||
|
|
||||||
|
const focusPanelActive = computed(() => focusPanelStore.focusPanelActive);
|
||||||
|
|
||||||
|
const expressionModeEnabled = computed(
|
||||||
|
() =>
|
||||||
|
focusedNodeParameter.value &&
|
||||||
|
isValueExpression(focusedNodeParameter.value.parameter, focusedNodeParameter.value.value),
|
||||||
|
);
|
||||||
|
|
||||||
|
function optionSelected() {
|
||||||
|
// TODO: Handle the option selected (command: string) from the dropdown
|
||||||
|
}
|
||||||
|
|
||||||
|
function valueChanged() {
|
||||||
|
// TODO: Update parameter value
|
||||||
|
}
|
||||||
|
|
||||||
|
function executeFocusedNode() {
|
||||||
|
// TODO: Implement execution of the focused node
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div v-if="focusPanelActive" :class="$style.container">
|
||||||
|
<div :class="$style.header">
|
||||||
|
<N8nText size="small" :bold="true">
|
||||||
|
{{ locale.baseText('nodeView.focusPanel.title') }}
|
||||||
|
</N8nText>
|
||||||
|
<div :class="$style.closeButton" @click="focusPanelStore.closeFocusPanel">
|
||||||
|
<n8n-icon icon="arrow-right" color="text-base" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="focusedNodeParameter" :class="$style.content">
|
||||||
|
<div :class="$style.tabHeader">
|
||||||
|
<div :class="$style.tabHeaderText">
|
||||||
|
<N8nText color="text-dark" size="small">{{
|
||||||
|
focusedNodeParameter.parameter.displayName
|
||||||
|
}}</N8nText>
|
||||||
|
<N8nText color="text-base" size="xsmall">{{ focusedNodeParameter.nodeName }}</N8nText>
|
||||||
|
</div>
|
||||||
|
<N8nTooltip>
|
||||||
|
<n8n-button
|
||||||
|
v-bind="{ icon: 'play', square: true }"
|
||||||
|
size="small"
|
||||||
|
type="primary"
|
||||||
|
@click="executeFocusedNode"
|
||||||
|
/>
|
||||||
|
<template #content>
|
||||||
|
<N8nText size="small">
|
||||||
|
{{ locale.baseText('nodeView.focusPanel.executeButtonTooltip') }}
|
||||||
|
</N8nText>
|
||||||
|
</template>
|
||||||
|
</N8nTooltip>
|
||||||
|
</div>
|
||||||
|
<div :class="$style.parameterDetailsWrapper">
|
||||||
|
<div :class="$style.parameterOptionsWrapper">
|
||||||
|
<div></div>
|
||||||
|
<ParameterOptions
|
||||||
|
:parameter="focusedNodeParameter.parameter"
|
||||||
|
:value="focusedNodeParameter.value"
|
||||||
|
:is-read-only="false"
|
||||||
|
@update:model-value="optionSelected"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div :class="$style.editorContainer">
|
||||||
|
<ExpressionEditorModalInput
|
||||||
|
v-if="expressionModeEnabled"
|
||||||
|
:model-value="focusedNodeParameter.value"
|
||||||
|
:class="$style.editor"
|
||||||
|
:is-read-only="false"
|
||||||
|
:path="focusedNodeParameter.parameterPath"
|
||||||
|
data-test-id="expression-modal-input"
|
||||||
|
:target-node-parameter-context="{
|
||||||
|
nodeName: focusedNodeParameter.nodeName,
|
||||||
|
parameterPath: focusedNodeParameter.parameterPath,
|
||||||
|
}"
|
||||||
|
@change="valueChanged"
|
||||||
|
/>
|
||||||
|
<N8nInput
|
||||||
|
v-else
|
||||||
|
v-model="focusedNodeParameter.value"
|
||||||
|
:class="$style.editor"
|
||||||
|
type="textarea"
|
||||||
|
resize="none"
|
||||||
|
></N8nInput>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else :class="[$style.content, $style.emptyContent]">
|
||||||
|
<div :class="$style.emptyText">
|
||||||
|
<N8nText color="text-base">
|
||||||
|
{{ locale.baseText('nodeView.focusPanel.noParameters') }}
|
||||||
|
</N8nText>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" module>
|
||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 528px;
|
||||||
|
border-left: 1px solid var(--color-foreground-base);
|
||||||
|
background: var(--color-background-base);
|
||||||
|
}
|
||||||
|
|
||||||
|
.closeButton:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
padding: var(--spacing-2xs);
|
||||||
|
justify-content: space-between;
|
||||||
|
border-bottom: 1px solid var(--color-foreground-base);
|
||||||
|
background: var(--color-background-xlight);
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
&.emptyContent {
|
||||||
|
text-align: center;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.emptyText {
|
||||||
|
max-width: 300px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabHeader {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
border-bottom: 1px solid var(--color-foreground-base);
|
||||||
|
padding: var(--spacing-2xs);
|
||||||
|
|
||||||
|
.tabHeaderText {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--spacing-4xs);
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttonWrapper {
|
||||||
|
display: flex;
|
||||||
|
padding: 6px 8px 6px 34px;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.parameterDetailsWrapper {
|
||||||
|
display: flex;
|
||||||
|
height: 100%;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--spacing-2xs);
|
||||||
|
padding: var(--spacing-2xs);
|
||||||
|
|
||||||
|
.parameterOptionsWrapper {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editorContainer {
|
||||||
|
display: flex;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
.editor {
|
||||||
|
display: flex;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
font-size: var(--font-size-xs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -70,6 +70,7 @@ import { completeExpressionSyntax, shouldConvertToExpression } from '@/utils/exp
|
|||||||
import { isPresent } from '@/utils/typesUtils';
|
import { isPresent } from '@/utils/typesUtils';
|
||||||
import CssEditor from './CssEditor/CssEditor.vue';
|
import CssEditor from './CssEditor/CssEditor.vue';
|
||||||
import { useUIStore } from '@/stores/ui.store';
|
import { useUIStore } from '@/stores/ui.store';
|
||||||
|
import { useFocusPanelStore } from '@/stores/focusPanel.store';
|
||||||
|
|
||||||
type Picker = { $emit: (arg0: string, arg1: Date) => void };
|
type Picker = { $emit: (arg0: string, arg1: Date) => void };
|
||||||
|
|
||||||
@@ -132,6 +133,7 @@ const workflowsStore = useWorkflowsStore();
|
|||||||
const settingsStore = useSettingsStore();
|
const settingsStore = useSettingsStore();
|
||||||
const nodeTypesStore = useNodeTypesStore();
|
const nodeTypesStore = useNodeTypesStore();
|
||||||
const uiStore = useUIStore();
|
const uiStore = useUIStore();
|
||||||
|
const focusPanelStore = useFocusPanelStore();
|
||||||
|
|
||||||
// ESLint: false positive
|
// ESLint: false positive
|
||||||
// eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents, @typescript-eslint/no-duplicate-type-constituents
|
// eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents, @typescript-eslint/no-duplicate-type-constituents
|
||||||
@@ -1043,6 +1045,23 @@ async function optionSelected(command: string) {
|
|||||||
telemetry.track('User switched parameter mode', telemetryPayload);
|
telemetry.track('User switched parameter mode', telemetryPayload);
|
||||||
void externalHooks.run('parameterInput.modeSwitch', telemetryPayload);
|
void externalHooks.run('parameterInput.modeSwitch', telemetryPayload);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (node.value && command === 'focus') {
|
||||||
|
focusPanelStore.setFocusedNodeParameter({
|
||||||
|
nodeName: node.value.name,
|
||||||
|
parameterPath: props.path,
|
||||||
|
parameter: props.parameter,
|
||||||
|
value: modelValueString.value,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (ndvStore.activeNode) {
|
||||||
|
ndvStore.activeNodeName = null;
|
||||||
|
// TODO: check what this does - close method on NodeDetailsView
|
||||||
|
ndvStore.resetNDVPushRef();
|
||||||
|
}
|
||||||
|
|
||||||
|
focusPanelStore.focusPanelActive = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ import {
|
|||||||
import { isValueExpression } from '@/utils/nodeTypesUtils';
|
import { isValueExpression } from '@/utils/nodeTypesUtils';
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
import { useNDVStore } from '@/stores/ndv.store';
|
import { useNDVStore } from '@/stores/ndv.store';
|
||||||
import { AI_TRANSFORM_NODE_TYPE } from '@/constants';
|
import { usePostHog } from '@/stores/posthog.store';
|
||||||
|
import { AI_TRANSFORM_NODE_TYPE, FOCUS_PANEL_EXPERIMENT } from '@/constants';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
parameter: INodeProperties;
|
parameter: INodeProperties;
|
||||||
@@ -37,6 +38,8 @@ const emit = defineEmits<{
|
|||||||
}>();
|
}>();
|
||||||
|
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
|
const ndvStore = useNDVStore();
|
||||||
|
const posthogStore = usePostHog();
|
||||||
|
|
||||||
const isDefault = computed(() => props.parameter.default === props.value);
|
const isDefault = computed(() => props.parameter.default === props.value);
|
||||||
const isValueAnExpression = computed(() => isValueExpression(props.parameter, props.value));
|
const isValueAnExpression = computed(() => isValueExpression(props.parameter, props.value));
|
||||||
@@ -53,6 +56,10 @@ const shouldShowOptions = computed(() => {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (hasFocusAction.value) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
if (['codeNodeEditor', 'sqlEditor'].includes(props.parameter.typeOptions?.editor ?? '')) {
|
if (['codeNodeEditor', 'sqlEditor'].includes(props.parameter.typeOptions?.editor ?? '')) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -64,7 +71,7 @@ const shouldShowOptions = computed(() => {
|
|||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
const selectedView = computed(() => (isValueAnExpression.value ? 'expression' : 'fixed'));
|
const selectedView = computed(() => (isValueAnExpression.value ? 'expression' : 'fixed'));
|
||||||
const activeNode = computed(() => useNDVStore().activeNode);
|
const activeNode = computed(() => ndvStore.activeNode);
|
||||||
const hasRemoteMethod = computed(
|
const hasRemoteMethod = computed(
|
||||||
() =>
|
() =>
|
||||||
!!props.parameter.typeOptions?.loadOptionsMethod || !!props.parameter.typeOptions?.loadOptions,
|
!!props.parameter.typeOptions?.loadOptionsMethod || !!props.parameter.typeOptions?.loadOptions,
|
||||||
@@ -77,27 +84,44 @@ const resetValueLabel = computed(() => {
|
|||||||
return i18n.baseText('parameterInput.resetValue');
|
return i18n.baseText('parameterInput.resetValue');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const isFocusPanelFeatureEnabled = computed(() => {
|
||||||
|
return posthogStore.getVariant(FOCUS_PANEL_EXPERIMENT.name) === FOCUS_PANEL_EXPERIMENT.variant;
|
||||||
|
});
|
||||||
|
const hasFocusAction = computed(
|
||||||
|
() =>
|
||||||
|
isFocusPanelFeatureEnabled.value &&
|
||||||
|
!props.isReadOnly &&
|
||||||
|
activeNode.value &&
|
||||||
|
(props.parameter.type === 'string' || props.parameter.type === 'json'),
|
||||||
|
);
|
||||||
|
|
||||||
const actions = computed(() => {
|
const actions = computed(() => {
|
||||||
if (Array.isArray(props.customActions) && props.customActions.length > 0) {
|
if (Array.isArray(props.customActions) && props.customActions.length > 0) {
|
||||||
return props.customActions;
|
return props.customActions;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const focusAction = {
|
||||||
|
label: i18n.baseText('parameterInput.focusParameter'),
|
||||||
|
value: 'focus',
|
||||||
|
disabled: false,
|
||||||
|
};
|
||||||
|
|
||||||
if (isHtmlEditor.value && !isValueAnExpression.value) {
|
if (isHtmlEditor.value && !isValueAnExpression.value) {
|
||||||
return [
|
const formatHtmlAction = {
|
||||||
{
|
label: i18n.baseText('parameterInput.formatHtml'),
|
||||||
label: i18n.baseText('parameterInput.formatHtml'),
|
value: 'formatHtml',
|
||||||
value: 'formatHtml',
|
};
|
||||||
},
|
|
||||||
];
|
return hasFocusAction.value ? [formatHtmlAction, focusAction] : [formatHtmlAction];
|
||||||
}
|
}
|
||||||
|
|
||||||
const parameterActions = [
|
const resetAction = {
|
||||||
{
|
label: resetValueLabel.value,
|
||||||
label: resetValueLabel.value,
|
value: 'resetValue',
|
||||||
value: 'resetValue',
|
disabled: isDefault.value,
|
||||||
disabled: isDefault.value,
|
};
|
||||||
},
|
|
||||||
];
|
const parameterActions = hasFocusAction.value ? [resetAction, focusAction] : [resetAction];
|
||||||
|
|
||||||
if (
|
if (
|
||||||
hasRemoteMethod.value ||
|
hasRemoteMethod.value ||
|
||||||
|
|||||||
@@ -745,6 +745,12 @@ export const RAG_STARTER_WORKFLOW_EXPERIMENT = {
|
|||||||
variant: 'variant',
|
variant: 'variant',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const FOCUS_PANEL_EXPERIMENT = {
|
||||||
|
name: 'focus_panel',
|
||||||
|
control: 'control',
|
||||||
|
variant: 'variant',
|
||||||
|
};
|
||||||
|
|
||||||
export const EXPERIMENTS_TO_TRACK = [
|
export const EXPERIMENTS_TO_TRACK = [
|
||||||
EASY_AI_WORKFLOW_EXPERIMENT.name,
|
EASY_AI_WORKFLOW_EXPERIMENT.name,
|
||||||
AI_CREDITS_EXPERIMENT.name,
|
AI_CREDITS_EXPERIMENT.name,
|
||||||
|
|||||||
36
packages/frontend/editor-ui/src/stores/focusPanel.store.ts
Normal file
36
packages/frontend/editor-ui/src/stores/focusPanel.store.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { STORES } from '@n8n/stores';
|
||||||
|
import { defineStore } from 'pinia';
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
import type { INodeProperties } from 'n8n-workflow';
|
||||||
|
|
||||||
|
type FocusedNodeParameter = {
|
||||||
|
nodeName: string;
|
||||||
|
parameter: INodeProperties;
|
||||||
|
parameterPath: string;
|
||||||
|
value: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useFocusPanelStore = defineStore(STORES.FOCUS_PANEL, () => {
|
||||||
|
const focusPanelActive = ref(false);
|
||||||
|
const focusedNodeParameters = ref<FocusedNodeParameter[]>([]);
|
||||||
|
|
||||||
|
const setFocusedNodeParameter = (nodeParameter: FocusedNodeParameter) => {
|
||||||
|
focusedNodeParameters.value = [
|
||||||
|
nodeParameter,
|
||||||
|
// Uncomment when tabs are implemented
|
||||||
|
// ...focusedNodeParameters.value.filter((p) => p.parameterPath !== nodeParameter.parameterPath),
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeFocusPanel = () => {
|
||||||
|
focusPanelActive.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
focusPanelActive,
|
||||||
|
focusedNodeParameters,
|
||||||
|
setFocusedNodeParameter,
|
||||||
|
closeFocusPanel,
|
||||||
|
};
|
||||||
|
});
|
||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
} from 'vue';
|
} from 'vue';
|
||||||
import { onBeforeRouteLeave, useRoute, useRouter } from 'vue-router';
|
import { onBeforeRouteLeave, useRoute, useRouter } from 'vue-router';
|
||||||
import WorkflowCanvas from '@/components/canvas/WorkflowCanvas.vue';
|
import WorkflowCanvas from '@/components/canvas/WorkflowCanvas.vue';
|
||||||
|
import FocusPanel from '@/components/FocusPanel.vue';
|
||||||
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
||||||
import { useUIStore } from '@/stores/ui.store';
|
import { useUIStore } from '@/stores/ui.store';
|
||||||
import CanvasRunWorkflowButton from '@/components/canvas/elements/buttons/CanvasRunWorkflowButton.vue';
|
import CanvasRunWorkflowButton from '@/components/canvas/elements/buttons/CanvasRunWorkflowButton.vue';
|
||||||
@@ -54,6 +55,7 @@ import {
|
|||||||
CHAT_TRIGGER_NODE_TYPE,
|
CHAT_TRIGGER_NODE_TYPE,
|
||||||
DRAG_EVENT_DATA_KEY,
|
DRAG_EVENT_DATA_KEY,
|
||||||
EnterpriseEditionFeature,
|
EnterpriseEditionFeature,
|
||||||
|
FOCUS_PANEL_EXPERIMENT,
|
||||||
FROM_AI_PARAMETERS_MODAL_KEY,
|
FROM_AI_PARAMETERS_MODAL_KEY,
|
||||||
MAIN_HEADER_TABS,
|
MAIN_HEADER_TABS,
|
||||||
MANUAL_CHAT_TRIGGER_NODE_TYPE,
|
MANUAL_CHAT_TRIGGER_NODE_TYPE,
|
||||||
@@ -69,6 +71,7 @@ import {
|
|||||||
} from '@/constants';
|
} from '@/constants';
|
||||||
import { useSourceControlStore } from '@/stores/sourceControl.store';
|
import { useSourceControlStore } from '@/stores/sourceControl.store';
|
||||||
import { useNodeCreatorStore } from '@/stores/nodeCreator.store';
|
import { useNodeCreatorStore } from '@/stores/nodeCreator.store';
|
||||||
|
import { usePostHog } from '@/stores/posthog.store';
|
||||||
import { useExternalHooks } from '@/composables/useExternalHooks';
|
import { useExternalHooks } from '@/composables/useExternalHooks';
|
||||||
import {
|
import {
|
||||||
NodeConnectionTypes,
|
NodeConnectionTypes,
|
||||||
@@ -242,6 +245,10 @@ const { extractWorkflow } = useWorkflowExtraction();
|
|||||||
const { applyExecutionData } = useExecutionDebugging();
|
const { applyExecutionData } = useExecutionDebugging();
|
||||||
useClipboard({ onPaste: onClipboardPaste });
|
useClipboard({ onPaste: onClipboardPaste });
|
||||||
|
|
||||||
|
const isFocusPanelFeatureEnabled = computed(() => {
|
||||||
|
return usePostHog().getVariant(FOCUS_PANEL_EXPERIMENT.name) === FOCUS_PANEL_EXPERIMENT.variant;
|
||||||
|
});
|
||||||
|
|
||||||
const isLoading = ref(true);
|
const isLoading = ref(true);
|
||||||
const isBlankRedirect = ref(false);
|
const isBlankRedirect = ref(false);
|
||||||
const readOnlyNotification = ref<null | { visible: boolean }>(null);
|
const readOnlyNotification = ref<null | { visible: boolean }>(null);
|
||||||
@@ -1981,146 +1988,154 @@ onBeforeUnmount(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<WorkflowCanvas
|
<div :class="$style.wrapper">
|
||||||
v-if="editableWorkflow && editableWorkflowObject && !isLoading"
|
<WorkflowCanvas
|
||||||
:id="editableWorkflow.id"
|
v-if="editableWorkflow && editableWorkflowObject && !isLoading"
|
||||||
:workflow="editableWorkflow"
|
:id="editableWorkflow.id"
|
||||||
:workflow-object="editableWorkflowObject"
|
:workflow="editableWorkflow"
|
||||||
:fallback-nodes="fallbackNodes"
|
:workflow-object="editableWorkflowObject"
|
||||||
:show-fallback-nodes="showFallbackNodes"
|
:fallback-nodes="fallbackNodes"
|
||||||
:event-bus="canvasEventBus"
|
:show-fallback-nodes="showFallbackNodes"
|
||||||
:read-only="isCanvasReadOnly"
|
:event-bus="canvasEventBus"
|
||||||
:executing="isWorkflowRunning"
|
:read-only="isCanvasReadOnly"
|
||||||
:key-bindings="keyBindingsEnabled"
|
:executing="isWorkflowRunning"
|
||||||
@update:nodes:position="onUpdateNodesPosition"
|
:key-bindings="keyBindingsEnabled"
|
||||||
@update:node:position="onUpdateNodePosition"
|
@update:nodes:position="onUpdateNodesPosition"
|
||||||
@update:node:activated="onSetNodeActivated"
|
@update:node:position="onUpdateNodePosition"
|
||||||
@update:node:deactivated="onSetNodeDeactivated"
|
@update:node:activated="onSetNodeActivated"
|
||||||
@update:node:selected="onSetNodeSelected"
|
@update:node:deactivated="onSetNodeDeactivated"
|
||||||
@update:node:enabled="onToggleNodeDisabled"
|
@update:node:selected="onSetNodeSelected"
|
||||||
@update:node:name="onOpenRenameNodeModal"
|
@update:node:enabled="onToggleNodeDisabled"
|
||||||
@update:node:parameters="onUpdateNodeParameters"
|
@update:node:name="onOpenRenameNodeModal"
|
||||||
@update:node:inputs="onUpdateNodeInputs"
|
@update:node:parameters="onUpdateNodeParameters"
|
||||||
@update:node:outputs="onUpdateNodeOutputs"
|
@update:node:inputs="onUpdateNodeInputs"
|
||||||
@update:logs-open="logsStore.toggleOpen($event)"
|
@update:node:outputs="onUpdateNodeOutputs"
|
||||||
@update:logs:input-open="logsStore.toggleInputOpen"
|
@update:logs-open="logsStore.toggleOpen($event)"
|
||||||
@update:logs:output-open="logsStore.toggleOutputOpen"
|
@update:logs:input-open="logsStore.toggleInputOpen"
|
||||||
@update:has-range-selection="canvasStore.setHasRangeSelection"
|
@update:logs:output-open="logsStore.toggleOutputOpen"
|
||||||
@open:sub-workflow="onOpenSubWorkflow"
|
@update:has-range-selection="canvasStore.setHasRangeSelection"
|
||||||
@click:node="onClickNode"
|
@open:sub-workflow="onOpenSubWorkflow"
|
||||||
@click:node:add="onClickNodeAdd"
|
@click:node="onClickNode"
|
||||||
@run:node="onRunWorkflowToNode"
|
@click:node:add="onClickNodeAdd"
|
||||||
@delete:node="onDeleteNode"
|
@run:node="onRunWorkflowToNode"
|
||||||
@create:connection="onCreateConnection"
|
@delete:node="onDeleteNode"
|
||||||
@create:connection:cancelled="onCreateConnectionCancelled"
|
@create:connection="onCreateConnection"
|
||||||
@delete:connection="onDeleteConnection"
|
@create:connection:cancelled="onCreateConnectionCancelled"
|
||||||
@click:connection:add="onClickConnectionAdd"
|
@delete:connection="onDeleteConnection"
|
||||||
@click:pane="onClickPane"
|
@click:connection:add="onClickConnectionAdd"
|
||||||
@create:node="onOpenNodeCreatorFromCanvas"
|
@click:pane="onClickPane"
|
||||||
@create:sticky="onCreateSticky"
|
@create:node="onOpenNodeCreatorFromCanvas"
|
||||||
@delete:nodes="onDeleteNodes"
|
@create:sticky="onCreateSticky"
|
||||||
@update:nodes:enabled="onToggleNodesDisabled"
|
@delete:nodes="onDeleteNodes"
|
||||||
@update:nodes:pin="onPinNodes"
|
@update:nodes:enabled="onToggleNodesDisabled"
|
||||||
@duplicate:nodes="onDuplicateNodes"
|
@update:nodes:pin="onPinNodes"
|
||||||
@copy:nodes="onCopyNodes"
|
@duplicate:nodes="onDuplicateNodes"
|
||||||
@cut:nodes="onCutNodes"
|
@copy:nodes="onCopyNodes"
|
||||||
@run:workflow="runEntireWorkflow('main')"
|
@cut:nodes="onCutNodes"
|
||||||
@save:workflow="onSaveWorkflow"
|
@run:workflow="runEntireWorkflow('main')"
|
||||||
@create:workflow="onCreateWorkflow"
|
@save:workflow="onSaveWorkflow"
|
||||||
@viewport:change="onViewportChange"
|
@create:workflow="onCreateWorkflow"
|
||||||
@selection:end="onSelectionEnd"
|
@viewport:change="onViewportChange"
|
||||||
@drag-and-drop="onDragAndDrop"
|
@selection:end="onSelectionEnd"
|
||||||
@tidy-up="onTidyUp"
|
@drag-and-drop="onDragAndDrop"
|
||||||
@extract-workflow="onExtractWorkflow"
|
@tidy-up="onTidyUp"
|
||||||
@start-chat="startChat()"
|
@extract-workflow="onExtractWorkflow"
|
||||||
>
|
@start-chat="startChat()"
|
||||||
<Suspense>
|
|
||||||
<LazySetupWorkflowCredentialsButton :class="$style.setupCredentialsButtonWrapper" />
|
|
||||||
</Suspense>
|
|
||||||
<div v-if="!isCanvasReadOnly" :class="$style.executionButtons">
|
|
||||||
<CanvasRunWorkflowButton
|
|
||||||
v-if="isRunWorkflowButtonVisible"
|
|
||||||
:waiting-for-webhook="isExecutionWaitingForWebhook"
|
|
||||||
:disabled="isExecutionDisabled"
|
|
||||||
:executing="isWorkflowRunning"
|
|
||||||
:trigger-nodes="triggerNodes"
|
|
||||||
:get-node-type="nodeTypesStore.getNodeType"
|
|
||||||
:selected-trigger-node-name="workflowsStore.selectedTriggerNodeName"
|
|
||||||
@mouseenter="onRunWorkflowButtonMouseEnter"
|
|
||||||
@mouseleave="onRunWorkflowButtonMouseLeave"
|
|
||||||
@execute="runEntireWorkflow('main')"
|
|
||||||
@select-trigger-node="workflowsStore.setSelectedTriggerNodeName"
|
|
||||||
/>
|
|
||||||
<template v-if="containsChatTriggerNodes">
|
|
||||||
<CanvasChatButton
|
|
||||||
v-if="isLogsPanelOpen"
|
|
||||||
type="tertiary"
|
|
||||||
:label="i18n.baseText('chat.hide')"
|
|
||||||
:class="$style.chatButton"
|
|
||||||
@click="logsStore.toggleOpen(false)"
|
|
||||||
/>
|
|
||||||
<KeyboardShortcutTooltip
|
|
||||||
v-else
|
|
||||||
:label="i18n.baseText('chat.open')"
|
|
||||||
:shortcut="{ keys: ['c'] }"
|
|
||||||
>
|
|
||||||
<CanvasChatButton
|
|
||||||
:type="isRunWorkflowButtonVisible ? 'secondary' : 'primary'"
|
|
||||||
:label="i18n.baseText('chat.open')"
|
|
||||||
:class="$style.chatButton"
|
|
||||||
@click="onOpenChat"
|
|
||||||
/>
|
|
||||||
</KeyboardShortcutTooltip>
|
|
||||||
</template>
|
|
||||||
<CanvasStopCurrentExecutionButton
|
|
||||||
v-if="isStopExecutionButtonVisible"
|
|
||||||
:stopping="isStoppingExecution"
|
|
||||||
@click="onStopExecution"
|
|
||||||
/>
|
|
||||||
<CanvasStopWaitingForWebhookButton
|
|
||||||
v-if="isStopWaitingForWebhookButtonVisible"
|
|
||||||
@click="onStopWaitingForWebhook"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<N8nCallout
|
|
||||||
v-if="isReadOnlyEnvironment"
|
|
||||||
theme="warning"
|
|
||||||
icon="lock"
|
|
||||||
:class="$style.readOnlyEnvironmentNotification"
|
|
||||||
>
|
>
|
||||||
{{ i18n.baseText('readOnlyEnv.cantEditOrRun') }}
|
<Suspense>
|
||||||
</N8nCallout>
|
<LazySetupWorkflowCredentialsButton :class="$style.setupCredentialsButtonWrapper" />
|
||||||
|
</Suspense>
|
||||||
|
<div v-if="!isCanvasReadOnly" :class="$style.executionButtons">
|
||||||
|
<CanvasRunWorkflowButton
|
||||||
|
v-if="isRunWorkflowButtonVisible"
|
||||||
|
:waiting-for-webhook="isExecutionWaitingForWebhook"
|
||||||
|
:disabled="isExecutionDisabled"
|
||||||
|
:executing="isWorkflowRunning"
|
||||||
|
:trigger-nodes="triggerNodes"
|
||||||
|
:get-node-type="nodeTypesStore.getNodeType"
|
||||||
|
:selected-trigger-node-name="workflowsStore.selectedTriggerNodeName"
|
||||||
|
@mouseenter="onRunWorkflowButtonMouseEnter"
|
||||||
|
@mouseleave="onRunWorkflowButtonMouseLeave"
|
||||||
|
@execute="runEntireWorkflow('main')"
|
||||||
|
@select-trigger-node="workflowsStore.setSelectedTriggerNodeName"
|
||||||
|
/>
|
||||||
|
<template v-if="containsChatTriggerNodes">
|
||||||
|
<CanvasChatButton
|
||||||
|
v-if="isLogsPanelOpen"
|
||||||
|
type="tertiary"
|
||||||
|
:label="i18n.baseText('chat.hide')"
|
||||||
|
:class="$style.chatButton"
|
||||||
|
@click="logsStore.toggleOpen(false)"
|
||||||
|
/>
|
||||||
|
<KeyboardShortcutTooltip
|
||||||
|
v-else
|
||||||
|
:label="i18n.baseText('chat.open')"
|
||||||
|
:shortcut="{ keys: ['c'] }"
|
||||||
|
>
|
||||||
|
<CanvasChatButton
|
||||||
|
:type="isRunWorkflowButtonVisible ? 'secondary' : 'primary'"
|
||||||
|
:label="i18n.baseText('chat.open')"
|
||||||
|
:class="$style.chatButton"
|
||||||
|
@click="onOpenChat"
|
||||||
|
/>
|
||||||
|
</KeyboardShortcutTooltip>
|
||||||
|
</template>
|
||||||
|
<CanvasStopCurrentExecutionButton
|
||||||
|
v-if="isStopExecutionButtonVisible"
|
||||||
|
:stopping="isStoppingExecution"
|
||||||
|
@click="onStopExecution"
|
||||||
|
/>
|
||||||
|
<CanvasStopWaitingForWebhookButton
|
||||||
|
v-if="isStopWaitingForWebhookButtonVisible"
|
||||||
|
@click="onStopWaitingForWebhook"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Suspense>
|
<N8nCallout
|
||||||
<LazyNodeCreation
|
v-if="isReadOnlyEnvironment"
|
||||||
v-if="!isCanvasReadOnly"
|
theme="warning"
|
||||||
:create-node-active="nodeCreatorStore.isCreateNodeActive"
|
icon="lock"
|
||||||
:node-view-scale="viewportTransform.zoom"
|
:class="$style.readOnlyEnvironmentNotification"
|
||||||
@toggle-node-creator="onToggleNodeCreator"
|
>
|
||||||
@add-nodes="onAddNodesAndConnections"
|
{{ i18n.baseText('readOnlyEnv.cantEditOrRun') }}
|
||||||
/>
|
</N8nCallout>
|
||||||
</Suspense>
|
|
||||||
<Suspense>
|
<Suspense>
|
||||||
<LazyNodeDetailsView
|
<LazyNodeCreation
|
||||||
:workflow-object="editableWorkflowObject"
|
v-if="!isCanvasReadOnly"
|
||||||
:read-only="isCanvasReadOnly"
|
:create-node-active="nodeCreatorStore.isCreateNodeActive"
|
||||||
:is-production-execution-preview="isProductionExecutionPreview"
|
:node-view-scale="viewportTransform.zoom"
|
||||||
:renaming="false"
|
@toggle-node-creator="onToggleNodeCreator"
|
||||||
@value-changed="onRenameNode"
|
@add-nodes="onAddNodesAndConnections"
|
||||||
@stop-execution="onStopExecution"
|
/>
|
||||||
@switch-selected-node="onSwitchActiveNode"
|
</Suspense>
|
||||||
@open-connection-node-creator="onOpenSelectiveNodeCreator"
|
<Suspense>
|
||||||
@save-keyboard-shortcut="onSaveWorkflow"
|
<LazyNodeDetailsView
|
||||||
/>
|
:workflow-object="editableWorkflowObject"
|
||||||
<!--
|
:read-only="isCanvasReadOnly"
|
||||||
|
:is-production-execution-preview="isProductionExecutionPreview"
|
||||||
|
:renaming="false"
|
||||||
|
@value-changed="onRenameNode"
|
||||||
|
@stop-execution="onStopExecution"
|
||||||
|
@switch-selected-node="onSwitchActiveNode"
|
||||||
|
@open-connection-node-creator="onOpenSelectiveNodeCreator"
|
||||||
|
@save-keyboard-shortcut="onSaveWorkflow"
|
||||||
|
/>
|
||||||
|
<!--
|
||||||
:renaming="renamingActive"
|
:renaming="renamingActive"
|
||||||
-->
|
-->
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</WorkflowCanvas>
|
</WorkflowCanvas>
|
||||||
|
<FocusPanel v-if="isFocusPanelFeatureEnabled" />
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" module>
|
<style lang="scss" module>
|
||||||
|
.wrapper {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.executionButtons {
|
.executionButtons {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
Reference in New Issue
Block a user