Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(editor): Show tip when user can type dot after an expression #8931

1 change: 1 addition & 0 deletions cypress/e2e/14-mapping.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,7 @@ describe('Data mapping', () => {
ndv.actions.typeIntoParameterInput('value', 'delete me');

ndv.actions.typeIntoParameterInput('name', 'test');
ndv.getters.parameterInput('name').find('input').blur();

ndv.actions.typeIntoParameterInput('value', 'fun');
ndv.actions.clearParameterInput('value'); // keep focus on param
Expand Down
1 change: 1 addition & 0 deletions packages/editor-ui/src/Interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1247,6 +1247,7 @@ export interface NDVState {
};
isMappingOnboarded: boolean;
isAutocompleteOnboarded: boolean;
highlightDraggables: boolean;
}

export interface NotificationOptions extends Partial<ElementNotificationOptions> {
Expand Down
260 changes: 127 additions & 133 deletions packages/editor-ui/src/components/ExpressionParameterInput.vue
Original file line number Diff line number Diff line change
@@ -1,3 +1,124 @@
<script setup lang="ts">
import { computed, ref, watch } from 'vue';

import ExpressionFunctionIcon from '@/components/ExpressionFunctionIcon.vue';
import InlineExpressionEditorInput from '@/components/InlineExpressionEditor/InlineExpressionEditorInput.vue';
import InlineExpressionEditorOutput from '@/components/InlineExpressionEditor/InlineExpressionEditorOutput.vue';
import { useNDVStore } from '@/stores/ndv.store';
import { useWorkflowsStore } from '@/stores/workflows.store';
import { createExpressionTelemetryPayload } from '@/utils/telemetryUtils';

import { useTelemetry } from '@/composables/useTelemetry';
import type { Segment } from '@/types/expressions';
import { createEventBus, type EventBus } from 'n8n-design-system/utils';
import type { IDataObject } from 'n8n-workflow';
import type { EditorState, SelectionRange } from '@codemirror/state';

const isFocused = ref(false);
const segments = ref<Segment[]>([]);
const editorState = ref<EditorState>();
const selection = ref<SelectionRange>();
const inlineInput = ref<InstanceType<typeof InlineExpressionEditorInput>>();

type Props = {
path: string;
modelValue: string;
isReadOnly: boolean;
rows: number;
isAssignment: boolean;
additionalExpressionData: IDataObject;
eventBus: EventBus;
};

const props = withDefaults(defineProps<Props>(), {
rows: 5,
isAssignment: false,
additionalExpressionData: () => ({}),
eventBus: () => createEventBus(),
});

const emit = defineEmits<{
(event: 'modal-opener-click'): void;
(event: 'update:model-value', value: string): void;
(event: 'focus'): void;
(event: 'blur'): void;
}>();

const telemetry = useTelemetry();
const ndvStore = useNDVStore();
const workflowsStore = useWorkflowsStore();

const hoveringItemNumber = computed(() => ndvStore.hoveringItemNumber);
const isDragging = computed(() => ndvStore.isDraggableDragging);
const noInputData = computed(() => ndvStore.hasInputData);

function focus() {
if (inlineInput.value) {
inlineInput.value.focus();
}
}

function onFocus() {
isFocused.value = true;
emit('focus');
}

function onBlur(event?: FocusEvent | KeyboardEvent) {
if (
event?.target instanceof Element &&
Array.from(event.target.classList).some((_class) => _class.includes('resizer'))
) {
return; // prevent blur on resizing
}

const wasFocused = isFocused.value;

isFocused.value = false;

if (wasFocused) {
emit('blur');

const telemetryPayload = createExpressionTelemetryPayload(
segments.value,
props.modelValue,
workflowsStore.workflowId,
ndvStore.sessionId,
ndvStore.activeNode?.type ?? '',
);

telemetry.track('User closed Expression Editor', telemetryPayload);
}
}

function onValueChange({ value, segments: newSegments }: { value: string; segments: Segment[] }) {
segments.value = newSegments;

if (isDragging.value) return;
if (value === '=' + props.modelValue) return; // prevent report on change of target item

emit('update:model-value', value);
}

function onSelectionChange({
state: newState,
selection: newSelection,
}: {
state: EditorState;
selection: SelectionRange;
}) {
editorState.value = newState;
selection.value = newSelection;
}

watch(isDragging, (newIsDragging) => {
if (newIsDragging) {
onBlur();
}
});

defineExpose({ focus });
</script>

<template>
<div
v-on-click-outside="onBlur"
Expand All @@ -24,7 +145,8 @@
:event-bus="eventBus"
@focus="onFocus"
@blur="onBlur"
@change="onChange"
@update:model-value="onValueChange"
@update:selection="onSelectionChange"
/>
<n8n-button
v-if="!isDragging"
Expand All @@ -35,10 +157,13 @@
size="xsmall"
:class="$style['expression-editor-modal-opener']"
data-test-id="expander"
@click="$emit('modal-opener-click')"
@click="emit('modal-opener-click')"
/>
</div>
<InlineExpressionEditorOutput
:unresolved-expression="modelValue"
:selection="selection"
:editor-state="editorState"
:segments="segments"
:is-read-only="isReadOnly"
:no-input-data="noInputData"
Expand All @@ -48,137 +173,6 @@
</div>
</template>

<script lang="ts">
import { mapStores } from 'pinia';
import type { PropType } from 'vue';
import { defineComponent } from 'vue';

import { useNDVStore } from '@/stores/ndv.store';
import { useWorkflowsStore } from '@/stores/workflows.store';
import InlineExpressionEditorInput from '@/components/InlineExpressionEditor/InlineExpressionEditorInput.vue';
import InlineExpressionEditorOutput from '@/components/InlineExpressionEditor/InlineExpressionEditorOutput.vue';
import ExpressionFunctionIcon from '@/components/ExpressionFunctionIcon.vue';
import { createExpressionTelemetryPayload } from '@/utils/telemetryUtils';

import type { Segment } from '@/types/expressions';
import type { IDataObject } from 'n8n-workflow';
import { useDebounce } from '@/composables/useDebounce';
import { type EventBus, createEventBus } from 'n8n-design-system/utils';

type InlineExpressionEditorInputRef = InstanceType<typeof InlineExpressionEditorInput>;

export default defineComponent({
name: 'ExpressionParameterInput',
components: {
InlineExpressionEditorInput,
InlineExpressionEditorOutput,
ExpressionFunctionIcon,
},
props: {
path: {
type: String,
required: true,
},
modelValue: {
type: String,
required: true,
},
isReadOnly: {
type: Boolean,
default: false,
},
rows: {
type: Number,
default: 5,
},
isAssignment: {
type: Boolean,
default: false,
},
additionalExpressionData: {
type: Object as PropType<IDataObject>,
default: () => ({}),
},
eventBus: {
type: Object as PropType<EventBus>,
default: () => createEventBus(),
},
},
emits: ['focus', 'blur', 'update:model-value', 'modal-opener-click'],
setup() {
const { callDebounced } = useDebounce();
return { callDebounced };
},
data() {
return {
isFocused: false,
segments: [] as Segment[],
};
},
computed: {
...mapStores(useNDVStore, useWorkflowsStore),
hoveringItemNumber(): number {
return this.ndvStore.hoveringItemNumber;
},
isDragging(): boolean {
return this.ndvStore.isDraggableDragging;
},
noInputData(): boolean {
return !this.ndvStore.hasInputData;
},
},
methods: {
focus() {
const inlineInputRef = this.$refs.inlineInput as InlineExpressionEditorInputRef | undefined;
if (inlineInputRef?.$el) {
inlineInputRef.focus();
}
},
onFocus() {
this.isFocused = true;

this.$emit('focus');
},
onBlur(event?: FocusEvent | KeyboardEvent) {
if (
event?.target instanceof Element &&
Array.from(event.target.classList).some((_class) => _class.includes('resizer'))
) {
return; // prevent blur on resizing
}

if (this.isDragging) return; // prevent blur on dragging

const wasFocused = this.isFocused;

this.isFocused = false;

if (wasFocused) {
this.$emit('blur');

const telemetryPayload = createExpressionTelemetryPayload(
this.segments,
this.modelValue,
this.workflowsStore.workflowId,
this.ndvStore.sessionId,
this.ndvStore.activeNode?.type ?? '',
);

this.$telemetry.track('User closed Expression Editor', telemetryPayload);
}
},
onChange({ value, segments }: { value: string; segments: Segment[] }) {
this.segments = segments;

if (this.isDragging) return;
if (value === '=' + this.modelValue) return; // prevent report on change of target item

this.$emit('update:model-value', value);
},
},
});
</script>

<style lang="scss" module>
.expression-parameter-input {
position: relative;
Expand Down
Loading
Loading