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:
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 CssEditor from './CssEditor/CssEditor.vue';
|
||||
import { useUIStore } from '@/stores/ui.store';
|
||||
import { useFocusPanelStore } from '@/stores/focusPanel.store';
|
||||
|
||||
type Picker = { $emit: (arg0: string, arg1: Date) => void };
|
||||
|
||||
@@ -132,6 +133,7 @@ const workflowsStore = useWorkflowsStore();
|
||||
const settingsStore = useSettingsStore();
|
||||
const nodeTypesStore = useNodeTypesStore();
|
||||
const uiStore = useUIStore();
|
||||
const focusPanelStore = useFocusPanelStore();
|
||||
|
||||
// ESLint: false positive
|
||||
// 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);
|
||||
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(() => {
|
||||
|
||||
@@ -8,7 +8,8 @@ import {
|
||||
import { isValueExpression } from '@/utils/nodeTypesUtils';
|
||||
import { computed } from 'vue';
|
||||
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 {
|
||||
parameter: INodeProperties;
|
||||
@@ -37,6 +38,8 @@ const emit = defineEmits<{
|
||||
}>();
|
||||
|
||||
const i18n = useI18n();
|
||||
const ndvStore = useNDVStore();
|
||||
const posthogStore = usePostHog();
|
||||
|
||||
const isDefault = computed(() => props.parameter.default === props.value);
|
||||
const isValueAnExpression = computed(() => isValueExpression(props.parameter, props.value));
|
||||
@@ -53,6 +56,10 @@ const shouldShowOptions = computed(() => {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (hasFocusAction.value) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (['codeNodeEditor', 'sqlEditor'].includes(props.parameter.typeOptions?.editor ?? '')) {
|
||||
return false;
|
||||
}
|
||||
@@ -64,7 +71,7 @@ const shouldShowOptions = computed(() => {
|
||||
return false;
|
||||
});
|
||||
const selectedView = computed(() => (isValueAnExpression.value ? 'expression' : 'fixed'));
|
||||
const activeNode = computed(() => useNDVStore().activeNode);
|
||||
const activeNode = computed(() => ndvStore.activeNode);
|
||||
const hasRemoteMethod = computed(
|
||||
() =>
|
||||
!!props.parameter.typeOptions?.loadOptionsMethod || !!props.parameter.typeOptions?.loadOptions,
|
||||
@@ -77,27 +84,44 @@ const resetValueLabel = computed(() => {
|
||||
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(() => {
|
||||
if (Array.isArray(props.customActions) && props.customActions.length > 0) {
|
||||
return props.customActions;
|
||||
}
|
||||
|
||||
const focusAction = {
|
||||
label: i18n.baseText('parameterInput.focusParameter'),
|
||||
value: 'focus',
|
||||
disabled: false,
|
||||
};
|
||||
|
||||
if (isHtmlEditor.value && !isValueAnExpression.value) {
|
||||
return [
|
||||
{
|
||||
label: i18n.baseText('parameterInput.formatHtml'),
|
||||
value: 'formatHtml',
|
||||
},
|
||||
];
|
||||
const formatHtmlAction = {
|
||||
label: i18n.baseText('parameterInput.formatHtml'),
|
||||
value: 'formatHtml',
|
||||
};
|
||||
|
||||
return hasFocusAction.value ? [formatHtmlAction, focusAction] : [formatHtmlAction];
|
||||
}
|
||||
|
||||
const parameterActions = [
|
||||
{
|
||||
label: resetValueLabel.value,
|
||||
value: 'resetValue',
|
||||
disabled: isDefault.value,
|
||||
},
|
||||
];
|
||||
const resetAction = {
|
||||
label: resetValueLabel.value,
|
||||
value: 'resetValue',
|
||||
disabled: isDefault.value,
|
||||
};
|
||||
|
||||
const parameterActions = hasFocusAction.value ? [resetAction, focusAction] : [resetAction];
|
||||
|
||||
if (
|
||||
hasRemoteMethod.value ||
|
||||
|
||||
Reference in New Issue
Block a user