mirror of
https://github.com/n8n-io/n8n.git
synced 2025-01-11 04:47:29 -08:00
fix(editor): UI enhancements and fixes for expression inputs (#8996)
This commit is contained in:
parent
1cbd044e41
commit
8788e2a35b
|
@ -12,3 +12,10 @@ window.ResizeObserver =
|
||||||
}));
|
}));
|
||||||
|
|
||||||
Element.prototype.scrollIntoView = vi.fn();
|
Element.prototype.scrollIntoView = vi.fn();
|
||||||
|
|
||||||
|
Range.prototype.getBoundingClientRect = vi.fn();
|
||||||
|
Range.prototype.getClientRects = vi.fn(() => ({
|
||||||
|
item: vi.fn(),
|
||||||
|
length: 0,
|
||||||
|
[Symbol.iterator]: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
|
@ -169,7 +169,6 @@ const onBlur = (): void => {
|
||||||
display-options
|
display-options
|
||||||
hide-label
|
hide-label
|
||||||
hide-hint
|
hide-hint
|
||||||
:rows="3"
|
|
||||||
:is-read-only="isReadOnly"
|
:is-read-only="isReadOnly"
|
||||||
:parameter="nameParameter"
|
:parameter="nameParameter"
|
||||||
:value="assignment.name"
|
:value="assignment.name"
|
||||||
|
@ -196,7 +195,6 @@ const onBlur = (): void => {
|
||||||
hide-label
|
hide-label
|
||||||
hide-issues
|
hide-issues
|
||||||
hide-hint
|
hide-hint
|
||||||
:rows="3"
|
|
||||||
is-assignment
|
is-assignment
|
||||||
:is-read-only="isReadOnly"
|
:is-read-only="isReadOnly"
|
||||||
:options-position="breakpoint === 'default' ? 'top' : 'bottom'"
|
:options-position="breakpoint === 'default' ? 'top' : 'bottom'"
|
||||||
|
|
|
@ -156,7 +156,6 @@ const onBlur = (): void => {
|
||||||
hide-label
|
hide-label
|
||||||
hide-hint
|
hide-hint
|
||||||
hide-issues
|
hide-issues
|
||||||
:rows="3"
|
|
||||||
:is-read-only="readOnly"
|
:is-read-only="readOnly"
|
||||||
:parameter="leftParameter"
|
:parameter="leftParameter"
|
||||||
:value="condition.leftValue"
|
:value="condition.leftValue"
|
||||||
|
@ -181,7 +180,6 @@ const onBlur = (): void => {
|
||||||
hide-label
|
hide-label
|
||||||
hide-hint
|
hide-hint
|
||||||
hide-issues
|
hide-issues
|
||||||
:rows="3"
|
|
||||||
:is-read-only="readOnly"
|
:is-read-only="readOnly"
|
||||||
:options-position="breakpoint === 'default' ? 'top' : 'bottom'"
|
:options-position="breakpoint === 'default' ? 'top' : 'bottom'"
|
||||||
:parameter="rightParameter"
|
:parameter="rightParameter"
|
||||||
|
|
|
@ -45,21 +45,24 @@ const resolvedExpression = computed(() => {
|
||||||
});
|
});
|
||||||
|
|
||||||
const plaintextSegments = computed<Plaintext[]>(() => {
|
const plaintextSegments = computed<Plaintext[]>(() => {
|
||||||
if (props.segments.length === 0) {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
from: 0,
|
|
||||||
to: resolvedExpression.value.length - 1,
|
|
||||||
plaintext: resolvedExpression.value,
|
|
||||||
kind: 'plaintext',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
return props.segments.filter((s): s is Plaintext => s.kind === 'plaintext');
|
return props.segments.filter((s): s is Plaintext => s.kind === 'plaintext');
|
||||||
});
|
});
|
||||||
|
|
||||||
const resolvedSegments = computed<Resolved[]>(() => {
|
const resolvedSegments = computed<Resolved[]>(() => {
|
||||||
|
if (props.segments.length === 0) {
|
||||||
|
const emptyExpression = resolvedExpression.value;
|
||||||
|
const emptySegment: Resolved = {
|
||||||
|
from: 0,
|
||||||
|
to: emptyExpression.length,
|
||||||
|
kind: 'resolvable',
|
||||||
|
error: null,
|
||||||
|
resolvable: '',
|
||||||
|
resolved: emptyExpression,
|
||||||
|
state: 'pending',
|
||||||
|
};
|
||||||
|
return [emptySegment];
|
||||||
|
}
|
||||||
|
|
||||||
let cursor = 0;
|
let cursor = 0;
|
||||||
|
|
||||||
return props.segments
|
return props.segments
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<n8n-input-label
|
<n8n-input-label
|
||||||
:class="$style.wrapper"
|
:class="[$style.wrapper, { [$style.tipVisible]: showDragnDropTip }]"
|
||||||
:label="hideLabel ? '' : i18n.nodeText().inputLabelDisplayName(parameter, path)"
|
:label="hideLabel ? '' : i18n.nodeText().inputLabelDisplayName(parameter, path)"
|
||||||
:tooltip-text="hideLabel ? '' : i18n.nodeText().inputLabelDescription(parameter, path)"
|
:tooltip-text="hideLabel ? '' : i18n.nodeText().inputLabelDescription(parameter, path)"
|
||||||
:show-tooltip="focused"
|
:show-tooltip="focused"
|
||||||
|
@ -357,6 +357,11 @@ export default defineComponent({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tipVisible {
|
||||||
|
--input-border-bottom-left-radius: 0;
|
||||||
|
--input-border-bottom-right-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.tip {
|
.tip {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
|
|
|
@ -6,8 +6,6 @@ import { setActivePinia } from 'pinia';
|
||||||
import { waitFor } from '@testing-library/vue';
|
import { waitFor } from '@testing-library/vue';
|
||||||
|
|
||||||
describe('ExpressionParameterInput', () => {
|
describe('ExpressionParameterInput', () => {
|
||||||
const originalRangeGetBoundingClientRect = Range.prototype.getBoundingClientRect;
|
|
||||||
const originalRangeGetClientRects = Range.prototype.getClientRects;
|
|
||||||
const renderComponent = createComponentRenderer(ExpressionEditorModalInput);
|
const renderComponent = createComponentRenderer(ExpressionEditorModalInput);
|
||||||
let pinia: TestingPinia;
|
let pinia: TestingPinia;
|
||||||
|
|
||||||
|
@ -16,19 +14,6 @@ describe('ExpressionParameterInput', () => {
|
||||||
setActivePinia(pinia);
|
setActivePinia(pinia);
|
||||||
});
|
});
|
||||||
|
|
||||||
beforeAll(() => {
|
|
||||||
Range.prototype.getBoundingClientRect = vi.fn();
|
|
||||||
Range.prototype.getClientRects = () => ({
|
|
||||||
item: vi.fn(),
|
|
||||||
length: 0,
|
|
||||||
[Symbol.iterator]: vi.fn(),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
afterAll(() => {
|
|
||||||
Range.prototype.getBoundingClientRect = originalRangeGetBoundingClientRect;
|
|
||||||
Range.prototype.getClientRects = originalRangeGetClientRects;
|
|
||||||
});
|
|
||||||
test.each([
|
test.each([
|
||||||
['not be editable', 'readonly', true, ''],
|
['not be editable', 'readonly', true, ''],
|
||||||
['be editable', 'not readonly', false, 'test'],
|
['be editable', 'not readonly', false, 'test'],
|
||||||
|
|
|
@ -17,9 +17,6 @@ const DEFAULT_SETUP = {
|
||||||
};
|
};
|
||||||
|
|
||||||
describe('HtmlEditor.vue', () => {
|
describe('HtmlEditor.vue', () => {
|
||||||
const originalRangeGetBoundingClientRect = Range.prototype.getBoundingClientRect;
|
|
||||||
const originalRangeGetClientRects = Range.prototype.getClientRects;
|
|
||||||
|
|
||||||
const pinia = createTestingPinia({
|
const pinia = createTestingPinia({
|
||||||
initialState: {
|
initialState: {
|
||||||
[STORES.SETTINGS]: {
|
[STORES.SETTINGS]: {
|
||||||
|
@ -29,20 +26,6 @@ describe('HtmlEditor.vue', () => {
|
||||||
});
|
});
|
||||||
setActivePinia(pinia);
|
setActivePinia(pinia);
|
||||||
|
|
||||||
beforeAll(() => {
|
|
||||||
Range.prototype.getBoundingClientRect = vi.fn();
|
|
||||||
Range.prototype.getClientRects = () => ({
|
|
||||||
item: vi.fn(),
|
|
||||||
length: 0,
|
|
||||||
[Symbol.iterator]: vi.fn(),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
afterAll(() => {
|
|
||||||
Range.prototype.getBoundingClientRect = originalRangeGetBoundingClientRect;
|
|
||||||
Range.prototype.getClientRects = originalRangeGetClientRects;
|
|
||||||
});
|
|
||||||
|
|
||||||
afterAll(() => {
|
afterAll(() => {
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
});
|
});
|
||||||
|
|
|
@ -28,9 +28,6 @@ vi.mock('@/stores/n8nRoot.store', () => ({
|
||||||
}));
|
}));
|
||||||
|
|
||||||
describe('useAutocompleteTelemetry', () => {
|
describe('useAutocompleteTelemetry', () => {
|
||||||
const originalRangeGetBoundingClientRect = Range.prototype.getBoundingClientRect;
|
|
||||||
const originalRangeGetClientRects = Range.prototype.getClientRects;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
setActivePinia(createTestingPinia());
|
setActivePinia(createTestingPinia());
|
||||||
});
|
});
|
||||||
|
@ -39,20 +36,6 @@ describe('useAutocompleteTelemetry', () => {
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
beforeAll(() => {
|
|
||||||
Range.prototype.getBoundingClientRect = vi.fn();
|
|
||||||
Range.prototype.getClientRects = () => ({
|
|
||||||
item: vi.fn(),
|
|
||||||
length: 0,
|
|
||||||
[Symbol.iterator]: vi.fn(),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
afterAll(() => {
|
|
||||||
Range.prototype.getBoundingClientRect = originalRangeGetBoundingClientRect;
|
|
||||||
Range.prototype.getClientRects = originalRangeGetClientRects;
|
|
||||||
});
|
|
||||||
|
|
||||||
const getEditor = (defaultDoc = '') => {
|
const getEditor = (defaultDoc = '') => {
|
||||||
const extensionCompartment = new Compartment();
|
const extensionCompartment = new Compartment();
|
||||||
const state = EditorState.create({
|
const state = EditorState.create({
|
||||||
|
|
|
@ -21,9 +21,6 @@ vi.mock('@/stores/ndv.store', () => ({
|
||||||
}));
|
}));
|
||||||
|
|
||||||
describe('useExpressionEditor', () => {
|
describe('useExpressionEditor', () => {
|
||||||
const originalRangeGetBoundingClientRect = Range.prototype.getBoundingClientRect;
|
|
||||||
const originalRangeGetClientRects = Range.prototype.getClientRects;
|
|
||||||
|
|
||||||
const mockResolveExpression = () => {
|
const mockResolveExpression = () => {
|
||||||
const mock = vi.fn();
|
const mock = vi.fn();
|
||||||
vi.spyOn(workflowHelpers, 'useWorkflowHelpers').mockReturnValueOnce({
|
vi.spyOn(workflowHelpers, 'useWorkflowHelpers').mockReturnValueOnce({
|
||||||
|
@ -42,20 +39,6 @@ describe('useExpressionEditor', () => {
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
beforeAll(() => {
|
|
||||||
Range.prototype.getBoundingClientRect = vi.fn();
|
|
||||||
Range.prototype.getClientRects = () => ({
|
|
||||||
item: vi.fn(),
|
|
||||||
length: 0,
|
|
||||||
[Symbol.iterator]: vi.fn(),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
afterAll(() => {
|
|
||||||
Range.prototype.getBoundingClientRect = originalRangeGetBoundingClientRect;
|
|
||||||
Range.prototype.getClientRects = originalRangeGetClientRects;
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should create an editor', async () => {
|
test('should create an editor', async () => {
|
||||||
const root = ref<HTMLElement>();
|
const root = ref<HTMLElement>();
|
||||||
const { editor } = useExpressionEditor({
|
const { editor } = useExpressionEditor({
|
||||||
|
|
|
@ -185,7 +185,7 @@ export const useExpressionEditor = ({
|
||||||
if (editor.value) {
|
if (editor.value) {
|
||||||
editor.value.destroy();
|
editor.value.destroy();
|
||||||
}
|
}
|
||||||
editor.value = new EditorView({ parent, state });
|
editor.value = new EditorView({ parent, state, scrollTo: EditorView.scrollIntoView(0) });
|
||||||
debouncedUpdateSegments();
|
debouncedUpdateSegments();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -385,7 +385,8 @@ export const useExpressionEditor = ({
|
||||||
function setCursorPosition(pos: number | 'lastExpression' | 'end'): void {
|
function setCursorPosition(pos: number | 'lastExpression' | 'end'): void {
|
||||||
if (pos === 'lastExpression') {
|
if (pos === 'lastExpression') {
|
||||||
const END_OF_EXPRESSION = ' }}';
|
const END_OF_EXPRESSION = ' }}';
|
||||||
pos = Math.max(readEditorValue().lastIndexOf(END_OF_EXPRESSION), 0);
|
const endOfLastExpression = readEditorValue().lastIndexOf(END_OF_EXPRESSION);
|
||||||
|
pos = endOfLastExpression !== -1 ? endOfLastExpression : editor.value?.state.doc.length ?? 0;
|
||||||
} else if (pos === 'end') {
|
} else if (pos === 'end') {
|
||||||
pos = editor.value?.state.doc.length ?? 0;
|
pos = editor.value?.state.doc.length ?? 0;
|
||||||
}
|
}
|
||||||
|
|
|
@ -382,7 +382,10 @@ function parseJson(value: string): unknown {
|
||||||
try {
|
try {
|
||||||
return JSON.parse(value);
|
return JSON.parse(value);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return undefined;
|
if (value.includes("'")) {
|
||||||
|
throw new ExpressionExtensionError("Parsing failed. Check you're using double quotes");
|
||||||
|
}
|
||||||
|
throw new ExpressionExtensionError('Parsing failed');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -270,7 +270,13 @@ describe('Data Transformation Functions', () => {
|
||||||
test1: 1,
|
test1: 1,
|
||||||
test2: '2',
|
test2: '2',
|
||||||
});
|
});
|
||||||
expect(evaluate('={{ "hi".parseJson() }}')).toBeUndefined();
|
});
|
||||||
|
|
||||||
|
test('.parseJson should throw on invalid JSON', () => {
|
||||||
|
expect(() => evaluate("={{ \"{'test1':1,'test2':'2'}\".parseJson() }}")).toThrowError(
|
||||||
|
"Parsing failed. Check you're using double quotes",
|
||||||
|
);
|
||||||
|
expect(() => evaluate('={{ "No JSON here".parseJson() }}')).toThrowError('Parsing failed');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('.toBoolean should work on a string', () => {
|
test('.toBoolean should work on a string', () => {
|
||||||
|
|
Loading…
Reference in a new issue