feat(editor): Autocomplete info box: improve structure and add examples (#9019)
Co-authored-by: Giulio Andreini <andreini@netseven.it>
This commit is contained in:
@@ -130,8 +130,10 @@ describe('Luxon method completions', () => {
|
||||
vi.spyOn(workflowHelpers, 'resolveParameter').mockReturnValueOnce(DateTime.now());
|
||||
|
||||
expect(completions('{{ $now.| }}')).toHaveLength(
|
||||
uniqBy(luxonInstanceOptions().concat(extensions('date')), (option) => option.label).length +
|
||||
LUXON_RECOMMENDED_OPTIONS.length,
|
||||
uniqBy(
|
||||
luxonInstanceOptions().concat(extensions({ typeName: 'date' })),
|
||||
(option) => option.label,
|
||||
).length + LUXON_RECOMMENDED_OPTIONS.length,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -140,8 +142,10 @@ describe('Luxon method completions', () => {
|
||||
vi.spyOn(workflowHelpers, 'resolveParameter').mockReturnValueOnce(DateTime.now());
|
||||
|
||||
expect(completions('{{ $today.| }}')).toHaveLength(
|
||||
uniqBy(luxonInstanceOptions().concat(extensions('date')), (option) => option.label).length +
|
||||
LUXON_RECOMMENDED_OPTIONS.length,
|
||||
uniqBy(
|
||||
luxonInstanceOptions().concat(extensions({ typeName: 'date' })),
|
||||
(option) => option.label,
|
||||
).length + LUXON_RECOMMENDED_OPTIONS.length,
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -153,7 +157,9 @@ describe('Resolution-based completions', () => {
|
||||
vi.spyOn(workflowHelpers, 'resolveParameter').mockReturnValueOnce('abc');
|
||||
|
||||
expect(completions('{{ "abc".| }}')).toHaveLength(
|
||||
natives('string').length + extensions('string').length + STRING_RECOMMENDED_OPTIONS.length,
|
||||
natives({ typeName: 'string' }).length +
|
||||
extensions({ typeName: 'string' }).length +
|
||||
STRING_RECOMMENDED_OPTIONS.length,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -163,7 +169,9 @@ describe('Resolution-based completions', () => {
|
||||
|
||||
const result = completions('{{ "You \'owe\' me 200$".| }}');
|
||||
|
||||
expect(result).toHaveLength(natives('string').length + extensions('string').length + 1);
|
||||
expect(result).toHaveLength(
|
||||
natives({ typeName: 'string' }).length + extensions({ typeName: 'string' }).length + 1,
|
||||
);
|
||||
});
|
||||
|
||||
test('should return completions for number literal: {{ (123).| }}', () => {
|
||||
@@ -171,7 +179,9 @@ describe('Resolution-based completions', () => {
|
||||
vi.spyOn(workflowHelpers, 'resolveParameter').mockReturnValueOnce(123);
|
||||
|
||||
expect(completions('{{ (123).| }}')).toHaveLength(
|
||||
natives('number').length + extensions('number').length + ['isEven()', 'isOdd()'].length,
|
||||
natives({ typeName: 'number' }).length +
|
||||
extensions({ typeName: 'number' }).length +
|
||||
['isEven()', 'isOdd()'].length,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -180,7 +190,7 @@ describe('Resolution-based completions', () => {
|
||||
vi.spyOn(workflowHelpers, 'resolveParameter').mockReturnValueOnce([1, 2, 3]);
|
||||
|
||||
expect(completions('{{ [1, 2, 3].| }}')).toHaveLength(
|
||||
natives('array').length + extensions('array').length,
|
||||
natives({ typeName: 'array' }).length + extensions({ typeName: 'array' }).length,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -192,7 +202,9 @@ describe('Resolution-based completions', () => {
|
||||
|
||||
if (!found) throw new Error('Expected to find completion');
|
||||
|
||||
expect(found).toHaveLength(natives('array').length + extensions('array').length);
|
||||
expect(found).toHaveLength(
|
||||
natives({ typeName: 'array' }).length + extensions({ typeName: 'array' }).length,
|
||||
);
|
||||
});
|
||||
|
||||
test('should return completions for object literal', () => {
|
||||
@@ -201,7 +213,7 @@ describe('Resolution-based completions', () => {
|
||||
vi.spyOn(workflowHelpers, 'resolveParameter').mockReturnValueOnce(object);
|
||||
|
||||
expect(completions('{{ ({ a: 1 }).| }}')).toHaveLength(
|
||||
Object.keys(object).length + extensions('object').length,
|
||||
Object.keys(object).length + extensions({ typeName: 'object' }).length,
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -212,7 +224,9 @@ describe('Resolution-based completions', () => {
|
||||
vi.spyOn(workflowHelpers, 'resolveParameter').mockReturnValueOnce('a');
|
||||
|
||||
expect(completions('{{ "abc"[0].| }}')).toHaveLength(
|
||||
natives('string').length + extensions('string').length + STRING_RECOMMENDED_OPTIONS.length,
|
||||
natives({ typeName: 'string' }).length +
|
||||
extensions({ typeName: 'string' }).length +
|
||||
STRING_RECOMMENDED_OPTIONS.length,
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -225,7 +239,9 @@ describe('Resolution-based completions', () => {
|
||||
const found = completions('{{ Math.abs($input.item.json.num1).| }}');
|
||||
if (!found) throw new Error('Expected to find completions');
|
||||
expect(found).toHaveLength(
|
||||
extensions('number').length + natives('number').length + ['isEven()', 'isOdd()'].length,
|
||||
extensions({ typeName: 'number' }).length +
|
||||
natives({ typeName: 'number' }).length +
|
||||
['isEven()', 'isOdd()'].length,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -240,8 +256,10 @@ describe('Resolution-based completions', () => {
|
||||
test('should return completions for complex expression: {{ $now.diff($now.diff($now.|)) }}', () => {
|
||||
vi.spyOn(workflowHelpers, 'resolveParameter').mockReturnValueOnce(DateTime.now());
|
||||
expect(completions('{{ $now.diff($now.diff($now.|)) }}')).toHaveLength(
|
||||
uniqBy(luxonInstanceOptions().concat(extensions('date')), (option) => option.label).length +
|
||||
LUXON_RECOMMENDED_OPTIONS.length,
|
||||
uniqBy(
|
||||
luxonInstanceOptions().concat(extensions({ typeName: 'date' })),
|
||||
(option) => option.label,
|
||||
).length + LUXON_RECOMMENDED_OPTIONS.length,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -251,7 +269,9 @@ describe('Resolution-based completions', () => {
|
||||
const found = completions('{{ $execution.resumeUrl.includes($json.|) }}');
|
||||
|
||||
if (!found) throw new Error('Expected to find completions');
|
||||
expect(found).toHaveLength(Object.keys($json).length + extensions('object').length);
|
||||
expect(found).toHaveLength(
|
||||
Object.keys($json).length + extensions({ typeName: 'object' }).length,
|
||||
);
|
||||
});
|
||||
|
||||
test('should return completions for operation expression: {{ $now.day + $json. }}', () => {
|
||||
@@ -261,7 +281,9 @@ describe('Resolution-based completions', () => {
|
||||
|
||||
if (!found) throw new Error('Expected to find completions');
|
||||
|
||||
expect(found).toHaveLength(Object.keys($json).length + extensions('object').length);
|
||||
expect(found).toHaveLength(
|
||||
Object.keys($json).length + extensions({ typeName: 'object' }).length,
|
||||
);
|
||||
});
|
||||
|
||||
test('should return completions for operation expression: {{ Math.abs($now.day) >= 10 ? $now : Math.abs($json.). }}', () => {
|
||||
@@ -271,7 +293,9 @@ describe('Resolution-based completions', () => {
|
||||
|
||||
if (!found) throw new Error('Expected to find completions');
|
||||
|
||||
expect(found).toHaveLength(Object.keys($json).length + extensions('object').length);
|
||||
expect(found).toHaveLength(
|
||||
Object.keys($json).length + extensions({ typeName: 'object' }).length,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -286,7 +310,9 @@ describe('Resolution-based completions', () => {
|
||||
if (!found) throw new Error('Expected to find completions');
|
||||
|
||||
expect(found).toHaveLength(
|
||||
extensions('string').length + natives('string').length + STRING_RECOMMENDED_OPTIONS.length,
|
||||
extensions({ typeName: 'string' }).length +
|
||||
natives({ typeName: 'string' }).length +
|
||||
STRING_RECOMMENDED_OPTIONS.length,
|
||||
);
|
||||
expect(found.map((c) => c.label).every((l) => !l.endsWith('()')));
|
||||
});
|
||||
@@ -299,7 +325,9 @@ describe('Resolution-based completions', () => {
|
||||
if (!found) throw new Error('Expected to find completions');
|
||||
|
||||
expect(found).toHaveLength(
|
||||
extensions('number').length + natives('number').length + ['isEven()', 'isOdd()'].length,
|
||||
extensions({ typeName: 'number' }).length +
|
||||
natives({ typeName: 'number' }).length +
|
||||
['isEven()', 'isOdd()'].length,
|
||||
);
|
||||
expect(found.map((c) => c.label).every((l) => !l.endsWith('()')));
|
||||
});
|
||||
@@ -311,7 +339,9 @@ describe('Resolution-based completions', () => {
|
||||
|
||||
if (!found) throw new Error('Expected to find completions');
|
||||
|
||||
expect(found).toHaveLength(extensions('array').length + natives('array').length);
|
||||
expect(found).toHaveLength(
|
||||
extensions({ typeName: 'array' }).length + natives({ typeName: 'array' }).length,
|
||||
);
|
||||
expect(found.map((c) => c.label).every((l) => !l.endsWith('()')));
|
||||
});
|
||||
});
|
||||
@@ -339,7 +369,6 @@ describe('Resolution-based completions', () => {
|
||||
{
|
||||
info: expect.any(Function),
|
||||
label: provider,
|
||||
type: 'keyword',
|
||||
apply: expect.any(Function),
|
||||
},
|
||||
]);
|
||||
@@ -363,13 +392,11 @@ describe('Resolution-based completions', () => {
|
||||
{
|
||||
info: expect.any(Function),
|
||||
label: secrets[0],
|
||||
type: 'keyword',
|
||||
apply: expect.any(Function),
|
||||
},
|
||||
{
|
||||
info: expect.any(Function),
|
||||
label: secrets[1],
|
||||
type: 'keyword',
|
||||
apply: expect.any(Function),
|
||||
},
|
||||
]);
|
||||
@@ -445,7 +472,9 @@ describe('Resolution-based completions', () => {
|
||||
vi.spyOn(workflowHelpers, 'resolveParameter').mockReturnValue([$input.item]);
|
||||
|
||||
expect(completions('{{ $input.all().| }}')).toHaveLength(
|
||||
extensions('array').length + natives('array').length - ARRAY_NUMBER_ONLY_METHODS.length,
|
||||
extensions({ typeName: 'array' }).length +
|
||||
natives({ typeName: 'array' }).length -
|
||||
ARRAY_NUMBER_ONLY_METHODS.length,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -453,7 +482,7 @@ describe('Resolution-based completions', () => {
|
||||
vi.spyOn(workflowHelpers, 'resolveParameter').mockReturnValue($input.item.json);
|
||||
|
||||
expect(completions('{{ $input.item.| }}')).toHaveLength(
|
||||
Object.keys($input.item.json).length + extensions('object').length,
|
||||
Object.keys($input.item.json).length + extensions({ typeName: 'object' }).length,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -461,7 +490,7 @@ describe('Resolution-based completions', () => {
|
||||
vi.spyOn(workflowHelpers, 'resolveParameter').mockReturnValue($input.first().json);
|
||||
|
||||
expect(completions('{{ $input.first().| }}')).toHaveLength(
|
||||
Object.keys($input.first().json).length + extensions('object').length,
|
||||
Object.keys($input.first().json).length + extensions({ typeName: 'object' }).length,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -469,7 +498,7 @@ describe('Resolution-based completions', () => {
|
||||
vi.spyOn(workflowHelpers, 'resolveParameter').mockReturnValue($input.last().json);
|
||||
|
||||
expect(completions('{{ $input.last().| }}')).toHaveLength(
|
||||
Object.keys($input.last().json).length + extensions('object').length,
|
||||
Object.keys($input.last().json).length + extensions({ typeName: 'object' }).length,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -477,7 +506,7 @@ describe('Resolution-based completions', () => {
|
||||
vi.spyOn(workflowHelpers, 'resolveParameter').mockReturnValue($input.all()[0].json);
|
||||
|
||||
expect(completions('{{ $input.all()[0].| }}')).toHaveLength(
|
||||
Object.keys($input.all()[0].json).length + extensions('object').length,
|
||||
Object.keys($input.all()[0].json).length + extensions({ typeName: 'object' }).length,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -485,7 +514,9 @@ describe('Resolution-based completions', () => {
|
||||
vi.spyOn(workflowHelpers, 'resolveParameter').mockReturnValue($input.item.json.str);
|
||||
|
||||
expect(completions('{{ $input.item.json.str.| }}')).toHaveLength(
|
||||
extensions('string').length + natives('string').length + STRING_RECOMMENDED_OPTIONS.length,
|
||||
extensions({ typeName: 'string' }).length +
|
||||
natives({ typeName: 'string' }).length +
|
||||
STRING_RECOMMENDED_OPTIONS.length,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -493,7 +524,9 @@ describe('Resolution-based completions', () => {
|
||||
vi.spyOn(workflowHelpers, 'resolveParameter').mockReturnValue($input.item.json.num);
|
||||
|
||||
expect(completions('{{ $input.item.json.num.| }}')).toHaveLength(
|
||||
extensions('number').length + natives('number').length + ['isEven()', 'isOdd()'].length,
|
||||
extensions({ typeName: 'number' }).length +
|
||||
natives({ typeName: 'number' }).length +
|
||||
['isEven()', 'isOdd()'].length,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -501,7 +534,7 @@ describe('Resolution-based completions', () => {
|
||||
vi.spyOn(workflowHelpers, 'resolveParameter').mockReturnValue($input.item.json.arr);
|
||||
|
||||
expect(completions('{{ $input.item.json.arr.| }}')).toHaveLength(
|
||||
extensions('array').length + natives('array').length,
|
||||
extensions({ typeName: 'array' }).length + natives({ typeName: 'array' }).length,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -509,7 +542,7 @@ describe('Resolution-based completions', () => {
|
||||
vi.spyOn(workflowHelpers, 'resolveParameter').mockReturnValue($input.item.json.obj);
|
||||
|
||||
expect(completions('{{ $input.item.json.obj.| }}')).toHaveLength(
|
||||
Object.keys($input.item.json.obj).length + extensions('object').length,
|
||||
Object.keys($input.item.json.obj).length + extensions({ typeName: 'object' }).length,
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -591,15 +624,12 @@ describe('Resolution-based completions', () => {
|
||||
);
|
||||
});
|
||||
|
||||
test('should recommend toInt(),toFloat() for: {{ "5.3".| }}', () => {
|
||||
test('should recommend toNumber() for: {{ "5.3".| }}', () => {
|
||||
// @ts-expect-error Spied function is mistyped
|
||||
vi.spyOn(workflowHelpers, 'resolveParameter').mockReturnValueOnce('5.3');
|
||||
const options = completions('{{ "5.3".| }}');
|
||||
expect(options?.[0]).toEqual(
|
||||
expect.objectContaining({ label: 'toInt()', section: RECOMMENDED_SECTION }),
|
||||
);
|
||||
expect(options?.[1]).toEqual(
|
||||
expect.objectContaining({ label: 'toFloat()', section: RECOMMENDED_SECTION }),
|
||||
expect.objectContaining({ label: 'toNumber()', section: RECOMMENDED_SECTION }),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -659,25 +689,25 @@ describe('Resolution-based completions', () => {
|
||||
);
|
||||
});
|
||||
|
||||
test('should recommend toDateTime("s") for: {{ (1900062210).| }}', () => {
|
||||
test("should recommend toDateTime('s') for: {{ (1900062210).| }}", () => {
|
||||
vi.spyOn(workflowHelpers, 'resolveParameter').mockReturnValueOnce(
|
||||
// @ts-expect-error Spied function is mistyped
|
||||
1900062210,
|
||||
);
|
||||
const options = completions('{{ (1900062210).| }}');
|
||||
expect(options?.[0]).toEqual(
|
||||
expect.objectContaining({ label: 'toDateTime("s")', section: RECOMMENDED_SECTION }),
|
||||
expect.objectContaining({ label: "toDateTime('s')", section: RECOMMENDED_SECTION }),
|
||||
);
|
||||
});
|
||||
|
||||
test('should recommend toDateTime("ms") for: {{ (1900062210000).| }}', () => {
|
||||
test("should recommend toDateTime('ms') for: {{ (1900062210000).| }}", () => {
|
||||
vi.spyOn(workflowHelpers, 'resolveParameter').mockReturnValueOnce(
|
||||
// @ts-expect-error Spied function is mistyped
|
||||
1900062210000,
|
||||
);
|
||||
const options = completions('{{ (1900062210000).| }}');
|
||||
expect(options?.[0]).toEqual(
|
||||
expect.objectContaining({ label: 'toDateTime("ms")', section: RECOMMENDED_SECTION }),
|
||||
expect.objectContaining({ label: "toDateTime('ms')", section: RECOMMENDED_SECTION }),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -714,7 +744,9 @@ describe('Resolution-based completions', () => {
|
||||
|
||||
const result = completions('{{ $json.foo| }}', true);
|
||||
expect(result).toHaveLength(
|
||||
extensions('string').length + natives('string').length + STRING_RECOMMENDED_OPTIONS.length,
|
||||
extensions({ typeName: 'string' }).length +
|
||||
natives({ typeName: 'string' }).length +
|
||||
STRING_RECOMMENDED_OPTIONS.length,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { Completion, CompletionSection } from '@codemirror/autocomplete';
|
||||
import { i18n } from '@/plugins/i18n';
|
||||
import { withSectionHeader } from './utils';
|
||||
import { createInfoBoxRenderer } from './infoBoxRenderer';
|
||||
|
||||
export const FIELDS_SECTION: CompletionSection = withSectionHeader({
|
||||
name: i18n.baseText('codeNodeEditor.completer.section.fields'),
|
||||
@@ -51,87 +52,234 @@ export const ROOT_DOLLAR_COMPLETIONS: Completion[] = [
|
||||
{
|
||||
label: '$json',
|
||||
section: RECOMMENDED_SECTION,
|
||||
info: i18n.rootVars.$json,
|
||||
info: createInfoBoxRenderer({
|
||||
name: '$json',
|
||||
returnType: 'object',
|
||||
description: i18n.rootVars.$json,
|
||||
docURL: 'https://docs.n8n.io/data/data-structure/',
|
||||
}),
|
||||
},
|
||||
{
|
||||
label: '$binary',
|
||||
section: RECOMMENDED_SECTION,
|
||||
info: i18n.rootVars.$binary,
|
||||
info: createInfoBoxRenderer({
|
||||
name: '$binary',
|
||||
returnType: 'object',
|
||||
description: i18n.rootVars.$binary,
|
||||
}),
|
||||
},
|
||||
{
|
||||
label: '$now',
|
||||
section: RECOMMENDED_SECTION,
|
||||
info: i18n.rootVars.$now,
|
||||
info: createInfoBoxRenderer({
|
||||
name: '$now',
|
||||
returnType: 'DateTime',
|
||||
description: i18n.rootVars.$now,
|
||||
}),
|
||||
},
|
||||
{
|
||||
label: '$if()',
|
||||
section: RECOMMENDED_SECTION,
|
||||
info: i18n.rootVars.$if,
|
||||
info: createInfoBoxRenderer(
|
||||
{
|
||||
name: '$if',
|
||||
returnType: 'boolean',
|
||||
description: i18n.rootVars.$if,
|
||||
args: [
|
||||
{
|
||||
name: 'condition',
|
||||
optional: false,
|
||||
type: 'boolean',
|
||||
},
|
||||
{
|
||||
name: 'valueIfTrue',
|
||||
optional: false,
|
||||
type: 'any',
|
||||
},
|
||||
{
|
||||
name: 'valueIfFalse',
|
||||
optional: false,
|
||||
type: 'any',
|
||||
},
|
||||
],
|
||||
},
|
||||
true,
|
||||
),
|
||||
},
|
||||
{
|
||||
label: '$ifEmpty()',
|
||||
section: RECOMMENDED_SECTION,
|
||||
info: i18n.rootVars.$ifEmpty,
|
||||
info: createInfoBoxRenderer(
|
||||
{
|
||||
name: '$ifEmpty',
|
||||
returnType: 'boolean',
|
||||
description: i18n.rootVars.$ifEmpty,
|
||||
args: [
|
||||
{
|
||||
name: 'value',
|
||||
optional: false,
|
||||
type: 'any',
|
||||
},
|
||||
{
|
||||
name: 'valueIfEmpty',
|
||||
optional: false,
|
||||
type: 'any',
|
||||
},
|
||||
],
|
||||
},
|
||||
true,
|
||||
),
|
||||
},
|
||||
{
|
||||
label: '$execution',
|
||||
section: METADATA_SECTION,
|
||||
info: i18n.rootVars.$execution,
|
||||
info: createInfoBoxRenderer({
|
||||
name: '$execution',
|
||||
returnType: 'object',
|
||||
description: i18n.rootVars.$execution,
|
||||
}),
|
||||
},
|
||||
{
|
||||
label: '$itemIndex',
|
||||
section: METADATA_SECTION,
|
||||
info: i18n.rootVars.$itemIndex,
|
||||
info: createInfoBoxRenderer({
|
||||
name: '$itemIndex',
|
||||
returnType: 'number',
|
||||
description: i18n.rootVars.$itemIndex,
|
||||
}),
|
||||
},
|
||||
{
|
||||
label: '$input',
|
||||
section: METADATA_SECTION,
|
||||
info: i18n.rootVars.$input,
|
||||
info: createInfoBoxRenderer({
|
||||
name: '$input',
|
||||
returnType: 'object',
|
||||
description: i18n.rootVars.$input,
|
||||
}),
|
||||
},
|
||||
{
|
||||
label: '$parameter',
|
||||
section: METADATA_SECTION,
|
||||
info: i18n.rootVars.$parameter,
|
||||
info: createInfoBoxRenderer({
|
||||
name: '$parameter',
|
||||
returnType: 'object',
|
||||
description: i18n.rootVars.$parameter,
|
||||
}),
|
||||
},
|
||||
{
|
||||
label: '$prevNode',
|
||||
section: METADATA_SECTION,
|
||||
info: i18n.rootVars.$prevNode,
|
||||
info: createInfoBoxRenderer({
|
||||
name: '$prevNode',
|
||||
returnType: 'object',
|
||||
description: i18n.rootVars.$prevNode,
|
||||
}),
|
||||
},
|
||||
{
|
||||
label: '$runIndex',
|
||||
section: METADATA_SECTION,
|
||||
info: i18n.rootVars.$runIndex,
|
||||
info: createInfoBoxRenderer({
|
||||
name: '$runIndex',
|
||||
returnType: 'number',
|
||||
description: i18n.rootVars.$runIndex,
|
||||
}),
|
||||
},
|
||||
{
|
||||
label: '$today',
|
||||
section: METADATA_SECTION,
|
||||
info: i18n.rootVars.$today,
|
||||
info: createInfoBoxRenderer({
|
||||
name: '$today',
|
||||
returnType: 'DateTime',
|
||||
description: i18n.rootVars.$today,
|
||||
}),
|
||||
},
|
||||
{
|
||||
label: '$vars',
|
||||
section: METADATA_SECTION,
|
||||
info: i18n.rootVars.$vars,
|
||||
info: createInfoBoxRenderer({
|
||||
name: '$vars',
|
||||
returnType: 'object',
|
||||
description: i18n.rootVars.$vars,
|
||||
}),
|
||||
},
|
||||
{
|
||||
label: '$workflow',
|
||||
section: METADATA_SECTION,
|
||||
info: i18n.rootVars.$workflow,
|
||||
info: createInfoBoxRenderer({
|
||||
name: '$workflow',
|
||||
returnType: 'object',
|
||||
description: i18n.rootVars.$workflow,
|
||||
}),
|
||||
},
|
||||
{
|
||||
label: '$jmespath()',
|
||||
section: METHODS_SECTION,
|
||||
info: i18n.rootVars.$jmespath,
|
||||
info: createInfoBoxRenderer(
|
||||
{
|
||||
name: '$jmespath',
|
||||
returnType: 'any',
|
||||
description: i18n.rootVars.$jmespath,
|
||||
},
|
||||
true,
|
||||
),
|
||||
},
|
||||
{
|
||||
label: '$max()',
|
||||
section: METHODS_SECTION,
|
||||
info: i18n.rootVars.$max,
|
||||
info: createInfoBoxRenderer(
|
||||
{
|
||||
name: '$max',
|
||||
returnType: 'number',
|
||||
description: i18n.rootVars.$max,
|
||||
args: [
|
||||
{
|
||||
name: 'number1',
|
||||
optional: false,
|
||||
type: 'number',
|
||||
},
|
||||
{
|
||||
name: 'number2',
|
||||
optional: true,
|
||||
type: 'number',
|
||||
},
|
||||
{
|
||||
name: 'numberN',
|
||||
optional: true,
|
||||
type: 'number',
|
||||
},
|
||||
],
|
||||
},
|
||||
true,
|
||||
),
|
||||
},
|
||||
{
|
||||
label: '$min()',
|
||||
section: METHODS_SECTION,
|
||||
info: i18n.rootVars.$min,
|
||||
info: createInfoBoxRenderer(
|
||||
{
|
||||
name: '$min',
|
||||
returnType: 'number',
|
||||
description: i18n.rootVars.$min,
|
||||
args: [
|
||||
{
|
||||
name: 'number1',
|
||||
optional: false,
|
||||
type: 'number',
|
||||
},
|
||||
{
|
||||
name: 'number2',
|
||||
optional: true,
|
||||
type: 'number',
|
||||
},
|
||||
{
|
||||
name: 'numberN',
|
||||
optional: true,
|
||||
type: 'number',
|
||||
},
|
||||
],
|
||||
},
|
||||
true,
|
||||
),
|
||||
},
|
||||
{
|
||||
label: '$nodeVersion',
|
||||
@@ -148,7 +296,6 @@ export const STRING_RECOMMENDED_OPTIONS = [
|
||||
'length',
|
||||
];
|
||||
|
||||
export const DATE_RECOMMENDED_OPTIONS = ['format()', 'minus()', 'plus()', 'extract()'];
|
||||
export const LUXON_RECOMMENDED_OPTIONS = ['format()', 'minus()', 'plus()', 'diff()', 'extract()'];
|
||||
export const OBJECT_RECOMMENDED_OPTIONS = ['keys()', 'values()', 'isEmpty()', 'hasField()'];
|
||||
export const ARRAY_RECOMMENDED_OPTIONS = ['length', 'last()', 'includes()', 'map()', 'filter()'];
|
||||
|
||||
@@ -1,50 +1,22 @@
|
||||
import type { IDataObject, DocMetadata, NativeDoc } from 'n8n-workflow';
|
||||
import { Expression, ExpressionExtensions, NativeMethods, validateFieldType } from 'n8n-workflow';
|
||||
import { DateTime } from 'luxon';
|
||||
import { i18n } from '@/plugins/i18n';
|
||||
import { resolveParameter } from '@/composables/useWorkflowHelpers';
|
||||
import {
|
||||
setRank,
|
||||
hasNoParams,
|
||||
prefixMatch,
|
||||
isAllowedInDotNotation,
|
||||
isSplitInBatchesAbsent,
|
||||
longestCommonPrefix,
|
||||
splitBaseTail,
|
||||
isPseudoParam,
|
||||
stripExcessParens,
|
||||
isCredentialsModalOpen,
|
||||
applyCompletion,
|
||||
sortCompletionsAlpha,
|
||||
hasRequiredArgs,
|
||||
getDefaultArgs,
|
||||
insertDefaultArgs,
|
||||
applyBracketAccessCompletion,
|
||||
applyBracketAccess,
|
||||
} from './utils';
|
||||
import { VALID_EMAIL_REGEX } from '@/constants';
|
||||
import { i18n } from '@/plugins/i18n';
|
||||
import { useEnvironmentsStore } from '@/stores/environments.ee.store';
|
||||
import { useExternalSecretsStore } from '@/stores/externalSecrets.ee.store';
|
||||
|
||||
import type {
|
||||
Completion,
|
||||
CompletionContext,
|
||||
CompletionResult,
|
||||
CompletionSection,
|
||||
} from '@codemirror/autocomplete';
|
||||
import type {
|
||||
AutocompleteInput,
|
||||
AutocompleteOptionType,
|
||||
ExtensionTypeName,
|
||||
FnToDoc,
|
||||
Resolved,
|
||||
} from './types';
|
||||
import { sanitizeHtml } from '@/utils/htmlUtils';
|
||||
import { isFunctionOption } from './typeGuards';
|
||||
import { luxonInstanceDocs } from './nativesAutocompleteDocs/luxon.instance.docs';
|
||||
import { luxonStaticDocs } from './nativesAutocompleteDocs/luxon.static.docs';
|
||||
import { useEnvironmentsStore } from '@/stores/environments.ee.store';
|
||||
import { useExternalSecretsStore } from '@/stores/externalSecrets.ee.store';
|
||||
import { uniqBy } from 'lodash-es';
|
||||
import { DateTime } from 'luxon';
|
||||
import type { DocMetadata, IDataObject, NativeDoc } from 'n8n-workflow';
|
||||
import { Expression, ExpressionExtensions, NativeMethods, validateFieldType } from 'n8n-workflow';
|
||||
import {
|
||||
ARRAY_NUMBER_ONLY_METHODS,
|
||||
ARRAY_RECOMMENDED_OPTIONS,
|
||||
DATE_RECOMMENDED_OPTIONS,
|
||||
FIELDS_SECTION,
|
||||
LUXON_RECOMMENDED_OPTIONS,
|
||||
LUXON_SECTIONS,
|
||||
@@ -58,8 +30,29 @@ import {
|
||||
STRING_RECOMMENDED_OPTIONS,
|
||||
STRING_SECTIONS,
|
||||
} from './constants';
|
||||
import { VALID_EMAIL_REGEX } from '@/constants';
|
||||
import { uniqBy } from 'lodash-es';
|
||||
import { createInfoBoxRenderer } from './infoBoxRenderer';
|
||||
import { luxonInstanceDocs } from './nativesAutocompleteDocs/luxon.instance.docs';
|
||||
import { luxonStaticDocs } from './nativesAutocompleteDocs/luxon.static.docs';
|
||||
import type { AutocompleteInput, ExtensionTypeName, FnToDoc, Resolved } from './types';
|
||||
import {
|
||||
applyBracketAccess,
|
||||
applyBracketAccessCompletion,
|
||||
applyCompletion,
|
||||
getDefaultArgs,
|
||||
hasNoParams,
|
||||
hasRequiredArgs,
|
||||
insertDefaultArgs,
|
||||
isAllowedInDotNotation,
|
||||
isCredentialsModalOpen,
|
||||
isPseudoParam,
|
||||
isSplitInBatchesAbsent,
|
||||
longestCommonPrefix,
|
||||
prefixMatch,
|
||||
setRank,
|
||||
sortCompletionsAlpha,
|
||||
splitBaseTail,
|
||||
stripExcessParens,
|
||||
} from './utils';
|
||||
|
||||
/**
|
||||
* Resolution-based completions offered according to datatype.
|
||||
@@ -92,7 +85,7 @@ export function datatypeCompletions(context: CompletionContext): CompletionResul
|
||||
|
||||
try {
|
||||
resolved = resolveParameter(`={{ ${base} }}`);
|
||||
} catch {
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -100,7 +93,7 @@ export function datatypeCompletions(context: CompletionContext): CompletionResul
|
||||
|
||||
try {
|
||||
options = datatypeOptions({ resolved, base, tail }).map(stripExcessParens(context));
|
||||
} catch {
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -163,7 +156,7 @@ function datatypeOptions(input: AutocompleteInput): Completion[] {
|
||||
return booleanOptions();
|
||||
}
|
||||
|
||||
if (resolved instanceof DateTime) {
|
||||
if (DateTime.isDateTime(resolved)) {
|
||||
return luxonOptions(input as AutocompleteInput<DateTime>);
|
||||
}
|
||||
|
||||
@@ -182,42 +175,61 @@ function datatypeOptions(input: AutocompleteInput): Completion[] {
|
||||
return [];
|
||||
}
|
||||
|
||||
export const natives = (
|
||||
typeName: ExtensionTypeName,
|
||||
transformLabel: (label: string) => string = (label) => label,
|
||||
): Completion[] => {
|
||||
const natives: NativeDoc = NativeMethods.find((ee) => ee.typeName.toLowerCase() === typeName);
|
||||
export const natives = ({
|
||||
typeName,
|
||||
transformLabel = (label) => label,
|
||||
}: {
|
||||
typeName: ExtensionTypeName;
|
||||
transformLabel?: (label: string) => string;
|
||||
}): Completion[] => {
|
||||
const nativeDocs: NativeDoc = NativeMethods.find((ee) => ee.typeName.toLowerCase() === typeName);
|
||||
|
||||
if (!natives) return [];
|
||||
if (!nativeDocs) return [];
|
||||
|
||||
const nativeProps = natives.properties
|
||||
? toOptions(natives.properties, typeName, 'keyword', false, transformLabel)
|
||||
const nativeProps = nativeDocs.properties
|
||||
? toOptions({
|
||||
fnToDoc: nativeDocs.properties,
|
||||
includeHidden: false,
|
||||
isFunction: false,
|
||||
transformLabel,
|
||||
})
|
||||
: [];
|
||||
const nativeMethods = toOptions(
|
||||
natives.functions,
|
||||
typeName,
|
||||
'native-function',
|
||||
false,
|
||||
|
||||
const nativeMethods = toOptions({
|
||||
fnToDoc: nativeDocs.functions,
|
||||
includeHidden: false,
|
||||
isFunction: true,
|
||||
transformLabel,
|
||||
);
|
||||
});
|
||||
|
||||
return [...nativeProps, ...nativeMethods];
|
||||
};
|
||||
|
||||
export const extensions = (
|
||||
typeName: ExtensionTypeName,
|
||||
export const extensions = ({
|
||||
typeName,
|
||||
includeHidden = false,
|
||||
transformLabel: (label: string) => string = (label) => label,
|
||||
) => {
|
||||
const extensions = ExpressionExtensions.find((ee) => ee.typeName.toLowerCase() === typeName);
|
||||
transformLabel = (label) => label,
|
||||
}: {
|
||||
typeName: ExtensionTypeName;
|
||||
includeHidden?: boolean;
|
||||
transformLabel?: (label: string) => string;
|
||||
}) => {
|
||||
const expressionExtensions = ExpressionExtensions.find(
|
||||
(ee) => ee.typeName.toLowerCase() === typeName,
|
||||
);
|
||||
|
||||
if (!extensions) return [];
|
||||
if (!expressionExtensions) return [];
|
||||
|
||||
const fnToDoc = Object.entries(extensions.functions).reduce<FnToDoc>((acc, [fnName, fn]) => {
|
||||
return { ...acc, [fnName]: { doc: fn.doc } };
|
||||
}, {});
|
||||
const fnToDoc = Object.entries(expressionExtensions.functions).reduce<FnToDoc>(
|
||||
(acc, [fnName, fn]) => {
|
||||
// Extension method docs do not have more info than info box, do not show
|
||||
delete fn.doc?.docURL;
|
||||
return { ...acc, [fnName]: { doc: fn.doc } };
|
||||
},
|
||||
{},
|
||||
);
|
||||
|
||||
return toOptions(fnToDoc, typeName, 'extension-function', includeHidden, transformLabel);
|
||||
return toOptions({ fnToDoc, isFunction: true, includeHidden, transformLabel });
|
||||
};
|
||||
|
||||
export const getType = (value: unknown): string => {
|
||||
@@ -238,144 +250,56 @@ export const getDetail = (base: string, value: unknown): string | undefined => {
|
||||
return type;
|
||||
};
|
||||
|
||||
export const toOptions = (
|
||||
fnToDoc: FnToDoc,
|
||||
typeName: ExtensionTypeName,
|
||||
optionType: AutocompleteOptionType = 'native-function',
|
||||
export const toOptions = ({
|
||||
fnToDoc,
|
||||
isFunction = false,
|
||||
includeHidden = false,
|
||||
transformLabel: (label: string) => string = (label) => label,
|
||||
) => {
|
||||
transformLabel = (label) => label,
|
||||
}: {
|
||||
fnToDoc: FnToDoc;
|
||||
isFunction?: boolean;
|
||||
includeHidden?: boolean;
|
||||
transformLabel?: (label: string) => string;
|
||||
}) => {
|
||||
return Object.entries(fnToDoc)
|
||||
.sort((a, b) => a[0].localeCompare(b[0]))
|
||||
.filter(([, docInfo]) => (docInfo.doc && !docInfo.doc?.hidden) || includeHidden)
|
||||
.filter(([, docInfo]) => Boolean(docInfo.doc && !docInfo.doc?.hidden) || includeHidden)
|
||||
.map(([fnName, docInfo]) => {
|
||||
return createCompletionOption(typeName, fnName, optionType, docInfo, transformLabel);
|
||||
return createCompletionOption({
|
||||
name: fnName,
|
||||
doc: docInfo.doc,
|
||||
isFunction,
|
||||
transformLabel,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const createCompletionOption = (
|
||||
typeName: string,
|
||||
name: string,
|
||||
optionType: AutocompleteOptionType,
|
||||
docInfo: { doc?: DocMetadata | undefined },
|
||||
transformLabel: (label: string) => string = (label) => label,
|
||||
): Completion => {
|
||||
const isFunction = isFunctionOption(optionType);
|
||||
const createCompletionOption = ({
|
||||
name,
|
||||
doc,
|
||||
isFunction = false,
|
||||
transformLabel = (label) => label,
|
||||
}: {
|
||||
name: string;
|
||||
doc?: DocMetadata;
|
||||
isFunction?: boolean;
|
||||
transformLabel?: (label: string) => string;
|
||||
}): Completion => {
|
||||
const label = isFunction ? name + '()' : name;
|
||||
const option: Completion = {
|
||||
label,
|
||||
type: optionType,
|
||||
section: docInfo.doc?.section,
|
||||
section: doc?.section,
|
||||
apply: applyCompletion({
|
||||
hasArgs: hasRequiredArgs(docInfo?.doc),
|
||||
defaultArgs: getDefaultArgs(docInfo?.doc),
|
||||
hasArgs: hasRequiredArgs(doc),
|
||||
defaultArgs: getDefaultArgs(doc),
|
||||
transformLabel,
|
||||
}),
|
||||
};
|
||||
|
||||
option.info = () => {
|
||||
const tooltipContainer = document.createElement('div');
|
||||
tooltipContainer.classList.add('autocomplete-info-container');
|
||||
|
||||
if (!docInfo.doc) return null;
|
||||
|
||||
const header = isFunctionOption(optionType)
|
||||
? createFunctionHeader(typeName, docInfo)
|
||||
: createPropHeader(typeName, docInfo);
|
||||
header.classList.add('autocomplete-info-header');
|
||||
tooltipContainer.appendChild(header);
|
||||
|
||||
if (docInfo.doc.description) {
|
||||
const descriptionBody = document.createElement('div');
|
||||
descriptionBody.classList.add('autocomplete-info-description');
|
||||
const descriptionText = document.createElement('p');
|
||||
descriptionText.innerHTML = sanitizeHtml(
|
||||
docInfo.doc.description.replace(/`(.*?)`/g, '<code>$1</code>'),
|
||||
);
|
||||
descriptionBody.appendChild(descriptionText);
|
||||
if (docInfo.doc.docURL) {
|
||||
const descriptionLink = document.createElement('a');
|
||||
descriptionLink.setAttribute('target', '_blank');
|
||||
descriptionLink.setAttribute('href', docInfo.doc.docURL);
|
||||
descriptionLink.innerText = i18n.autocompleteUIValues.docLinkLabel || 'Learn more';
|
||||
descriptionLink.addEventListener('mousedown', (event: MouseEvent) => {
|
||||
// This will prevent documentation popup closing before click
|
||||
// event gets to links
|
||||
event.preventDefault();
|
||||
});
|
||||
descriptionLink.classList.add('autocomplete-info-doc-link');
|
||||
descriptionBody.appendChild(descriptionLink);
|
||||
}
|
||||
tooltipContainer.appendChild(descriptionBody);
|
||||
}
|
||||
|
||||
return tooltipContainer;
|
||||
};
|
||||
option.info = createInfoBoxRenderer(doc, isFunction);
|
||||
|
||||
return option;
|
||||
};
|
||||
|
||||
const createFunctionHeader = (typeName: string, fn: { doc?: DocMetadata | undefined }) => {
|
||||
const header = document.createElement('div');
|
||||
if (fn.doc) {
|
||||
const typeNameSpan = document.createElement('span');
|
||||
typeNameSpan.innerHTML = typeName.slice(0, 1).toUpperCase() + typeName.slice(1) + '.';
|
||||
header.appendChild(typeNameSpan);
|
||||
|
||||
const functionNameSpan = document.createElement('span');
|
||||
functionNameSpan.classList.add('autocomplete-info-name');
|
||||
functionNameSpan.innerHTML = `${fn.doc.name}`;
|
||||
header.appendChild(functionNameSpan);
|
||||
let functionArgs = '(';
|
||||
if (fn.doc.args) {
|
||||
functionArgs += fn.doc.args
|
||||
.map((arg) => {
|
||||
let argString = `${arg.name}`;
|
||||
if (arg.type) {
|
||||
argString += `: ${arg.type}`;
|
||||
}
|
||||
return argString;
|
||||
})
|
||||
.join(', ');
|
||||
}
|
||||
functionArgs += ')';
|
||||
const argsSpan = document.createElement('span');
|
||||
argsSpan.classList.add('autocomplete-info-name-args');
|
||||
argsSpan.innerText = functionArgs;
|
||||
header.appendChild(argsSpan);
|
||||
if (fn.doc.returnType) {
|
||||
const returnTypeSpan = document.createElement('span');
|
||||
returnTypeSpan.innerHTML = ': ' + fn.doc.returnType;
|
||||
header.appendChild(returnTypeSpan);
|
||||
}
|
||||
}
|
||||
return header;
|
||||
};
|
||||
|
||||
const createPropHeader = (typeName: string, property: { doc?: DocMetadata | undefined }) => {
|
||||
const header = document.createElement('div');
|
||||
if (property.doc) {
|
||||
const typeNameSpan = document.createElement('span');
|
||||
typeNameSpan.innerHTML = typeName.charAt(0).toUpperCase() + typeName.slice(1);
|
||||
|
||||
if (!property.doc.name.startsWith("['")) {
|
||||
typeNameSpan.innerHTML += '.';
|
||||
}
|
||||
|
||||
const propNameSpan = document.createElement('span');
|
||||
propNameSpan.classList.add('autocomplete-info-name');
|
||||
propNameSpan.innerText = property.doc.name;
|
||||
|
||||
const returnTypeSpan = document.createElement('span');
|
||||
returnTypeSpan.innerHTML = ': ' + property.doc.returnType;
|
||||
|
||||
header.appendChild(typeNameSpan);
|
||||
header.appendChild(propNameSpan);
|
||||
header.appendChild(returnTypeSpan);
|
||||
}
|
||||
return header;
|
||||
};
|
||||
|
||||
const objectOptions = (input: AutocompleteInput<IDataObject>): Completion[] => {
|
||||
const { base, resolved, transformLabel = (label) => label } = input;
|
||||
const rank = setRank(['item', 'all', 'first', 'last']);
|
||||
@@ -422,19 +346,16 @@ const objectOptions = (input: AutocompleteInput<IDataObject>): Completion[] => {
|
||||
|
||||
const infoKey = [name, key].join('.');
|
||||
const infoName = needsBracketAccess ? applyBracketAccess(key) : key;
|
||||
option.info = createCompletionOption(
|
||||
'',
|
||||
infoName,
|
||||
isFunction ? 'native-function' : 'keyword',
|
||||
{
|
||||
doc: {
|
||||
name: infoName,
|
||||
returnType: getType(resolvedProp),
|
||||
description: i18n.proxyVars[infoKey],
|
||||
},
|
||||
option.info = createCompletionOption({
|
||||
name: infoName,
|
||||
doc: {
|
||||
name: infoName,
|
||||
returnType: isFunction ? 'any' : getType(resolvedProp),
|
||||
description: i18n.proxyVars[infoKey],
|
||||
},
|
||||
isFunction,
|
||||
transformLabel,
|
||||
).info;
|
||||
}).info;
|
||||
|
||||
return option;
|
||||
});
|
||||
@@ -448,15 +369,19 @@ const objectOptions = (input: AutocompleteInput<IDataObject>): Completion[] => {
|
||||
base === 'Math';
|
||||
|
||||
if (skipObjectExtensions) {
|
||||
return sortCompletionsAlpha([...localKeys, ...natives('object')]);
|
||||
return sortCompletionsAlpha([...localKeys, ...natives({ typeName: 'object' })]);
|
||||
}
|
||||
|
||||
return applySections({
|
||||
options: sortCompletionsAlpha([...localKeys, ...natives('object'), ...extensions('object')]),
|
||||
options: sortCompletionsAlpha([
|
||||
...localKeys,
|
||||
...natives({ typeName: 'object' }),
|
||||
...extensions({ typeName: 'object' }),
|
||||
]),
|
||||
recommended: OBJECT_RECOMMENDED_OPTIONS,
|
||||
recommendedSection: RECOMMENDED_METHODS_SECTION,
|
||||
methodsSection: OTHER_METHODS_SECTION,
|
||||
propSection: FIELDS_SECTION,
|
||||
methodsSection: OTHER_METHODS_SECTION,
|
||||
excludeRecommended: true,
|
||||
});
|
||||
};
|
||||
@@ -536,14 +461,14 @@ const isUrl = (url: string): boolean => {
|
||||
const stringOptions = (input: AutocompleteInput<string>): Completion[] => {
|
||||
const { resolved, transformLabel } = input;
|
||||
const options = sortCompletionsAlpha([
|
||||
...natives('string', transformLabel),
|
||||
...extensions('string', false, transformLabel),
|
||||
...natives({ typeName: 'string', transformLabel }),
|
||||
...extensions({ typeName: 'string', includeHidden: false, transformLabel }),
|
||||
]);
|
||||
|
||||
if (validateFieldType('string', resolved, 'number').valid) {
|
||||
if (resolved && validateFieldType('string', resolved, 'number').valid) {
|
||||
return applySections({
|
||||
options,
|
||||
recommended: ['toInt()', 'toFloat()'],
|
||||
recommended: ['toNumber()'],
|
||||
sections: STRING_SECTIONS,
|
||||
});
|
||||
}
|
||||
@@ -609,15 +534,18 @@ const stringOptions = (input: AutocompleteInput<string>): Completion[] => {
|
||||
|
||||
const booleanOptions = (): Completion[] => {
|
||||
return applySections({
|
||||
options: sortCompletionsAlpha([...natives('boolean'), ...extensions('boolean')]),
|
||||
options: sortCompletionsAlpha([
|
||||
...natives({ typeName: 'boolean' }),
|
||||
...extensions({ typeName: 'boolean' }),
|
||||
]),
|
||||
});
|
||||
};
|
||||
|
||||
const numberOptions = (input: AutocompleteInput<number>): Completion[] => {
|
||||
const { resolved, transformLabel } = input;
|
||||
const options = sortCompletionsAlpha([
|
||||
...natives('number', transformLabel),
|
||||
...extensions('number', false, transformLabel),
|
||||
...natives({ typeName: 'number', transformLabel }),
|
||||
...extensions({ typeName: 'number', includeHidden: false, transformLabel }),
|
||||
]);
|
||||
const ONLY_INTEGER = ['isEven()', 'isOdd()'];
|
||||
|
||||
@@ -630,7 +558,7 @@ const numberOptions = (input: AutocompleteInput<number>): Completion[] => {
|
||||
if (isPlausableMillisDateTime) {
|
||||
return applySections({
|
||||
options,
|
||||
recommended: [{ label: 'toDateTime()', args: ['ms'] }],
|
||||
recommended: [{ label: 'toDateTime()', args: ["'ms'"] }],
|
||||
});
|
||||
}
|
||||
|
||||
@@ -641,7 +569,7 @@ const numberOptions = (input: AutocompleteInput<number>): Completion[] => {
|
||||
if (isPlausableSecondsDateTime) {
|
||||
return applySections({
|
||||
options,
|
||||
recommended: [{ label: 'toDateTime()', args: ['s'] }],
|
||||
recommended: [{ label: 'toDateTime()', args: ["'s'"] }],
|
||||
});
|
||||
}
|
||||
|
||||
@@ -666,22 +594,20 @@ const numberOptions = (input: AutocompleteInput<number>): Completion[] => {
|
||||
};
|
||||
|
||||
const dateOptions = (input: AutocompleteInput<Date>): Completion[] => {
|
||||
return applySections({
|
||||
options: sortCompletionsAlpha([
|
||||
...natives('date', input.transformLabel),
|
||||
...extensions('date', true, input.transformLabel),
|
||||
]),
|
||||
recommended: DATE_RECOMMENDED_OPTIONS,
|
||||
});
|
||||
const { transformLabel } = input;
|
||||
return extensions({ typeName: 'date', includeHidden: true, transformLabel }).filter(
|
||||
(ext) => ext.label === 'toDateTime()',
|
||||
);
|
||||
};
|
||||
|
||||
const luxonOptions = (input: AutocompleteInput<DateTime>): Completion[] => {
|
||||
return applySections({
|
||||
const { transformLabel } = input;
|
||||
const result = applySections({
|
||||
options: sortCompletionsAlpha(
|
||||
uniqBy(
|
||||
[
|
||||
...extensions('date', false, input.transformLabel),
|
||||
...luxonInstanceOptions(false, input.transformLabel),
|
||||
...extensions({ typeName: 'date', includeHidden: false, transformLabel }),
|
||||
...luxonInstanceOptions({ includeHidden: false, transformLabel }),
|
||||
],
|
||||
(option) => option.label,
|
||||
),
|
||||
@@ -689,14 +615,16 @@ const luxonOptions = (input: AutocompleteInput<DateTime>): Completion[] => {
|
||||
recommended: LUXON_RECOMMENDED_OPTIONS,
|
||||
sections: LUXON_SECTIONS,
|
||||
});
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
const arrayOptions = (input: AutocompleteInput<unknown[]>): Completion[] => {
|
||||
const { resolved, transformLabel } = input;
|
||||
const options = applySections({
|
||||
options: sortCompletionsAlpha([
|
||||
...natives('array', transformLabel),
|
||||
...extensions('array', false, transformLabel),
|
||||
...natives({ typeName: 'array', transformLabel }),
|
||||
...extensions({ typeName: 'array', includeHidden: false, transformLabel }),
|
||||
]),
|
||||
recommended: ARRAY_RECOMMENDED_OPTIONS,
|
||||
methodsSection: OTHER_SECTION,
|
||||
@@ -726,7 +654,8 @@ export const variablesOptions = () => {
|
||||
const variables = environmentsStore.variables;
|
||||
|
||||
return variables.map((variable) =>
|
||||
createCompletionOption('Object', variable.key, 'keyword', {
|
||||
createCompletionOption({
|
||||
name: variable.key,
|
||||
doc: {
|
||||
name: variable.key,
|
||||
returnType: 'string',
|
||||
@@ -756,7 +685,8 @@ export const secretOptions = (base: string) => {
|
||||
return [];
|
||||
}
|
||||
return Object.entries(resolved).map(([secret, value]) =>
|
||||
createCompletionOption('', secret, 'keyword', {
|
||||
createCompletionOption({
|
||||
name: secret,
|
||||
doc: {
|
||||
name: secret,
|
||||
returnType: typeof value,
|
||||
@@ -774,7 +704,8 @@ export const secretProvidersOptions = () => {
|
||||
const externalSecretsStore = useExternalSecretsStore();
|
||||
|
||||
return Object.keys(externalSecretsStore.secretsAsObject).map((provider) =>
|
||||
createCompletionOption('Object', provider, 'keyword', {
|
||||
createCompletionOption({
|
||||
name: provider,
|
||||
doc: {
|
||||
name: provider,
|
||||
returnType: 'object',
|
||||
@@ -788,10 +719,13 @@ export const secretProvidersOptions = () => {
|
||||
/**
|
||||
* Methods and fields defined on a Luxon `DateTime` class instance.
|
||||
*/
|
||||
export const luxonInstanceOptions = (
|
||||
export const luxonInstanceOptions = ({
|
||||
includeHidden = false,
|
||||
transformLabel: (label: string) => string = (label) => label,
|
||||
) => {
|
||||
transformLabel = (label) => label,
|
||||
}: {
|
||||
includeHidden?: boolean;
|
||||
transformLabel?: (label: string) => string;
|
||||
} = {}) => {
|
||||
const SKIP = new Set(['constructor', 'get', 'invalidExplanation', 'invalidReason']);
|
||||
|
||||
return Object.entries(Object.getOwnPropertyDescriptors(DateTime.prototype))
|
||||
@@ -799,15 +733,14 @@ export const luxonInstanceOptions = (
|
||||
.sort(([a], [b]) => a.localeCompare(b))
|
||||
.map(([key, descriptor]) => {
|
||||
const isFunction = typeof descriptor.value === 'function';
|
||||
const optionType = isFunction ? 'native-function' : 'keyword';
|
||||
return createLuxonAutocompleteOption(
|
||||
key,
|
||||
optionType,
|
||||
luxonInstanceDocs,
|
||||
i18n.luxonInstance,
|
||||
return createLuxonAutocompleteOption({
|
||||
name: key,
|
||||
isFunction,
|
||||
docs: luxonInstanceDocs,
|
||||
translations: i18n.luxonInstance,
|
||||
includeHidden,
|
||||
transformLabel,
|
||||
) as Completion;
|
||||
}) as Completion;
|
||||
})
|
||||
.filter(Boolean);
|
||||
};
|
||||
@@ -822,33 +755,39 @@ export const luxonStaticOptions = () => {
|
||||
Object.keys(Object.getOwnPropertyDescriptors(DateTime))
|
||||
.filter((key) => !SKIP.has(key) && !key.includes('_'))
|
||||
.map((key) => {
|
||||
return createLuxonAutocompleteOption(
|
||||
key,
|
||||
'native-function',
|
||||
luxonStaticDocs,
|
||||
i18n.luxonStatic,
|
||||
) as Completion;
|
||||
return createLuxonAutocompleteOption({
|
||||
name: key,
|
||||
isFunction: true,
|
||||
docs: luxonStaticDocs,
|
||||
translations: i18n.luxonStatic,
|
||||
}) as Completion;
|
||||
})
|
||||
.filter(Boolean),
|
||||
);
|
||||
};
|
||||
|
||||
const createLuxonAutocompleteOption = (
|
||||
name: string,
|
||||
type: AutocompleteOptionType,
|
||||
docDefinition: NativeDoc,
|
||||
translations: Record<string, string | undefined>,
|
||||
const createLuxonAutocompleteOption = ({
|
||||
name,
|
||||
docs,
|
||||
translations,
|
||||
isFunction = false,
|
||||
includeHidden = false,
|
||||
transformLabel: (label: string) => string = (label) => label,
|
||||
): Completion | null => {
|
||||
const isFunction = isFunctionOption(type);
|
||||
transformLabel = (label) => label,
|
||||
}: {
|
||||
name: string;
|
||||
docs: NativeDoc;
|
||||
translations: Record<string, string | undefined>;
|
||||
isFunction?: boolean;
|
||||
includeHidden?: boolean;
|
||||
transformLabel?: (label: string) => string;
|
||||
}): Completion | null => {
|
||||
const label = isFunction ? name + '()' : name;
|
||||
|
||||
let doc: DocMetadata | undefined;
|
||||
if (docDefinition.properties && docDefinition.properties.hasOwnProperty(name)) {
|
||||
doc = docDefinition.properties[name].doc;
|
||||
} else if (docDefinition.functions.hasOwnProperty(name)) {
|
||||
doc = docDefinition.functions[name].doc;
|
||||
if (docs.properties && docs.properties.hasOwnProperty(name)) {
|
||||
doc = docs.properties[name].doc;
|
||||
} else if (docs.functions.hasOwnProperty(name)) {
|
||||
doc = docs.functions[name].doc;
|
||||
} else {
|
||||
// Use inferred/default values if docs are still not updated
|
||||
// This should happen when our doc specification becomes
|
||||
@@ -867,7 +806,6 @@ const createLuxonAutocompleteOption = (
|
||||
|
||||
const option: Completion = {
|
||||
label,
|
||||
type,
|
||||
section: doc?.section,
|
||||
apply: applyCompletion({
|
||||
hasArgs: hasRequiredArgs(doc),
|
||||
@@ -875,16 +813,13 @@ const createLuxonAutocompleteOption = (
|
||||
transformLabel,
|
||||
}),
|
||||
};
|
||||
option.info = createCompletionOption(
|
||||
'DateTime',
|
||||
option.info = createCompletionOption({
|
||||
name,
|
||||
type,
|
||||
{
|
||||
// Add translated description
|
||||
doc: { ...doc, description: translations[name] } as DocMetadata,
|
||||
},
|
||||
isFunction,
|
||||
// Add translated description
|
||||
doc: { ...doc, description: translations[name] } as DocMetadata,
|
||||
transformLabel,
|
||||
).info;
|
||||
}).info;
|
||||
return option;
|
||||
};
|
||||
|
||||
@@ -911,8 +846,8 @@ const regexes = {
|
||||
selectorRef: /\$\(['"][\S\s]+['"]\)\.(.*)/, // $('nodeName').
|
||||
|
||||
numberLiteral: /\((\d+)\.?(\d*)\)\.(.*)/, // (123). or (123.4).
|
||||
singleQuoteStringLiteral: /('.+')\.([^'{\s])*/, // 'abc'.
|
||||
doubleQuoteStringLiteral: /(".+")\.([^"{\s])*/, // "abc".
|
||||
singleQuoteStringLiteral: /('.*')\.([^'{\s])*/, // 'abc'.
|
||||
doubleQuoteStringLiteral: /(".*")\.([^"{\s])*/, // "abc".
|
||||
dateLiteral: /\(?new Date\(\(?.*?\)\)?\.(.*)/, // new Date(). or (new Date()).
|
||||
arrayLiteral: /\(?(\[.*\])\)?\.(.*)/, // [1, 2, 3].
|
||||
indexedAccess: /([^"{\s]+\[.+\])\.(.*)/, // 'abc'[0]. or 'abc'.split('')[0] or similar ones
|
||||
|
||||
@@ -14,6 +14,7 @@ import type { Completion, CompletionContext, CompletionResult } from '@codemirro
|
||||
import { useExternalSecretsStore } from '@/stores/externalSecrets.ee.store';
|
||||
import { escapeMappingString } from '@/utils/mappingUtils';
|
||||
import { PREVIOUS_NODES_SECTION, RECOMMENDED_SECTION, ROOT_DOLLAR_COMPLETIONS } from './constants';
|
||||
import { createInfoBoxRenderer } from './infoBoxRenderer';
|
||||
|
||||
/**
|
||||
* Completions offered at the dollar position: `$|`
|
||||
@@ -53,9 +54,33 @@ export function dollarOptions(): Completion[] {
|
||||
|
||||
if (isInHttpNodePagination()) {
|
||||
recommendedCompletions = [
|
||||
{ label: '$pageCount', section: RECOMMENDED_SECTION, info: i18n.rootVars.$pageCount },
|
||||
{ label: '$response', section: RECOMMENDED_SECTION, info: i18n.rootVars.$response },
|
||||
{ label: '$request', section: RECOMMENDED_SECTION, info: i18n.rootVars.$request },
|
||||
{
|
||||
label: '$pageCount',
|
||||
section: RECOMMENDED_SECTION,
|
||||
info: createInfoBoxRenderer({
|
||||
name: '$pageCount',
|
||||
returnType: 'number',
|
||||
description: i18n.rootVars.$pageCount,
|
||||
}),
|
||||
},
|
||||
{
|
||||
label: '$response',
|
||||
section: RECOMMENDED_SECTION,
|
||||
info: createInfoBoxRenderer({
|
||||
name: '$response',
|
||||
returnType: 'object',
|
||||
description: i18n.rootVars.$response,
|
||||
}),
|
||||
},
|
||||
{
|
||||
label: '$request',
|
||||
section: RECOMMENDED_SECTION,
|
||||
info: createInfoBoxRenderer({
|
||||
name: '$request',
|
||||
returnType: 'object',
|
||||
description: i18n.rootVars.$request,
|
||||
}),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
@@ -80,12 +105,18 @@ export function dollarOptions(): Completion[] {
|
||||
|
||||
if (receivesNoBinaryData()) SKIP.add('$binary');
|
||||
|
||||
const previousNodesCompletions = autocompletableNodeNames().map((nodeName) => ({
|
||||
label: `$('${escapeMappingString(nodeName)}')`,
|
||||
type: 'keyword',
|
||||
info: i18n.baseText('codeNodeEditor.completer.$()', { interpolate: { nodeName } }),
|
||||
section: PREVIOUS_NODES_SECTION,
|
||||
}));
|
||||
const previousNodesCompletions = autocompletableNodeNames().map((nodeName) => {
|
||||
const label = `$('${escapeMappingString(nodeName)}')`;
|
||||
return {
|
||||
label,
|
||||
info: createInfoBoxRenderer({
|
||||
name: label,
|
||||
returnType: 'object',
|
||||
description: i18n.baseText('codeNodeEditor.completer.$()', { interpolate: { nodeName } }),
|
||||
}),
|
||||
section: PREVIOUS_NODES_SECTION,
|
||||
};
|
||||
});
|
||||
|
||||
return recommendedCompletions
|
||||
.concat(ROOT_DOLLAR_COMPLETIONS)
|
||||
|
||||
@@ -0,0 +1,263 @@
|
||||
import type { DocMetadata, DocMetadataArgument, DocMetadataExample } from 'n8n-workflow';
|
||||
import { sanitizeHtml } from '@/utils/htmlUtils';
|
||||
import { i18n } from '@/plugins/i18n';
|
||||
|
||||
const renderFunctionHeader = (doc?: DocMetadata) => {
|
||||
const header = document.createElement('div');
|
||||
if (doc) {
|
||||
const functionNameSpan = document.createElement('span');
|
||||
functionNameSpan.classList.add('autocomplete-info-name');
|
||||
functionNameSpan.textContent = doc.name;
|
||||
header.appendChild(functionNameSpan);
|
||||
|
||||
const openBracketsSpan = document.createElement('span');
|
||||
openBracketsSpan.textContent = '(';
|
||||
header.appendChild(openBracketsSpan);
|
||||
|
||||
const argsSpan = document.createElement('span');
|
||||
doc.args?.forEach((arg, index, array) => {
|
||||
const optional = arg.optional && !arg.name.endsWith('?');
|
||||
const argSpan = document.createElement('span');
|
||||
argSpan.textContent = arg.name;
|
||||
|
||||
if (optional) {
|
||||
argSpan.textContent += '?';
|
||||
}
|
||||
|
||||
if (arg.variadic) {
|
||||
argSpan.textContent = '...' + argSpan.textContent;
|
||||
}
|
||||
|
||||
argSpan.classList.add('autocomplete-info-arg');
|
||||
argsSpan.appendChild(argSpan);
|
||||
|
||||
if (index !== array.length - 1) {
|
||||
const separatorSpan = document.createElement('span');
|
||||
separatorSpan.textContent = ', ';
|
||||
argsSpan.appendChild(separatorSpan);
|
||||
} else {
|
||||
argSpan.textContent += ')';
|
||||
}
|
||||
});
|
||||
header.appendChild(argsSpan);
|
||||
|
||||
const preTypeInfo = document.createElement('span');
|
||||
preTypeInfo.textContent = !doc.args || doc.args.length === 0 ? '): ' : ': ';
|
||||
header.appendChild(preTypeInfo);
|
||||
|
||||
const returnTypeSpan = document.createElement('span');
|
||||
returnTypeSpan.textContent = doc.returnType;
|
||||
returnTypeSpan.classList.add('autocomplete-info-return');
|
||||
header.appendChild(returnTypeSpan);
|
||||
}
|
||||
return header;
|
||||
};
|
||||
|
||||
const renderPropHeader = (doc?: DocMetadata) => {
|
||||
const header = document.createElement('div');
|
||||
if (doc) {
|
||||
const propNameSpan = document.createElement('span');
|
||||
propNameSpan.classList.add('autocomplete-info-name');
|
||||
propNameSpan.innerText = doc.name;
|
||||
|
||||
const returnTypeSpan = document.createElement('span');
|
||||
returnTypeSpan.textContent = ': ' + doc.returnType;
|
||||
|
||||
header.appendChild(propNameSpan);
|
||||
header.appendChild(returnTypeSpan);
|
||||
}
|
||||
return header;
|
||||
};
|
||||
|
||||
const renderDescription = ({
|
||||
description,
|
||||
docUrl,
|
||||
example,
|
||||
}: {
|
||||
description: string;
|
||||
docUrl?: string;
|
||||
example?: DocMetadataExample;
|
||||
}) => {
|
||||
const descriptionBody = document.createElement('div');
|
||||
descriptionBody.classList.add('autocomplete-info-description');
|
||||
const descriptionText = document.createElement('p');
|
||||
const separator = !description.endsWith('.') && docUrl ? '. ' : ' ';
|
||||
descriptionText.innerHTML = sanitizeHtml(
|
||||
description.replace(/`(.*?)`/g, '<code>$1</code>') + separator,
|
||||
);
|
||||
descriptionBody.appendChild(descriptionText);
|
||||
|
||||
if (docUrl) {
|
||||
const descriptionLink = document.createElement('a');
|
||||
descriptionLink.setAttribute('target', '_blank');
|
||||
descriptionLink.setAttribute('href', docUrl);
|
||||
descriptionLink.innerText =
|
||||
i18n.autocompleteUIValues.docLinkLabel ?? i18n.baseText('generic.learnMore');
|
||||
descriptionLink.addEventListener('mousedown', (event: MouseEvent) => {
|
||||
// This will prevent documentation popup closing before click
|
||||
// event gets to links
|
||||
event.preventDefault();
|
||||
});
|
||||
descriptionLink.classList.add('autocomplete-info-doc-link');
|
||||
descriptionText.appendChild(descriptionLink);
|
||||
}
|
||||
|
||||
if (example) {
|
||||
const renderedExample = renderExample(example);
|
||||
descriptionBody.appendChild(renderedExample);
|
||||
}
|
||||
|
||||
return descriptionBody;
|
||||
};
|
||||
|
||||
const renderArgs = (args: DocMetadataArgument[]) => {
|
||||
const argsContainer = document.createElement('div');
|
||||
argsContainer.classList.add('autocomplete-info-args-container');
|
||||
|
||||
const argsTitle = document.createElement('div');
|
||||
argsTitle.classList.add('autocomplete-info-section-title');
|
||||
argsTitle.textContent = i18n.baseText('codeNodeEditor.parameters');
|
||||
argsContainer.appendChild(argsTitle);
|
||||
|
||||
const argsList = document.createElement('ul');
|
||||
argsList.classList.add('autocomplete-info-args');
|
||||
|
||||
for (const arg of args.filter((a) => a.name !== '...')) {
|
||||
const argItem = document.createElement('li');
|
||||
const argName = document.createElement('span');
|
||||
argName.classList.add('autocomplete-info-arg-name');
|
||||
argName.textContent = arg.name.replaceAll('?', '');
|
||||
const tags = [];
|
||||
|
||||
if (arg.type) {
|
||||
tags.push(arg.type);
|
||||
}
|
||||
|
||||
if (arg.optional || arg.name.endsWith('?')) {
|
||||
tags.push(i18n.baseText('codeNodeEditor.optional'));
|
||||
}
|
||||
|
||||
if (args.length > 0) {
|
||||
argName.textContent += ` (${tags.join(', ')})`;
|
||||
}
|
||||
|
||||
if (arg.description) {
|
||||
argName.textContent += ':';
|
||||
}
|
||||
argItem.appendChild(argName);
|
||||
|
||||
if (arg.description) {
|
||||
const argDescription = document.createElement('span');
|
||||
argDescription.classList.add('autocomplete-info-arg-description');
|
||||
|
||||
if (arg.default && !arg.description.toLowerCase().includes('default')) {
|
||||
const separator = arg.description.endsWith('.') ? ' ' : '. ';
|
||||
arg.description +=
|
||||
separator +
|
||||
i18n.baseText('codeNodeEditor.defaultsTo', {
|
||||
interpolate: { default: arg.default },
|
||||
});
|
||||
}
|
||||
|
||||
argDescription.innerHTML = sanitizeHtml(
|
||||
arg.description.replace(/`(.*?)`/g, '<code>$1</code>'),
|
||||
);
|
||||
|
||||
argItem.appendChild(argDescription);
|
||||
}
|
||||
|
||||
argsList.appendChild(argItem);
|
||||
}
|
||||
|
||||
argsContainer.appendChild(argsList);
|
||||
return argsContainer;
|
||||
};
|
||||
|
||||
const renderExample = (example: DocMetadataExample) => {
|
||||
const examplePre = document.createElement('pre');
|
||||
examplePre.classList.add('autocomplete-info-example');
|
||||
const exampleCode = document.createElement('code');
|
||||
examplePre.appendChild(exampleCode);
|
||||
|
||||
if (example.description) {
|
||||
const exampleDescription = document.createElement('span');
|
||||
exampleDescription.classList.add('autocomplete-info-example-comment');
|
||||
exampleDescription.textContent = `// ${example.description}\n`;
|
||||
exampleCode.appendChild(exampleDescription);
|
||||
}
|
||||
|
||||
const exampleExpression = document.createElement('span');
|
||||
exampleExpression.classList.add('autocomplete-info-example-expr');
|
||||
exampleExpression.textContent = example.example + '\n';
|
||||
exampleCode.appendChild(exampleExpression);
|
||||
|
||||
if (example.evaluated) {
|
||||
const exampleEvaluated = document.createElement('span');
|
||||
exampleEvaluated.classList.add('autocomplete-info-example-comment');
|
||||
exampleEvaluated.textContent = `// => ${example.evaluated}\n`;
|
||||
exampleCode.appendChild(exampleEvaluated);
|
||||
}
|
||||
|
||||
return examplePre;
|
||||
};
|
||||
|
||||
const renderExamples = (examples: DocMetadataExample[]) => {
|
||||
const examplesContainer = document.createElement('div');
|
||||
examplesContainer.classList.add('autocomplete-info-examples');
|
||||
|
||||
const examplesTitle = document.createElement('div');
|
||||
examplesTitle.classList.add('autocomplete-info-section-title');
|
||||
examplesTitle.textContent = i18n.baseText('codeNodeEditor.examples');
|
||||
examplesContainer.appendChild(examplesTitle);
|
||||
|
||||
const examplesList = document.createElement('div');
|
||||
examplesList.classList.add('autocomplete-info-examples-list');
|
||||
|
||||
for (const example of examples) {
|
||||
const renderedExample = renderExample(example);
|
||||
examplesList.appendChild(renderedExample);
|
||||
}
|
||||
|
||||
examplesContainer.appendChild(examplesList);
|
||||
return examplesContainer;
|
||||
};
|
||||
|
||||
export const createInfoBoxRenderer =
|
||||
(doc?: DocMetadata, isFunction = false) =>
|
||||
() => {
|
||||
const tooltipContainer = document.createElement('div');
|
||||
tooltipContainer.setAttribute('tabindex', '-1');
|
||||
tooltipContainer.setAttribute('title', '');
|
||||
tooltipContainer.classList.add('autocomplete-info-container');
|
||||
|
||||
if (!doc) return null;
|
||||
|
||||
const { examples, args } = doc;
|
||||
const hasArgs = args && args.length > 0;
|
||||
const hasExamples = examples && examples.length > 0;
|
||||
|
||||
const header = isFunction ? renderFunctionHeader(doc) : renderPropHeader(doc);
|
||||
header.classList.add('autocomplete-info-header');
|
||||
tooltipContainer.appendChild(header);
|
||||
|
||||
if (doc.description) {
|
||||
const descriptionBody = renderDescription({
|
||||
description: doc.description,
|
||||
docUrl: doc.docURL,
|
||||
example: hasArgs && hasExamples ? examples[0] : undefined,
|
||||
});
|
||||
tooltipContainer.appendChild(descriptionBody);
|
||||
}
|
||||
|
||||
if (hasArgs) {
|
||||
const argsContainer = renderArgs(args);
|
||||
tooltipContainer.appendChild(argsContainer);
|
||||
}
|
||||
|
||||
if (hasExamples && (examples.length > 1 || !hasArgs)) {
|
||||
const examplesContainer = renderExamples(examples);
|
||||
tooltipContainer.appendChild(examplesContainer);
|
||||
}
|
||||
|
||||
return tooltipContainer;
|
||||
};
|
||||
@@ -202,6 +202,7 @@ export const luxonInstanceDocs: Required<NativeDoc> = {
|
||||
doc: {
|
||||
name: 'weekYear',
|
||||
section: 'query',
|
||||
hidden: true,
|
||||
docURL: 'https://moment.github.io/luxon/api-docs/index.html#datetimeweekyear',
|
||||
returnType: 'number',
|
||||
},
|
||||
@@ -226,6 +227,7 @@ export const luxonInstanceDocs: Required<NativeDoc> = {
|
||||
doc: {
|
||||
name: 'zoneName',
|
||||
section: 'query',
|
||||
hidden: true,
|
||||
docURL: 'https://moment.github.io/luxon/api-docs/index.html#datetimezonename',
|
||||
returnType: 'string',
|
||||
},
|
||||
@@ -297,7 +299,7 @@ export const luxonInstanceDocs: Required<NativeDoc> = {
|
||||
section: 'edit',
|
||||
docURL: 'https://moment.github.io/luxon/api-docs/index.html#datetimeendof',
|
||||
returnType: 'DateTime',
|
||||
args: [{ name: 'unit', type: 'string' }],
|
||||
args: [{ name: 'unit', type: 'string', default: "'month'" }],
|
||||
},
|
||||
},
|
||||
equals: {
|
||||
@@ -395,7 +397,7 @@ export const luxonInstanceDocs: Required<NativeDoc> = {
|
||||
section: 'edit',
|
||||
docURL: 'https://moment.github.io/luxon/api-docs/index.html#datetimestartof',
|
||||
returnType: 'DateTime',
|
||||
args: [{ name: 'unit', type: 'string' }],
|
||||
args: [{ name: 'unit', type: 'string', default: "'month'" }],
|
||||
},
|
||||
},
|
||||
toBSON: {
|
||||
@@ -488,7 +490,7 @@ export const luxonInstanceDocs: Required<NativeDoc> = {
|
||||
toLocal: {
|
||||
doc: {
|
||||
name: 'toLocal',
|
||||
section: 'format',
|
||||
section: 'edit',
|
||||
docURL: 'https://moment.github.io/luxon/api-docs/index.html#datetimetolocal',
|
||||
returnType: 'DateTime',
|
||||
},
|
||||
@@ -633,6 +635,7 @@ export const luxonInstanceDocs: Required<NativeDoc> = {
|
||||
doc: {
|
||||
name: 'until',
|
||||
section: 'compare',
|
||||
hidden: true,
|
||||
docURL: 'https://moment.github.io/luxon/api-docs/index.html#datetimeuntil',
|
||||
returnType: 'Interval',
|
||||
args: [{ name: 'other', type: 'DateTime' }],
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
import type { AutocompleteOptionType, FunctionOptionType } from './types';
|
||||
|
||||
export const isFunctionOption = (value: AutocompleteOptionType): value is FunctionOptionType => {
|
||||
return value === 'native-function' || value === 'extension-function';
|
||||
};
|
||||
@@ -164,13 +164,18 @@ export const stripExcessParens = (context: CompletionContext) => (option: Comple
|
||||
return option;
|
||||
};
|
||||
|
||||
export const getDefaultArgs = (doc?: DocMetadata): unknown[] => {
|
||||
return doc?.args?.map((arg) => arg.default).filter(Boolean) ?? [];
|
||||
export const getDefaultArgs = (doc?: DocMetadata): string[] => {
|
||||
return (
|
||||
doc?.args
|
||||
?.filter((arg) => !arg.optional)
|
||||
.map((arg) => arg.default)
|
||||
.filter((def): def is string => !!def) ?? []
|
||||
);
|
||||
};
|
||||
|
||||
export const insertDefaultArgs = (label: string, args: unknown[]): string => {
|
||||
if (!label.endsWith('()')) return label;
|
||||
const argList = args.map((arg) => JSON.stringify(arg)).join(', ');
|
||||
const argList = args.join(', ');
|
||||
const fnName = label.replace('()', '');
|
||||
|
||||
return `${fnName}(${argList})`;
|
||||
@@ -239,7 +244,7 @@ export const applyBracketAccessCompletion = (
|
||||
|
||||
export const hasRequiredArgs = (doc?: DocMetadata): boolean => {
|
||||
if (!doc) return false;
|
||||
const requiredArgs = doc?.args?.filter((arg) => !arg.name.endsWith('?')) ?? [];
|
||||
const requiredArgs = doc?.args?.filter((arg) => !arg.name.endsWith('?') && !arg.optional) ?? [];
|
||||
return requiredArgs.length > 0;
|
||||
};
|
||||
|
||||
|
||||
@@ -25,4 +25,5 @@ export function n8nLang() {
|
||||
]);
|
||||
}
|
||||
|
||||
export const n8nAutocompletion = () => autocompletion({ icons: false });
|
||||
export const n8nAutocompletion = () =>
|
||||
autocompletion({ icons: false, aboveCursor: true, closeOnBlur: false });
|
||||
|
||||
Reference in New Issue
Block a user