feat(editor): Completions for extensions in expression editor (#5130)

* 🔥 Remove test extensions

* 🚧 Add test description

* 📘 Expand types

*  Export extensions

*  Export collection

*  Mark all proxies

* ✏️ Rename for clarity

*  Export from barrel

*  Create datatype completions

*  Mount datatype completions

* 🧪 Adjust tests

*  Add `path` prop

* 🔥 Remove `()` from completion labels

*  Filter out completions for pseudo-proxies

* 🐛 Fix method error

*  Add metrics

* ✏️ Improve naming

*  Start completion on empty resolvable

*  Implement completion previews

*  Break out completion manager

*  Implement in expression editor modal

* ✏️ Improve naming

*  Filter out irrelevant completions

*  Add preview hint

* ✏️ Improve comments

* 🎨 Style preview hint

*  Expand `hasNoParams`

*  Add spacing for readability

*  Add error codes

* ✏️ Add comment

* 🐛 Fix Esc behavior

*  Parse Unicode

*  Throw on invalid `DateTime`

*  Fix second root completion detection

*  Switch message at completable prefix position

* 🐛 Fix function names for non-dev build

* 🐛 Fix `json` handling

* 🔥 Comment out previews

* ♻️ Apply feedback

* 🔥 Remove extensions

* 🚚 Rename extensions

*  Adjust some implementations

* 🔥 Remove dummy extensions

* 🐛 Fix object regex

* ♻️ Apply feedback

* ✏️ Fix typos

* ✏️ Add `fn is not a function` message

* 🔥 Remove check

*  Add `isNotEmpty` for objects

* 🚚 Rename `global` to `alpha`

* 🔥 Remove `encrypt`

*  Restore `is not a function` error

*  Support `week` on `extract()`

* 🧪 Fix tests

*  Add validation to some string extensions

*  Validate number arrays in some extensions

* 🧪 Fix tests

* ✏️ Improve error message

*  Revert extensions framework changes

* 🧹 Previews cleanup

*  Condense blank completions

*  Refactor dollar completions

*  Refactor non-dollar completions

*  Refactor Luxon completions

*  Refactor datatype completions

*  Use `DATETIMEUNIT_MAP`

* ✏️ Update test description

*  Revert "Use `DATETIMEUNIT_MAP`"

This reverts commit 472a77df5cd789905d162f3c3db02ac767b89b4e.

* 🧪 Add tests

* ♻️ Restore generic extensions

* 🔥 Remove logs

* 🧪 Expand tests

*  Add `Math` completions

* ✏️ List breaking change

*  Add doc tooltips

* 🐛 Fix node selector regex

* 🐛 Fix `context` resolution

* 🐛 Allow dollar completions in args

*  Make numeric array methods context-dependent

* 📝 Adjust docs

* 🐛 Fix selector ref

*  Surface error for valid URL

* 🐛 Disallow whitespace in `isEmail` check

* 🧪 Fix test for `isUrl`

*  Add comma validator in `toFloat`

*  Add validation to `$jmespath()`

*  Revert valid URL error

*  Adjust `$jmespath()` validation

* 🧪 Adjust `isUrl` test

*  Remove `{}` and `[]` from compact

* ✏️ Update docs

* 🚚 Rename `stripTags` to `removeTags`

*  Do not inject whitespace inside resolvable

*  Make completions aware of `()`

* ✏️ Add note

*  Update sorting

*  Hide active node name from node selector

* 🔥 Remove `length()` and its aliases

*  Validate non-zero for `chunk`

* ✏️ Reword all error messages

* 🐛 Fix `$now` and `$today`

*  Simplify with `stripExcessParens`

*  Fold luxon into datatype

* 🧪 Clean up tests

* 🔥 Remove tests for removed methods

* 👕 Fix type

* ⬆️ Upgrade lang pack

*  Undo change to `vitest` command

* 🔥 Remove unused method

*  Separate `return` line

* ✏️ Improve description

* 🧪 Expand tests for initial-only completions

* 🧪 Add bracket-aware completions

*  Make check for `all()` stricter

* ✏️ Adjust explanatory comments

* 🔥 Remove unneded copy

* 🔥 Remove outdated comment

*  Make naming consistent

* ✏️ Update comments

*  Improve URL scheme check

* ✏️ Add comment

* 🚚 Move extension

* ✏️ Update `BREAKING-CHANGES.md`

* ✏️ Update upcoming version

* ✏️ Fix grammar

* ✏️ Shorten message

* 🐛 Fix `Esc` behavior

* 🐛 Fix `isNumeric`

*  Support native methods

* 🧪 Skip Pinia tests

* ✏️ Shorten description

* 🔥 Remove outdated comment

* 🧪 Unskip Pinia tests

* ✏️ Add comments

* 🧪 Expand tests to natives

* ✏️ Add clarifying comments

*  Use `setTimeout` to make telemetry non-blocking

* 🐛 Account for no active node in cred modal

*  Resolve without workflow

* 🔥 Remove `Esc` handling on NDV

*  Use `isDateTime`

* 🚚 Move `unique` to next phase

This array extension takes optional args.

*  Merge export

* 🧪 Fix tests

*  Restore check

* ✏️ Make breaking change description more accurate

* 🧪 Fix e2e tests
This commit is contained in:
Iván Ovejero
2023-02-02 12:35:38 +01:00
committed by GitHub
parent ee210e8507
commit 6d811f0d9f
58 changed files with 2269 additions and 1240 deletions

View File

@@ -87,11 +87,11 @@ function first(value: unknown[]): unknown {
return value[0];
}
function isBlank(value: unknown[]): boolean {
function isEmpty(value: unknown[]): boolean {
return value.length === 0;
}
function isPresent(value: unknown[]): boolean {
function isNotEmpty(value: unknown[]): boolean {
return value.length > 0;
}
@@ -99,10 +99,6 @@ function last(value: unknown[]): unknown {
return value[value.length - 1];
}
function length(value: unknown[]): number {
return Array.isArray(value) ? value.length : 0;
}
function pluck(value: unknown[], extraArgs: unknown[]): unknown[] {
if (!Array.isArray(extraArgs)) {
throw new ExpressionError('arguments must be passed to pluck');
@@ -121,7 +117,7 @@ function pluck(value: unknown[], extraArgs: unknown[]): unknown[] {
}) as unknown[];
}
function random(value: unknown[]): unknown {
function randomItem(value: unknown[]): unknown {
const len = value === undefined ? 0 : value.length;
return len ? value[Math.floor(Math.random() * len)] : undefined;
}
@@ -149,7 +145,15 @@ function unique(value: unknown[], extraArgs: string[]): unknown[] {
}, []);
}
const ensureNumberArray = (arr: unknown[], { fnName }: { fnName: string }) => {
if (arr.some((i) => typeof i !== 'number')) {
throw new ExpressionExtensionError(`${fnName}(): all array elements must be numbers`);
}
};
function sum(value: unknown[]): number {
ensureNumberArray(value, { fnName: 'sum' });
return value.reduce((p: number, c: unknown) => {
if (typeof c === 'string') {
return p + parseFloat(c);
@@ -162,6 +166,8 @@ function sum(value: unknown[]): number {
}
function min(value: unknown[]): number {
ensureNumberArray(value, { fnName: 'min' });
return Math.min(
...value.map((v) => {
if (typeof v === 'string') {
@@ -176,6 +182,8 @@ function min(value: unknown[]): number {
}
function max(value: unknown[]): number {
ensureNumberArray(value, { fnName: 'max' });
return Math.max(
...value.map((v) => {
if (typeof v === 'string') {
@@ -190,6 +198,8 @@ function max(value: unknown[]): number {
}
export function average(value: unknown[]) {
ensureNumberArray(value, { fnName: 'average' });
// This would usually be NaN but I don't think users
// will expect that
if (value.length === 0) {
@@ -200,7 +210,11 @@ export function average(value: unknown[]) {
function compact(value: unknown[]): unknown[] {
return value
.filter((v) => v !== null && v !== undefined)
.filter((v) => {
if (v && typeof v === 'object' && Object.keys(v).length === 0) return false;
return v !== null && v !== undefined && v !== 'nil' && v !== '';
})
.map((v) => {
if (typeof v === 'object' && v !== null) {
return oCompact(v);
@@ -213,7 +227,7 @@ function smartJoin(value: unknown[], extraArgs: string[]): object {
const [keyField, valueField] = extraArgs;
if (!keyField || !valueField || typeof keyField !== 'string' || typeof valueField !== 'string') {
throw new ExpressionExtensionError(
'smartJoin requires 2 arguments: keyField and nameField. e.g. .smartJoin("name", "value")',
'smartJoin(): expected two string args, e.g. .smartJoin("name", "value")',
);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-return
@@ -229,8 +243,8 @@ function smartJoin(value: unknown[], extraArgs: string[]): object {
function chunk(value: unknown[], extraArgs: number[]) {
const [chunkSize] = extraArgs;
if (typeof chunkSize !== 'number') {
throw new ExpressionExtensionError('chunk requires 1 parameter: chunkSize. e.g. .chunk(5)');
if (typeof chunkSize !== 'number' || chunkSize === 0) {
throw new ExpressionExtensionError('chunk(): expected non-zero numeric arg, e.g. .chunk(5)');
}
const chunks: unknown[][] = [];
for (let i = 0; i < value.length; i += chunkSize) {
@@ -241,30 +255,10 @@ function chunk(value: unknown[], extraArgs: number[]) {
return chunks;
}
function filter(value: unknown[], extraArgs: unknown[]): unknown[] {
const [field, term] = extraArgs as [string | (() => void), unknown | string];
if (typeof field !== 'string' && typeof field !== 'function') {
throw new ExpressionExtensionError(
'filter requires 1 or 2 arguments: (field and term), (term and [optional keepOrRemove "keep" or "remove" default "keep"] (for string arrays)), or function. e.g. .filter("type", "home") or .filter((i) => i.type === "home") or .filter("home", [optional keepOrRemove]) (for string arrays)',
);
}
if (value.every((i) => typeof i === 'string') && typeof field === 'string') {
return (value as string[]).filter((i) =>
term === 'remove' ? !i.includes(field) : i.includes(field),
);
} else if (typeof field === 'string') {
return value.filter(
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any
(v) => typeof v === 'object' && v !== null && field in v && (v as any)[field] === term,
);
}
return value.filter(field);
}
function renameKeys(value: unknown[], extraArgs: string[]): unknown[] {
if (extraArgs.length === 0 || extraArgs.length % 2 !== 0) {
throw new ExpressionExtensionError(
'renameKeys requires an even amount of arguments: from1, to1 [, from2, to2, ...]. e.g. .renameKeys("name", "title")',
'renameKeys(): expected an even amount of args: from1, to1 [, from2, to2, ...]. e.g. .renameKeys("name", "title")',
);
}
return value.map((v) => {
@@ -291,7 +285,7 @@ function merge(value: unknown[], extraArgs: unknown[][]): unknown[] {
const [others] = extraArgs;
if (!Array.isArray(others)) {
throw new ExpressionExtensionError(
'merge requires 1 argument that is an array. e.g. .merge([{ id: 1, otherValue: 3 }])',
'merge(): expected array arg, e.g. .merge([{ id: 1, otherValue: 3 }])',
);
}
const listLength = value.length > others.length ? value.length : others.length;
@@ -314,9 +308,7 @@ function merge(value: unknown[], extraArgs: unknown[][]): unknown[] {
function union(value: unknown[], extraArgs: unknown[][]): unknown[] {
const [others] = extraArgs;
if (!Array.isArray(others)) {
throw new ExpressionExtensionError(
'union requires 1 argument that is an array. e.g. .union([1, 2, 3, 4])',
);
throw new ExpressionExtensionError('union(): expected array arg, e.g. .union([1, 2, 3, 4])');
}
const newArr: unknown[] = Array.from(value);
for (const v of others) {
@@ -331,7 +323,7 @@ function difference(value: unknown[], extraArgs: unknown[][]): unknown[] {
const [others] = extraArgs;
if (!Array.isArray(others)) {
throw new ExpressionExtensionError(
'difference requires 1 argument that is an array. e.g. .difference([1, 2, 3, 4])',
'difference(): expected array arg, e.g. .difference([1, 2, 3, 4])',
);
}
const newArr: unknown[] = [];
@@ -347,7 +339,7 @@ function intersection(value: unknown[], extraArgs: unknown[][]): unknown[] {
const [others] = extraArgs;
if (!Array.isArray(others)) {
throw new ExpressionExtensionError(
'difference requires 1 argument that is an array. e.g. .difference([1, 2, 3, 4])',
'intersection(): expected array arg, e.g. .intersection([1, 2, 3, 4])',
);
}
const newArr: unknown[] = [];
@@ -364,27 +356,129 @@ function intersection(value: unknown[], extraArgs: unknown[][]): unknown[] {
return unique(newArr, []);
}
average.doc = {
name: 'average',
description: 'Returns the mean average of all values in the array',
returnType: 'number',
};
compact.doc = {
name: 'compact',
description: 'Removes all empty values from the array',
returnType: 'array',
};
isEmpty.doc = {
name: 'isEmpty',
description: 'Checks if the array doesnt have any elements',
returnType: 'boolean',
};
isNotEmpty.doc = {
name: 'isNotEmpty',
description: 'Checks if the array has elements',
returnType: 'boolean',
};
first.doc = {
name: 'first',
description: 'Returns the first element of the array',
returnType: 'array item',
};
last.doc = {
name: 'last',
description: 'Returns the last element of the array',
returnType: 'array item',
};
max.doc = {
name: 'max',
description: 'Gets the maximum value from a number-only array',
returnType: 'number',
};
min.doc = {
name: 'min',
description: 'Gets the minimum value from a number-only array',
returnType: 'number',
};
randomItem.doc = {
name: 'randomItem',
description: 'Returns a random element from an array',
returnType: 'number',
};
sum.doc = {
name: 'sum',
description: 'Returns the total sum all the values in an array of parsable numbers',
returnType: 'number',
};
// @TODO_NEXT_PHASE: Surface extensions below which take args
chunk.doc = {
name: 'chunk',
returnType: 'array',
};
difference.doc = {
name: 'difference',
returnType: 'array',
};
intersection.doc = {
name: 'intersection',
returnType: 'array',
};
merge.doc = {
name: 'merge',
returnType: 'array',
};
pluck.doc = {
name: 'pluck',
returnType: 'array',
};
renameKeys.doc = {
name: 'renameKeys',
returnType: 'array',
};
smartJoin.doc = {
name: 'smartJoin',
returnType: 'array',
};
union.doc = {
name: 'union',
returnType: 'array',
};
unique.doc = {
name: 'unique',
returnType: 'array item',
aliases: ['removeDuplicates'],
};
export const arrayExtensions: ExtensionMap = {
typeName: 'Array',
functions: {
count: length,
duplicates: unique,
filter,
removeDuplicates: unique,
first,
last,
length,
pluck,
unique,
random,
randomItem: random,
remove: unique,
size: length,
randomItem,
sum,
min,
max,
average,
isPresent,
isBlank,
isNotEmpty,
isEmpty,
compact,
smartJoin,
chunk,