PromucFlow_constructor/app/client/src/utils/autocomplete/defCreatorUtils.ts
arunvjn 24b93ebfd4
feat: Added hints for function arguments (#28214)
## Description
Adds a tooltip that shows the function signatures when the cursor is
between ( ) to help complete function arguments.

https://www.figma.com/file/5QitVVvqgEc6nhR7SbdinY/ADS2.0---Tokens?type=design&node-id=7639-124926&mode=dev



#### PR fixes following issue(s)
Fixes #4789
>
#### Media
<img width="483" alt="Screenshot 2023-11-06 at 12 10 42"
src="https://github.com/appsmithorg/appsmith/assets/32433245/efbe1aba-9067-4d1e-b9df-72a24f4f8c64">

#### Type of change
- New feature (non-breaking change which adds functionality)
>
>
## Testing
>
#### How Has This Been Tested?
- [x] Manual
- [x] Cypress
>
>
#### Test Plan
- [x] Function signature tooltip should show when the cursor is in
between `( )`.
- [x] Tooltip should disappear when the cursor is moved outside '( )`.
- [x] Tooltip should disappear when peek over popover is triggered.
- [x] Tooltip appear as soon as a function is selected from autocomplete
results.
- [x] Evaluated Value popover should disappear when tooltip popover is
open.


#### Issues raised during DP testing
> Link issues raised during DP testing for better visiblity and tracking
(copy link from comments dropped on this PR)
>
>
## Checklist:
#### Dev activity
- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my own code
- [x] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] PR is being merged under a feature flag


#### QA activity:
- [ ] [Speedbreak
features](https://github.com/appsmithorg/TestSmith/wiki/Guidelines-for-test-plans#speedbreakers-)
have been covered
- [ ] Test plan covers all impacted features and [areas of
interest](https://github.com/appsmithorg/TestSmith/wiki/Guidelines-for-test-plans#areas-of-interest-)
- [ ] Test plan has been peer reviewed by project stakeholders and other
QA members
- [ ] Manually tested functionality on DP
- [ ] We had an implementation alignment call with stakeholders post QA
Round 2
- [ ] Cypress test cases have been added and approved by SDET/manual QA
- [ ] Added `Test Plan Approved` label after Cypress tests were reviewed
- [ ] Added `Test Plan Approved` label after JUnit tests were reviewed

---------

Co-authored-by: Aishwarya UR <aishwarya@appsmith.com>
2023-11-07 11:30:32 +05:30

146 lines
4.3 KiB
TypeScript

import { isTrueObject } from "@shared/ast/src/utils";
import type { WidgetEntityConfig } from "@appsmith/entities/DataTree/types";
import type { DataTreeEntity } from "entities/DataTree/dataTreeTypes";
import type { Variable } from "entities/JSCollection";
import { isObject, uniqueId } from "lodash";
import type { Def } from "tern";
import { Types, getType } from "utils/TypeHelpers";
import { shouldAddSetter } from "workers/Evaluation/evaluate";
import { typeToTernType } from "workers/common/JSLibrary/ternDefinitionGenerator";
export type ExtraDef = Record<string, Def | string>;
export const flattenDef = (def: Def, entityName: string): Def => {
const flattenedDef = def;
if (!isTrueObject(def[entityName])) return flattenedDef;
Object.entries(def[entityName]).forEach(([key, value]) => {
if (key.startsWith("!")) return;
const keyIsValid = isValidVariableName(key);
const parentCompletion = !keyIsValid
? `${entityName}["${key}"]`
: `${entityName}.${key}`;
flattenedDef[parentCompletion] = value;
if (!isTrueObject(value)) return;
Object.entries(value).forEach(([subKey, subValue]) => {
if (subKey.startsWith("!")) return;
const childKeyIsValid = isValidVariableName(subKey);
const childCompletion = !childKeyIsValid
? `${parentCompletion}["${subKey}"]`
: `${parentCompletion}.${subKey}`;
flattenedDef[childCompletion] = subValue;
});
});
return flattenedDef;
};
export function generateTypeDef(
value: unknown,
extraDefsToDefine?: ExtraDef,
depth = 0,
): Def | string {
switch (getType(value)) {
case Types.ARRAY: {
const array = value as [unknown];
if (depth > 5) {
return `[?]`;
}
const arrayElementType = generateTypeDef(
array[0],
extraDefsToDefine,
depth + 1,
);
if (isObject(arrayElementType)) {
if (extraDefsToDefine) {
const uniqueDefName = uniqueId("def_");
extraDefsToDefine[uniqueDefName] = arrayElementType;
return `[${uniqueDefName}]`;
}
return `[?]`;
}
return `[${arrayElementType}]`;
}
case Types.OBJECT: {
const objType: Def = {};
const object = value as Record<string, unknown>;
Object.keys(object).forEach((k) => {
objType[k] = generateTypeDef(object[k], extraDefsToDefine, depth);
});
return objType;
}
case Types.STRING:
return "string";
case Types.NUMBER:
return "number";
case Types.BOOLEAN:
return "bool";
case Types.NULL:
case Types.UNDEFINED:
return "?";
default:
return "?";
}
}
const VALID_VARIABLE_NAME_REGEX = /^([a-zA-Z_$][a-zA-Z\d_$]*)$/;
export const isValidVariableName = (variableName: string) =>
VALID_VARIABLE_NAME_REGEX.test(variableName);
export const getFunctionsArgsType = (args: Variable[]): string => {
// skip same name args to avoiding creating invalid type
const argNames = new Set<string>();
// skip invalid args name
args.forEach((arg) => {
if (arg.name && isValidVariableName(arg.name)) argNames.add(arg.name);
});
const argNamesArray = [...argNames];
const argsTypeString = argNamesArray.reduce(
(accumulatedArgType, argName, currentIndex) => {
switch (currentIndex) {
case 0:
return `${argName}: ?`;
case 1:
return `${accumulatedArgType}, ${argName}: ?`;
default:
return `${accumulatedArgType}, ${argName}: ?`;
}
},
argNamesArray[0],
);
return argsTypeString ? `fn(${argsTypeString})` : `fn()`;
};
export function generateJSFunctionTypeDef(
jsData: Record<string, unknown> = {},
fullFunctionName: string,
extraDefs: ExtraDef,
) {
return {
"!type": getFunctionsArgsType([]),
data: generateTypeDef(jsData[fullFunctionName], extraDefs),
};
}
export function addSettersToDefinitions(
definitions: Def,
entity: DataTreeEntity,
entityConfig?: WidgetEntityConfig,
) {
if (entityConfig && entityConfig.__setters) {
const setters = Object.keys(entityConfig.__setters);
setters.forEach((setterName: string) => {
const setter = entityConfig.__setters?.[setterName];
const setterType = typeToTernType(
entityConfig.__setters?.[setterName].type,
);
if (shouldAddSetter(setter, entity)) {
definitions[setterName] = `fn(value: ${setterType}) -> +Promise`;
}
});
}
}