mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -08:00
feat(editor): Add type information to autocomplete dropdown (#8843)
This commit is contained in:
parent
ff8dd4e604
commit
d7bfd45333
|
@ -63,10 +63,16 @@ describe('Top-level completions', () => {
|
||||||
expect(result).toHaveLength(dollarOptions().length);
|
expect(result).toHaveLength(dollarOptions().length);
|
||||||
|
|
||||||
expect(result?.[0]).toEqual(
|
expect(result?.[0]).toEqual(
|
||||||
expect.objectContaining({ label: '$json', section: RECOMMENDED_SECTION }),
|
expect.objectContaining({
|
||||||
|
label: '$json',
|
||||||
|
section: RECOMMENDED_SECTION,
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
expect(result?.[4]).toEqual(
|
expect(result?.[4]).toEqual(
|
||||||
expect.objectContaining({ label: '$execution', section: METADATA_SECTION }),
|
expect.objectContaining({
|
||||||
|
label: '$execution',
|
||||||
|
section: METADATA_SECTION,
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
expect(result?.[14]).toEqual(
|
expect(result?.[14]).toEqual(
|
||||||
expect.objectContaining({ label: '$max()', section: METHODS_SECTION }),
|
expect.objectContaining({ label: '$max()', section: METHODS_SECTION }),
|
||||||
|
@ -614,6 +620,63 @@ describe('Resolution-based completions', () => {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('type information', () => {
|
||||||
|
test('should display type information for: {{ $json.obj.| }}', () => {
|
||||||
|
vi.spyOn(workflowHelpers, 'resolveParameter').mockReturnValueOnce({
|
||||||
|
str: 'bar',
|
||||||
|
empty: null,
|
||||||
|
arr: [],
|
||||||
|
obj: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = completions('{{ $json.obj.| }}');
|
||||||
|
expect(result).toContainEqual(expect.objectContaining({ label: 'str', detail: 'string' }));
|
||||||
|
expect(result).toContainEqual(expect.objectContaining({ label: 'empty', detail: 'null' }));
|
||||||
|
expect(result).toContainEqual(expect.objectContaining({ label: 'arr', detail: 'array' }));
|
||||||
|
expect(result).toContainEqual(expect.objectContaining({ label: 'obj', detail: 'object' }));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should display type information for: {{ $input.item.json.| }}', () => {
|
||||||
|
vi.spyOn(workflowHelpers, 'resolveParameter').mockReturnValueOnce({
|
||||||
|
str: 'bar',
|
||||||
|
empty: null,
|
||||||
|
arr: [],
|
||||||
|
obj: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = completions('{{ $json.item.json.| }}');
|
||||||
|
expect(result).toContainEqual(expect.objectContaining({ label: 'str', detail: 'string' }));
|
||||||
|
expect(result).toContainEqual(expect.objectContaining({ label: 'empty', detail: 'null' }));
|
||||||
|
expect(result).toContainEqual(expect.objectContaining({ label: 'arr', detail: 'array' }));
|
||||||
|
expect(result).toContainEqual(expect.objectContaining({ label: 'obj', detail: 'object' }));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should display type information for: {{ $("My Node").item.json.| }}', () => {
|
||||||
|
vi.spyOn(workflowHelpers, 'resolveParameter').mockReturnValueOnce({
|
||||||
|
str: 'bar',
|
||||||
|
empty: null,
|
||||||
|
arr: [],
|
||||||
|
obj: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = completions('{{ $("My Node").item.json.| }}');
|
||||||
|
expect(result).toContainEqual(expect.objectContaining({ label: 'str', detail: 'string' }));
|
||||||
|
expect(result).toContainEqual(expect.objectContaining({ label: 'empty', detail: 'null' }));
|
||||||
|
expect(result).toContainEqual(expect.objectContaining({ label: 'arr', detail: 'array' }));
|
||||||
|
expect(result).toContainEqual(expect.objectContaining({ label: 'obj', detail: 'object' }));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should not display type information for other completions', () => {
|
||||||
|
vi.spyOn(workflowHelpers, 'resolveParameter').mockReturnValue({
|
||||||
|
str: 'bar',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(completions('{{ $execution.| }}')?.every((item) => !item.detail)).toBe(true);
|
||||||
|
expect(completions('{{ $input.params.| }}')?.every((item) => !item.detail)).toBe(true);
|
||||||
|
expect(completions('{{ $("My Node").| }}')?.every((item) => !item.detail)).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
export function completions(docWithCursor: string, explicit = false) {
|
export function completions(docWithCursor: string, explicit = false) {
|
||||||
|
|
|
@ -212,6 +212,24 @@ export const extensions = (
|
||||||
return toOptions(fnToDoc, typeName, 'extension-function', includeHidden, transformLabel);
|
return toOptions(fnToDoc, typeName, 'extension-function', includeHidden, transformLabel);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getType = (value: unknown): string => {
|
||||||
|
if (Array.isArray(value)) return 'array';
|
||||||
|
if (value === null) return 'null';
|
||||||
|
return (typeof value).toLocaleLowerCase();
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isInputData = (base: string): boolean => {
|
||||||
|
return (
|
||||||
|
/^\$input\..*\.json]/.test(base) || /^\$json/.test(base) || /^\$\(.*\)\..*\.json/.test(base)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getDetail = (base: string, value: unknown): string | undefined => {
|
||||||
|
const type = getType(value);
|
||||||
|
if (!isInputData(base) || type === 'function') return undefined;
|
||||||
|
return type;
|
||||||
|
};
|
||||||
|
|
||||||
export const toOptions = (
|
export const toOptions = (
|
||||||
fnToDoc: FnToDoc,
|
fnToDoc: FnToDoc,
|
||||||
typeName: ExtensionTypeName,
|
typeName: ExtensionTypeName,
|
||||||
|
@ -377,6 +395,7 @@ const objectOptions = (input: AutocompleteInput<IDataObject>): Completion[] => {
|
||||||
label: isFunction ? key + '()' : key,
|
label: isFunction ? key + '()' : key,
|
||||||
type: isFunction ? 'function' : 'keyword',
|
type: isFunction ? 'function' : 'keyword',
|
||||||
section: getObjectPropertySection({ name, key, isFunction }),
|
section: getObjectPropertySection({ name, key, isFunction }),
|
||||||
|
detail: getDetail(name, resolvedProp),
|
||||||
apply: applyCompletion(hasArgs, transformLabel),
|
apply: applyCompletion(hasArgs, transformLabel),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -388,7 +407,7 @@ const objectOptions = (input: AutocompleteInput<IDataObject>): Completion[] => {
|
||||||
{
|
{
|
||||||
doc: {
|
doc: {
|
||||||
name: key,
|
name: key,
|
||||||
returnType: typeof resolvedProp,
|
returnType: getType(resolvedProp),
|
||||||
description: i18n.proxyVars[infoKey],
|
description: i18n.proxyVars[infoKey],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -651,7 +670,7 @@ export const secretOptions = (base: string) => {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
return Object.entries(resolved).map(([secret, value]) =>
|
return Object.entries(resolved).map(([secret, value]) =>
|
||||||
createCompletionOption('Object', secret, 'keyword', {
|
createCompletionOption('', secret, 'keyword', {
|
||||||
doc: {
|
doc: {
|
||||||
name: secret,
|
name: secret,
|
||||||
returnType: typeof value,
|
returnType: typeof value,
|
||||||
|
|
|
@ -30,15 +30,26 @@
|
||||||
li[role='option'] {
|
li[role='option'] {
|
||||||
color: var(--color-text-base);
|
color: var(--color-text-base);
|
||||||
display: flex;
|
display: flex;
|
||||||
font-size: var(--font-size-2xs);
|
line-height: var(--font-line-height-xloose);
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
padding: var(--spacing-5xs) var(--spacing-2xs);
|
padding: var(--spacing-5xs) var(--spacing-2xs);
|
||||||
|
gap: var(--spacing-2xs);
|
||||||
scroll-padding: 40px;
|
scroll-padding: 40px;
|
||||||
scroll-margin: 40px;
|
scroll-margin: 40px;
|
||||||
}
|
}
|
||||||
|
|
||||||
li .cm-completionLabel {
|
li .cm-completionLabel {
|
||||||
line-height: var(--font-line-height-xloose);
|
font-size: var(--font-size-2xs);
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
li .cm-completionDetail {
|
||||||
|
color: var(--color-text-light);
|
||||||
|
font-size: var(--font-size-3xs);
|
||||||
|
margin-left: 0;
|
||||||
|
font-style: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
li[aria-selected] {
|
li[aria-selected] {
|
||||||
|
|
Loading…
Reference in a new issue