fix(editor): Make inline text edit component reactive to prop changes (#17557)

This commit is contained in:
Elias Meire
2025-07-25 16:04:38 +02:00
committed by GitHub
parent 5a3b0a2481
commit 9c793a45c5
4 changed files with 62 additions and 44 deletions

View File

@@ -40,7 +40,7 @@ describe('N8nInlineTextEdit', () => {
expect(emittedEvents?.[0]).toEqual(['Updated Value']);
});
it('should not update value on blur if input is empty', async () => {
it('should not update value on enter if input is empty', async () => {
const wrapper = renderComponent({
props: {
modelValue: 'Test Value',
@@ -52,11 +52,26 @@ describe('N8nInlineTextEdit', () => {
const input = wrapper.getByTestId('inline-edit-input');
await userEvent.clear(input);
await userEvent.tab(); // Simulate blur
await userEvent.keyboard('{Enter}');
expect(preview).toHaveTextContent('Test Value');
});
it('should display changes to props.modelValue', async () => {
const wrapper = renderComponent({
props: {
modelValue: 'Test Value',
},
});
const preview = wrapper.getByTestId('inline-edit-preview');
expect(preview).toHaveTextContent('Test Value');
await wrapper.rerender({ modelValue: 'New Value!' });
expect(preview).toHaveTextContent('New Value!');
});
it('should not update on escape key press', async () => {
const wrapper = renderComponent({
props: {

View File

@@ -1,7 +1,7 @@
<script lang="ts" setup>
import { useElementSize } from '@vueuse/core';
import { EditableArea, EditableInput, EditablePreview, EditableRoot } from 'reka-ui';
import { computed, ref, useTemplateRef } from 'vue';
import { computed, ref, useTemplateRef, watchEffect } from 'vue';
type Props = {
modelValue: string;
@@ -25,9 +25,31 @@ const emit = defineEmits<{
'update:model-value': [value: string];
}>();
const newValue = ref(props.modelValue);
const temp = ref(props.modelValue || props.placeholder);
const editableRoot = useTemplateRef('editableRoot');
const measureSpan = useTemplateRef('measureSpan');
// Internal editing value
const editingValue = ref(props.modelValue);
// Content for width calculation
const displayContent = computed(() => editingValue.value || props.placeholder);
// Sync when modelValue prop changes
watchEffect(() => {
editingValue.value = props.modelValue;
});
// Resize logic
const { width: measuredWidth } = useElementSize(measureSpan);
const inputWidth = computed(() =>
Math.max(props.minWidth, Math.min(measuredWidth.value + 1, props.maxWidth)),
);
const computedInlineStyles = computed(() => ({
width: `${inputWidth.value}px`,
maxWidth: `${props.maxWidth}px`,
zIndex: 1,
}));
function forceFocus() {
if (editableRoot.value && !props.readOnly) {
@@ -37,54 +59,32 @@ function forceFocus() {
function forceCancel() {
if (editableRoot.value) {
newValue.value = props.modelValue;
editingValue.value = props.modelValue;
editableRoot.value.cancel();
}
}
function onSubmit() {
if (newValue.value === '') {
newValue.value = props.modelValue;
temp.value = props.modelValue;
const trimmed = editingValue.value.trim();
if (!trimmed) {
editingValue.value = props.modelValue;
return;
}
if (newValue.value !== props.modelValue) {
emit('update:model-value', newValue.value);
if (trimmed !== props.modelValue) {
emit('update:model-value', trimmed);
}
}
function onInput(value: string) {
newValue.value = value;
editingValue.value = value;
}
function onStateChange(state: string) {
if (state === 'cancel') {
temp.value = newValue.value;
editingValue.value = props.modelValue;
}
}
// Resize logic
const measureSpan = useTemplateRef('measureSpan');
const { width: measuredWidth } = useElementSize(measureSpan);
const inputWidth = computed(() => {
return Math.max(props.minWidth, Math.min(measuredWidth.value + 1, props.maxWidth));
});
function onChange(event: Event) {
const { value } = event.target as HTMLInputElement;
const processedValue = value.replace(/\s/g, '.');
temp.value = processedValue.trim() !== '' ? processedValue : props.placeholder;
}
const computedInlineStyles = computed(() => {
return {
width: `${inputWidth.value}px`,
maxWidth: `${props.maxWidth}px`,
zIndex: 1,
};
});
defineExpose({ forceFocus, forceCancel });
</script>
@@ -92,10 +92,10 @@ defineExpose({ forceFocus, forceCancel });
<EditableRoot
ref="editableRoot"
:placeholder="placeholder"
:model-value="newValue"
:model-value="editingValue"
submit-mode="both"
:class="$style.inlineRenameRoot"
:title="modelValue"
:title="props.modelValue"
:disabled="disabled"
:max-length="maxLength"
:readonly="readOnly"
@@ -111,7 +111,7 @@ defineExpose({ forceFocus, forceCancel });
data-test-id="inline-editable-area"
>
<span ref="measureSpan" :class="$style.measureSpan">
{{ temp }}
{{ displayContent }}
</span>
<EditablePreview
data-test-id="inline-edit-preview"
@@ -123,7 +123,7 @@ defineExpose({ forceFocus, forceCancel });
:class="$style.inlineRenameInput"
data-test-id="inline-edit-input"
:style="computedInlineStyles"
@input="onChange"
@input="onInput($event.target.value)"
/>
</EditableArea>
</EditableRoot>
@@ -185,7 +185,7 @@ defineExpose({ forceFocus, forceCancel });
position: absolute;
top: 0;
visibility: hidden;
white-space: nowrap;
white-space: pre;
font-family: inherit;
font-size: inherit;
font-weight: inherit;

View File

@@ -866,7 +866,8 @@ onBeforeUnmount(() => {
<style lang="scss" module>
.backdrop {
position: fixed;
position: absolute;
z-index: var(--z-index-ndv);
top: 0;
left: 0;
right: 0;
@@ -875,9 +876,10 @@ onBeforeUnmount(() => {
}
.dialog {
position: fixed;
width: calc(100vw - var(--spacing-2xl));
height: calc(100vh - var(--spacing-2xl));
position: absolute;
z-index: var(--z-index-ndv);
width: calc(100% - var(--spacing-2xl));
height: calc(100% - var(--spacing-2xl));
top: var(--spacing-l);
left: var(--spacing-l);
border: none;

View File

@@ -777,6 +777,7 @@ export const EXPERIMENTS_TO_TRACK = [
RAG_STARTER_WORKFLOW_EXPERIMENT.name,
EXTRA_TEMPLATE_LINKS_EXPERIMENT.name,
TEMPLATE_ONBOARDING_EXPERIMENT.name,
NDV_UI_OVERHAUL_EXPERIMENT.name,
];
export const MFA_FORM = {