From d7bfd45333cc9780ae5f1424f33de2093bd1a2f9 Mon Sep 17 00:00:00 2001 From: Elias Meire Date: Tue, 19 Mar 2024 18:10:14 +0100 Subject: [PATCH] feat(editor): Add type information to autocomplete dropdown (#8843) --- .../completions/__tests__/completions.test.ts | 67 ++++++++++++++++++- .../completions/datatype.completions.ts | 23 ++++++- .../src/styles/plugins/_codemirror.scss | 15 ++++- 3 files changed, 99 insertions(+), 6 deletions(-) diff --git a/packages/editor-ui/src/plugins/codemirror/completions/__tests__/completions.test.ts b/packages/editor-ui/src/plugins/codemirror/completions/__tests__/completions.test.ts index 3eef489d6274b..3a98b0cd59b89 100644 --- a/packages/editor-ui/src/plugins/codemirror/completions/__tests__/completions.test.ts +++ b/packages/editor-ui/src/plugins/codemirror/completions/__tests__/completions.test.ts @@ -63,10 +63,16 @@ describe('Top-level completions', () => { expect(result).toHaveLength(dollarOptions().length); expect(result?.[0]).toEqual( - expect.objectContaining({ label: '$json', section: RECOMMENDED_SECTION }), + expect.objectContaining({ + label: '$json', + section: RECOMMENDED_SECTION, + }), ); expect(result?.[4]).toEqual( - expect.objectContaining({ label: '$execution', section: METADATA_SECTION }), + expect.objectContaining({ + label: '$execution', + section: METADATA_SECTION, + }), ); expect(result?.[14]).toEqual( 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) { diff --git a/packages/editor-ui/src/plugins/codemirror/completions/datatype.completions.ts b/packages/editor-ui/src/plugins/codemirror/completions/datatype.completions.ts index 42657161ebfb7..ebbd63ba91b53 100644 --- a/packages/editor-ui/src/plugins/codemirror/completions/datatype.completions.ts +++ b/packages/editor-ui/src/plugins/codemirror/completions/datatype.completions.ts @@ -212,6 +212,24 @@ export const extensions = ( 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 = ( fnToDoc: FnToDoc, typeName: ExtensionTypeName, @@ -377,6 +395,7 @@ const objectOptions = (input: AutocompleteInput): Completion[] => { label: isFunction ? key + '()' : key, type: isFunction ? 'function' : 'keyword', section: getObjectPropertySection({ name, key, isFunction }), + detail: getDetail(name, resolvedProp), apply: applyCompletion(hasArgs, transformLabel), }; @@ -388,7 +407,7 @@ const objectOptions = (input: AutocompleteInput): Completion[] => { { doc: { name: key, - returnType: typeof resolvedProp, + returnType: getType(resolvedProp), description: i18n.proxyVars[infoKey], }, }, @@ -651,7 +670,7 @@ export const secretOptions = (base: string) => { return []; } return Object.entries(resolved).map(([secret, value]) => - createCompletionOption('Object', secret, 'keyword', { + createCompletionOption('', secret, 'keyword', { doc: { name: secret, returnType: typeof value, diff --git a/packages/editor-ui/src/styles/plugins/_codemirror.scss b/packages/editor-ui/src/styles/plugins/_codemirror.scss index cb9ad0ca9568a..c1eb3a1031390 100644 --- a/packages/editor-ui/src/styles/plugins/_codemirror.scss +++ b/packages/editor-ui/src/styles/plugins/_codemirror.scss @@ -30,15 +30,26 @@ li[role='option'] { color: var(--color-text-base); display: flex; - font-size: var(--font-size-2xs); + line-height: var(--font-line-height-xloose); justify-content: space-between; + align-items: center; padding: var(--spacing-5xs) var(--spacing-2xs); + gap: var(--spacing-2xs); scroll-padding: 40px; scroll-margin: 40px; } 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] {