chore: Move constants, types and utils to their own files in Action Creator (#16947)

* Move constants and regex to their own files from index and fields file

* Move types to to its own file

* Move utils to its file

* Add proper types for functions in fields file

* Rename Switch -> Switchtype

* Fix imports

* Fix NAVIGATION_TARGET_FIELD_OPTIONS constant so that the build works

* Jest tests to cover the utils

* Add jest test cases

* Update cases

* Code review changes
This commit is contained in:
Rimil Dey 2022-09-26 10:05:04 +05:30 committed by GitHub
parent a64f27ce27
commit 4e9068ddba
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 713 additions and 427 deletions

View File

@ -7,7 +7,7 @@ export function createMessage(
/*
For self hosted, it displays the string "Appsmith Community v1.10.0" or "Appsmith Business v1.10.0".
For cloud hosting, it displays "Appsmith v1.10.0".
For cloud hosting, it displays "Appsmith v1.10.0".
This is because Appsmith Cloud doesn't support business features yet.
*/
export const APPSMITH_DISPLAY_VERSION = (
@ -1244,3 +1244,11 @@ export const GENERATE_PAGE = () => "Generate page from data table";
export const GENERATE_PAGE_DESCRIPTION = () =>
"Start app with a simple CRUD UI and customize it";
export const ADD_PAGE_FROM_TEMPLATE = () => "Add Page From Template";
// Alert options and labels for showMessage types
export const ALERT_STYLE_OPTIONS = [
{ label: "Info", value: "'info'", id: "info" },
{ label: "Success", value: "'success'", id: "success" },
{ label: "Error", value: "'error'", id: "error" },
{ label: "Warning", value: "'warning'", id: "warning" },
];

View File

@ -1,80 +0,0 @@
jest.mock("sagas/ActionExecution/NavigateActionSaga", () => ({
__esModule: true,
default: "",
NavigationTargetType: { SAME_WINDOW: "" },
}));
import { argsStringToArray } from "./Fields";
describe("Test argStringToArray", () => {
const cases = [
{ index: 0, input: "", expected: [""] },
{ index: 1, input: "'a'", expected: ["'a'"] },
{ index: 2, input: "a", expected: ["a"] },
{ index: 3, input: "'a,b,c'", expected: ["'a,b,c'"] },
{ index: 4, input: "a,b,c", expected: ["a", "b", "c"] },
{ index: 5, input: "a, b, c", expected: ["a", " b", " c"] },
{ index: 6, input: "a , b , c", expected: ["a ", " b ", " c"] },
{ index: 7, input: "a\n,\nb,\nc", expected: ["a\n", "\nb", "\nc"] },
{ index: 8, input: "[a,b,c]", expected: ["[a,b,c]"] },
{ index: 9, input: "[a, b, c]", expected: ["[a, b, c]"] },
{
index: 10,
input: "[\n\ta,\n\tb,\n\tc\n]",
expected: ["[\n\ta,\n\tb,\n\tc\n]"],
},
{ index: 11, input: "{a:1,b:2,c:3}", expected: ["{a:1,b:2,c:3}"] },
{
index: 12,
input: '{"a":1,"b":2,"c":3}',
expected: ['{"a":1,"b":2,"c":3}'],
},
{
index: 13,
input: "{\n\ta:1,\n\tb:2,\n\tc:3}",
expected: ["{\n\ta:1,\n\tb:2,\n\tc:3}"],
},
{
index: 14,
input: "()=>{}",
expected: ["()=>{}"],
},
{
index: 15,
input: "(a, b)=>{return a+b}",
expected: ["(a, b)=>{return a+b}"],
},
{
index: 16,
input: "(a, b)=>{\n\treturn a+b;\n\t}",
expected: ["(a, b)=>{\n\treturn a+b;\n\t}"],
},
{
index: 17,
input: "(\n\ta,\n\tb\n)=>{\n\treturn a+b;\n\t}",
expected: ["(\n\ta,\n\tb\n)=>{\n\treturn a+b;\n\t}"],
},
{
index: 18,
input: `() => {return 5}`,
expected: ["() => {return 5}"],
},
{
index: 19,
input: `(a) => {return a + 1}`,
expected: ["(a) => {return a + 1}"],
},
{
index: 20,
input: `(a, b) => {return a + b}`,
expected: ["(a, b) => {return a + b}"],
},
];
test.each(cases.map((x) => [x.index, x.input, x.expected]))(
"test case %d",
(_, input, expected) => {
const result = argsStringToArray(input as string);
expect(result).toStrictEqual(expected);
},
);
});

View File

@ -1,11 +1,9 @@
import React from "react";
import {
TreeDropdown,
Setter,
TreeDropdownOption,
Switcher,
SwitcherProps,
} from "design-system";
import {
ControlWrapper,
@ -16,7 +14,6 @@ import {
} from "components/propertyControls/StyledControls";
import { KeyValueComponent } from "components/propertyControls/KeyValueComponent";
import { InputText } from "components/propertyControls/InputTextControl";
import { getDynamicBindings, isDynamicValue } from "utils/DynamicBindingUtils";
import HightlightedCode from "components/editorComponents/HighlightedCode";
import { Skin } from "constants/DefaultTheme";
import { DropdownOption } from "components/constants";
@ -26,13 +23,33 @@ import DividerComponent from "widgets/DividerWidget/component";
import store from "store";
import { getPageList } from "selectors/entitiesSelector";
import {
APPSMITH_GLOBAL_FUNCTIONS,
APPSMITH_NAMESPACED_FUNCTIONS,
RESET_CHILDREN_OPTIONS,
FILE_TYPE_OPTIONS,
NAVIGATION_TARGET_FIELD_OPTIONS,
ViewTypes,
AppsmithFunction,
FieldType,
} from "./constants";
import { PopoverPosition } from "@blueprintjs/core";
/* eslint-disable @typescript-eslint/ban-types */
/* TODO: Function and object types need to be updated to enable the lint rule */
import { ACTION_TRIGGER_REGEX } from "./regex";
import {
SwitchType,
ActionType,
SelectorViewProps,
KeyValueViewProps,
TextViewProps,
TabViewProps,
FieldConfigs,
} from "./types";
import {
modalSetter,
modalGetter,
textSetter,
textGetter,
enumTypeSetter,
enumTypeGetter,
} from "./utils";
import { ALERT_STYLE_OPTIONS } from "../../../ce/constants/messages";
/**
******** Steps to add a new function *******
@ -54,234 +71,6 @@ import { PopoverPosition } from "@blueprintjs/core";
* 2. Attach fields to the new action in the getFieldFromValue function
**/
type Switch = {
id: string;
text: string;
action: () => void;
};
const ALERT_STYLE_OPTIONS = [
{ label: "Info", value: "'info'", id: "info" },
{ label: "Success", value: "'success'", id: "success" },
{ label: "Error", value: "'error'", id: "error" },
{ label: "Warning", value: "'warning'", id: "warning" },
];
const RESET_CHILDREN_OPTIONS = [
{ label: "true", value: "true", id: "true" },
{ label: "false", value: "false", id: "false" },
];
const FILE_TYPE_OPTIONS = [
{ label: "Select file type (optional)", value: "", id: "" },
{ label: "Plain text", value: "'text/plain'", id: "text/plain" },
{ label: "HTML", value: "'text/html'", id: "text/html" },
{ label: "CSV", value: "'text/csv'", id: "text/csv" },
{ label: "JSON", value: "'application/json'", id: "application/json" },
{ label: "JPEG", value: "'image/jpeg'", id: "image/jpeg" },
{ label: "PNG", value: "'image/png'", id: "image/png" },
{ label: "SVG", value: "'image/svg+xml'", id: "image/svg+xml" },
];
const NAVIGATION_TARGET_FIELD_OPTIONS = [
{
label: "Same window",
value: `'${NavigationTargetType.SAME_WINDOW}'`,
id: NavigationTargetType.SAME_WINDOW,
},
{
label: "New window",
value: `'${NavigationTargetType.NEW_WINDOW}'`,
id: NavigationTargetType.NEW_WINDOW,
},
];
export const FUNC_ARGS_REGEX = /((["][^"]*["])|([\[][\s\S]*[\]])|([\{][\s\S]*[\}])|(['][^']*['])|([\(][\s\S]*[\)][ ]*=>[ ]*[{][\s\S]*[}])|([^'",][^,"+]*[^'",]*))*/gi;
export const ACTION_TRIGGER_REGEX = /^{{([\s\S]*?)\(([\s\S]*?)\)}}$/g;
//Old Regex:: /\(\) => ([\s\S]*?)(\([\s\S]*?\))/g;
export const ACTION_ANONYMOUS_FUNC_REGEX = /\(\) => (({[\s\S]*?})|([\s\S]*?)(\([\s\S]*?\)))/g;
export const IS_URL_OR_MODAL = /^'.*'$/;
const modalSetter = (changeValue: any, currentValue: string) => {
const matches = [...currentValue.matchAll(ACTION_TRIGGER_REGEX)];
let args: string[] = [];
if (matches.length) {
args = matches[0][2].split(",");
if (isDynamicValue(changeValue)) {
args[0] = `${changeValue.substring(2, changeValue.length - 2)}`;
} else {
args[0] = `'${changeValue}'`;
}
}
return currentValue.replace(
ACTION_TRIGGER_REGEX,
`{{$1(${args.join(",")})}}`,
);
};
export const modalGetter = (value: string) => {
const matches = [...value.matchAll(ACTION_TRIGGER_REGEX)];
let name = "none";
if (matches.length) {
const modalName = matches[0][2].split(",")[0];
if (IS_URL_OR_MODAL.test(modalName) || modalName === "") {
name = modalName.substring(1, modalName.length - 1);
} else {
name = `{{${modalName}}}`;
}
}
return name;
};
export const stringToJS = (string: string): string => {
const { jsSnippets, stringSegments } = getDynamicBindings(string);
const js = stringSegments
.map((segment, index) => {
if (jsSnippets[index] && jsSnippets[index].length > 0) {
return jsSnippets[index];
} else {
return `'${segment}'`;
}
})
.join(" + ");
return js;
};
export const JSToString = (js: string): string => {
const segments = js.split(" + ");
return segments
.map((segment) => {
if (segment.charAt(0) === "'") {
return segment.substring(1, segment.length - 1);
} else return "{{" + segment + "}}";
})
.join("");
};
export const argsStringToArray = (funcArgs: string): string[] => {
const argsplitMatches = [...funcArgs.matchAll(FUNC_ARGS_REGEX)];
const arr: string[] = [];
let isPrevUndefined = true;
argsplitMatches.forEach((match) => {
const matchVal = match[0];
if (!matchVal || matchVal === "") {
if (isPrevUndefined) {
arr.push(matchVal);
}
isPrevUndefined = true;
} else {
isPrevUndefined = false;
arr.push(matchVal);
}
});
return arr;
};
const textSetter = (
changeValue: any,
currentValue: string,
argNum: number,
): string => {
const matches = [...currentValue.matchAll(ACTION_TRIGGER_REGEX)];
let args: string[] = [];
if (matches.length) {
args = argsStringToArray(matches[0][2]);
const jsVal = stringToJS(changeValue);
args[argNum] = jsVal;
}
const result = currentValue.replace(
ACTION_TRIGGER_REGEX,
`{{$1(${args.join(",")})}}`,
);
return result;
};
const textGetter = (value: string, argNum: number) => {
const matches = [...value.matchAll(ACTION_TRIGGER_REGEX)];
if (matches.length) {
const args = argsStringToArray(matches[0][2]);
const arg = args[argNum];
const stringFromJS = arg ? JSToString(arg.trim()) : arg;
return stringFromJS;
}
return "";
};
const enumTypeSetter = (
changeValue: any,
currentValue: string,
argNum: number,
): string => {
const matches = [...currentValue.matchAll(ACTION_TRIGGER_REGEX)];
let args: string[] = [];
if (matches.length) {
args = argsStringToArray(matches[0][2]);
args[argNum] = changeValue as string;
}
const result = currentValue.replace(
ACTION_TRIGGER_REGEX,
`{{$1(${args.join(",")})}}`,
);
return result;
};
const enumTypeGetter = (
value: string,
argNum: number,
defaultValue = "",
): string => {
const matches = [...value.matchAll(ACTION_TRIGGER_REGEX)];
if (matches.length) {
const args = argsStringToArray(matches[0][2]);
const arg = args[argNum];
return arg ? arg.trim() : defaultValue;
}
return defaultValue;
};
export const ActionType = {
none: "none",
integration: "integration",
jsFunction: "jsFunction",
...APPSMITH_GLOBAL_FUNCTIONS,
...APPSMITH_NAMESPACED_FUNCTIONS,
};
type ActionType = typeof ActionType[keyof typeof ActionType];
const ViewTypes = {
SELECTOR_VIEW: "SELECTOR_VIEW",
KEY_VALUE_VIEW: "KEY_VALUE_VIEW",
TEXT_VIEW: "TEXT_VIEW",
BOOL_VIEW: "BOOL_VIEW",
TAB_VIEW: "TAB_VIEW",
};
type ViewTypes = typeof ViewTypes[keyof typeof ViewTypes];
type ViewProps = {
label: string;
get: Function;
set: Function;
value: string;
};
type SelectorViewProps = ViewProps & {
options: TreeDropdownOption[];
defaultText: string;
getDefaults?: (value?: any) => any;
displayValue?: string;
selectedLabelModifier?: (
option: TreeDropdownOption,
displayValue?: string,
) => React.ReactNode;
index?: number;
};
type KeyValueViewProps = ViewProps;
type TextViewProps = ViewProps & {
index?: number;
additionalAutoComplete?: Record<string, Record<string, unknown>>;
};
type TabViewProps = Omit<ViewProps, "get" | "set"> & SwitcherProps;
const views = {
[ViewTypes.SELECTOR_VIEW]: function SelectorView(props: SelectorViewProps) {
return (
@ -357,46 +146,6 @@ const views = {
},
};
export enum FieldType {
ACTION_SELECTOR_FIELD = "ACTION_SELECTOR_FIELD",
JS_ACTION_SELECTOR_FIELD = "JS_ACTION_SELECTOR_FIELD",
ON_SUCCESS_FIELD = "ON_SUCCESS_FIELD",
ON_ERROR_FIELD = "ON_ERROR_FIELD",
SHOW_MODAL_FIELD = "SHOW_MODAL_FIELD",
CLOSE_MODAL_FIELD = "CLOSE_MODAL_FIELD",
PAGE_SELECTOR_FIELD = "PAGE_SELECTOR_FIELD",
KEY_VALUE_FIELD = "KEY_VALUE_FIELD",
URL_FIELD = "URL_FIELD",
ALERT_TEXT_FIELD = "ALERT_TEXT_FIELD",
ALERT_TYPE_SELECTOR_FIELD = "ALERT_TYPE_SELECTOR_FIELD",
KEY_TEXT_FIELD = "KEY_TEXT_FIELD",
VALUE_TEXT_FIELD = "VALUE_TEXT_FIELD",
QUERY_PARAMS_FIELD = "QUERY_PARAMS_FIELD",
DOWNLOAD_DATA_FIELD = "DOWNLOAD_DATA_FIELD",
DOWNLOAD_FILE_NAME_FIELD = "DOWNLOAD_FILE_NAME_FIELD",
DOWNLOAD_FILE_TYPE_FIELD = "DOWNLOAD_FILE_TYPE_FIELD",
COPY_TEXT_FIELD = "COPY_TEXT_FIELD",
NAVIGATION_TARGET_FIELD = "NAVIGATION_TARGET_FIELD",
WIDGET_NAME_FIELD = "WIDGET_NAME_FIELD",
RESET_CHILDREN_FIELD = "RESET_CHILDREN_FIELD",
ARGUMENT_KEY_VALUE_FIELD = "ARGUMENT_KEY_VALUE_FIELD",
CALLBACK_FUNCTION_FIELD = "CALLBACK_FUNCTION_FIELD",
DELAY_FIELD = "DELAY_FIELD",
ID_FIELD = "ID_FIELD",
CLEAR_INTERVAL_ID_FIELD = "CLEAR_INTERVAL_ID_FIELD",
MESSAGE_FIELD = "MESSAGE_FIELD",
TARGET_ORIGIN_FIELD = "TARGET_ORIGIN_FIELD",
PAGE_NAME_AND_URL_TAB_SELECTOR_FIELD = "PAGE_NAME_AND_URL_TAB_SELECTOR_FIELD",
}
type FieldConfig = {
getter: Function;
setter: Function;
view: ViewTypes;
};
type FieldConfigs = Partial<Record<FieldType, FieldConfig>>;
const fieldConfigs: FieldConfigs = {
[FieldType.ACTION_SELECTOR_FIELD]: {
getter: (storedValue: string) => {
@ -406,9 +155,9 @@ const fieldConfigs: FieldConfigs = {
? [...storedValue.matchAll(ACTION_TRIGGER_REGEX)]
: [];
}
let mainFuncSelectedValue = ActionType.none;
let mainFuncSelectedValue = AppsmithFunction.none;
if (matches.length) {
mainFuncSelectedValue = matches[0][1] || ActionType.none;
mainFuncSelectedValue = matches[0][1] || AppsmithFunction.none;
}
const mainFuncSelectedValueSplit = mainFuncSelectedValue.split(".");
if (mainFuncSelectedValueSplit[1] === "run") {
@ -422,22 +171,22 @@ const fieldConfigs: FieldConfigs = {
let defaultParams = "";
let defaultArgs: Array<any> = [];
switch (type) {
case ActionType.integration:
case AppsmithFunction.integration:
value = `${value}.run`;
break;
case ActionType.navigateTo:
case AppsmithFunction.navigateTo:
defaultParams = `'', {}, 'SAME_WINDOW'`;
break;
case ActionType.jsFunction:
case AppsmithFunction.jsFunction:
defaultArgs = option.args ? option.args : [];
break;
case ActionType.setInterval:
case AppsmithFunction.setInterval:
defaultParams = "() => { \n\t // add code here \n}, 5000";
break;
case ActionType.getGeolocation:
case AppsmithFunction.getGeolocation:
defaultParams = "(location) => { \n\t // add code here \n }";
break;
case ActionType.resetWidget:
case AppsmithFunction.resetWidget:
defaultParams = `"",true`;
break;
default:
@ -687,7 +436,7 @@ const fieldConfigs: FieldConfigs = {
};
function renderField(props: {
onValueChange: Function;
onValueChange: (newValue: string, isUpdatedViaKeyboard: boolean) => void;
value: string;
field: { field: FieldType; value: string; label: string; index: number };
label?: string;
@ -698,8 +447,8 @@ function renderField(props: {
depth: number;
maxDepth: number;
additionalAutoComplete?: Record<string, Record<string, unknown>>;
activeNavigateToTab: Switch;
navigateToSwitches: Array<Switch>;
activeNavigateToTab: SwitchType;
navigateToSwitches: Array<SwitchType>;
}) {
const { field } = props;
const fieldType = field.field;
@ -739,7 +488,7 @@ function renderField(props: {
option: TreeDropdownOption,
displayValue?: string,
) {
if (option.type === ActionType.integration) {
if (option.type === AppsmithFunction.integration) {
return (
<HightlightedCode
codeText={`{{${option.label}.run()}}`}
@ -755,7 +504,7 @@ function renderField(props: {
};
getDefaults = (value: string) => {
return {
[ActionType.navigateTo]: `'${props.pageDropdownOptions[0].label}'`,
[AppsmithFunction.navigateTo]: `'${props.pageDropdownOptions[0].label}'`,
}[value];
};
}
@ -838,7 +587,7 @@ function renderField(props: {
props.value,
props.field.index,
);
props.onValueChange(finalValueToSet);
props.onValueChange(finalValueToSet, false);
},
index: props.field.index,
value: props.value || "",
@ -851,7 +600,7 @@ function renderField(props: {
get: fieldConfig.getter,
set: (value: string | DropdownOption) => {
const finalValueToSet = fieldConfig.setter(value, props.value);
props.onValueChange(finalValueToSet);
props.onValueChange(finalValueToSet, false);
},
value: props.value,
defaultText: "Select Action",
@ -918,7 +667,7 @@ function renderField(props: {
}
function Fields(props: {
onValueChange: Function;
onValueChange: (newValue: string, isUpdatedViaKeyboard: boolean) => void;
value: string;
fields: any;
label?: string;
@ -929,8 +678,8 @@ function Fields(props: {
depth: number;
maxDepth: number;
additionalAutoComplete?: Record<string, Record<string, unknown>>;
navigateToSwitches: Array<Switch>;
activeNavigateToTab: Switch;
navigateToSwitches: Array<SwitchType>;
activeNavigateToTab: SwitchType;
}) {
const { fields, ...otherProps } = props;

View File

@ -16,3 +16,85 @@ export const APPSMITH_NAMESPACED_FUNCTIONS = {
watchGeolocation: "appsmith.geolocation.watchPosition",
stopWatchGeolocation: "appsmith.geolocation.clearWatch",
};
export const AppsmithFunction = {
none: "none",
integration: "integration",
jsFunction: "jsFunction",
...APPSMITH_GLOBAL_FUNCTIONS,
...APPSMITH_NAMESPACED_FUNCTIONS,
};
export const RESET_CHILDREN_OPTIONS = [
{ label: "true", value: "true", id: "true" },
{ label: "false", value: "false", id: "false" },
];
export const FILE_TYPE_OPTIONS = [
{ label: "Select file type (optional)", value: "", id: "" },
{ label: "Plain text", value: "'text/plain'", id: "text/plain" },
{ label: "HTML", value: "'text/html'", id: "text/html" },
{ label: "CSV", value: "'text/csv'", id: "text/csv" },
{ label: "JSON", value: "'application/json'", id: "application/json" },
{ label: "JPEG", value: "'image/jpeg'", id: "image/jpeg" },
{ label: "PNG", value: "'image/png'", id: "image/png" },
{ label: "SVG", value: "'image/svg+xml'", id: "image/svg+xml" },
];
export const NAVIGATION_TARGET_FIELD_OPTIONS = [
{
label: "Same window",
value: "SAME_WINDOW",
id: "SAME_WINDOW",
},
{
label: "New window",
value: "NEW_WINDOW",
id: "NEW_WINDOW",
},
];
export const ViewTypes = {
SELECTOR_VIEW: "SELECTOR_VIEW",
KEY_VALUE_VIEW: "KEY_VALUE_VIEW",
TEXT_VIEW: "TEXT_VIEW",
BOOL_VIEW: "BOOL_VIEW",
TAB_VIEW: "TAB_VIEW",
};
export const NAVIGATE_TO_TAB_OPTIONS = {
PAGE_NAME: "page-name",
URL: "url",
};
export enum FieldType {
ACTION_SELECTOR_FIELD = "ACTION_SELECTOR_FIELD",
JS_ACTION_SELECTOR_FIELD = "JS_ACTION_SELECTOR_FIELD",
ON_SUCCESS_FIELD = "ON_SUCCESS_FIELD",
ON_ERROR_FIELD = "ON_ERROR_FIELD",
SHOW_MODAL_FIELD = "SHOW_MODAL_FIELD",
CLOSE_MODAL_FIELD = "CLOSE_MODAL_FIELD",
PAGE_SELECTOR_FIELD = "PAGE_SELECTOR_FIELD",
KEY_VALUE_FIELD = "KEY_VALUE_FIELD",
URL_FIELD = "URL_FIELD",
ALERT_TEXT_FIELD = "ALERT_TEXT_FIELD",
ALERT_TYPE_SELECTOR_FIELD = "ALERT_TYPE_SELECTOR_FIELD",
KEY_TEXT_FIELD = "KEY_TEXT_FIELD",
VALUE_TEXT_FIELD = "VALUE_TEXT_FIELD",
QUERY_PARAMS_FIELD = "QUERY_PARAMS_FIELD",
DOWNLOAD_DATA_FIELD = "DOWNLOAD_DATA_FIELD",
DOWNLOAD_FILE_NAME_FIELD = "DOWNLOAD_FILE_NAME_FIELD",
DOWNLOAD_FILE_TYPE_FIELD = "DOWNLOAD_FILE_TYPE_FIELD",
COPY_TEXT_FIELD = "COPY_TEXT_FIELD",
NAVIGATION_TARGET_FIELD = "NAVIGATION_TARGET_FIELD",
WIDGET_NAME_FIELD = "WIDGET_NAME_FIELD",
RESET_CHILDREN_FIELD = "RESET_CHILDREN_FIELD",
ARGUMENT_KEY_VALUE_FIELD = "ARGUMENT_KEY_VALUE_FIELD",
CALLBACK_FUNCTION_FIELD = "CALLBACK_FUNCTION_FIELD",
DELAY_FIELD = "DELAY_FIELD",
ID_FIELD = "ID_FIELD",
CLEAR_INTERVAL_ID_FIELD = "CLEAR_INTERVAL_ID_FIELD",
MESSAGE_FIELD = "MESSAGE_FIELD",
TARGET_ORIGIN_FIELD = "TARGET_ORIGIN_FIELD",
PAGE_NAME_AND_URL_TAB_SELECTOR_FIELD = "PAGE_NAME_AND_URL_TAB_SELECTOR_FIELD",
}

View File

@ -24,12 +24,7 @@ import {
getModalDropdownList,
getNextModalName,
} from "selectors/widgetSelectors";
import Fields, {
ACTION_ANONYMOUS_FUNC_REGEX,
ACTION_TRIGGER_REGEX,
ActionType,
FieldType,
} from "./Fields";
import Fields from "./Fields";
import { getDataTree } from "selectors/dataTreeSelectors";
import { DataTree, ENTITY_TYPE } from "entities/DataTree/dataTreeFactory";
import { getEntityNameAndPropertyPath } from "workers/evaluationUtils";
@ -62,69 +57,74 @@ import { selectFeatureFlags } from "selectors/usersSelectors";
import FeatureFlags from "entities/FeatureFlags";
import { connect } from "react-redux";
import { isValidURL } from "utils/URLUtils";
import { ACTION_ANONYMOUS_FUNC_REGEX, ACTION_TRIGGER_REGEX } from "./regex";
import {
NAVIGATE_TO_TAB_OPTIONS,
AppsmithFunction,
FieldType,
} from "./constants";
import { SwitchType, ActionCreatorProps, GenericFunction } from "./types";
/* eslint-disable @typescript-eslint/ban-types */
/* TODO: Function and object types need to be updated to enable the lint rule */
const baseOptions: { label: string; value: string }[] = [
{
label: createMessage(NO_ACTION),
value: ActionType.none,
value: AppsmithFunction.none,
},
{
label: createMessage(EXECUTE_A_QUERY),
value: ActionType.integration,
value: AppsmithFunction.integration,
},
{
label: createMessage(NAVIGATE_TO),
value: ActionType.navigateTo,
value: AppsmithFunction.navigateTo,
},
{
label: createMessage(SHOW_MESSAGE),
value: ActionType.showAlert,
value: AppsmithFunction.showAlert,
},
{
label: createMessage(OPEN_MODAL),
value: ActionType.showModal,
value: AppsmithFunction.showModal,
},
{
label: createMessage(CLOSE_MODAL),
value: ActionType.closeModal,
value: AppsmithFunction.closeModal,
},
{
label: createMessage(STORE_VALUE),
value: ActionType.storeValue,
value: AppsmithFunction.storeValue,
},
{
label: createMessage(DOWNLOAD),
value: ActionType.download,
value: AppsmithFunction.download,
},
{
label: createMessage(COPY_TO_CLIPBOARD),
value: ActionType.copyToClipboard,
value: AppsmithFunction.copyToClipboard,
},
{
label: createMessage(RESET_WIDGET),
value: ActionType.resetWidget,
value: AppsmithFunction.resetWidget,
},
{
label: createMessage(SET_INTERVAL),
value: ActionType.setInterval,
value: AppsmithFunction.setInterval,
},
{
label: createMessage(CLEAR_INTERVAL),
value: ActionType.clearInterval,
value: AppsmithFunction.clearInterval,
},
{
label: createMessage(GET_GEO_LOCATION),
value: ActionType.getGeolocation,
value: AppsmithFunction.getGeolocation,
},
{
label: createMessage(WATCH_GEO_LOCATION),
value: ActionType.watchGeolocation,
value: AppsmithFunction.watchGeolocation,
},
{
label: createMessage(STOP_WATCH_GEO_LOCATION),
value: ActionType.stopWatchGeolocation,
value: AppsmithFunction.stopWatchGeolocation,
},
];
@ -132,28 +132,22 @@ const getBaseOptions = (featureFlags: FeatureFlags) => {
const { JS_EDITOR: isJSEditorEnabled } = featureFlags;
if (isJSEditorEnabled) {
const jsOption = baseOptions.find(
(option: any) => option.value === ActionType.jsFunction,
(option: any) => option.value === AppsmithFunction.jsFunction,
);
if (!jsOption) {
baseOptions.splice(2, 0, {
label: createMessage(EXECUTE_JS_FUNCTION),
value: ActionType.jsFunction,
value: AppsmithFunction.jsFunction,
});
}
}
return baseOptions;
};
type Switch = {
id: string;
text: string;
action: () => void;
};
function getFieldFromValue(
value: string | undefined,
activeTabNavigateTo: Switch,
getParentValue?: Function,
activeTabNavigateTo: SwitchType,
getParentValue?: (changeValue: string) => string,
dataTree?: DataTree,
): any[] {
const fields: any[] = [];
@ -408,7 +402,7 @@ function useModalDropdownList() {
id: "create",
icon: "plus",
className: "t--create-modal-btn",
onSelect: (option: TreeDropdownOption, setter?: Function) => {
onSelect: (option: TreeDropdownOption, setter?: GenericFunction) => {
const modalName = nextModalName;
if (setter) {
setter({
@ -459,11 +453,11 @@ function getIntegrationOptionsWithChildren(
action.config.pluginType === PluginType.REMOTE,
);
const option = options.find(
(option) => option.value === ActionType.integration,
(option) => option.value === AppsmithFunction.integration,
);
const jsOption = options.find(
(option) => option.value === ActionType.jsFunction,
(option) => option.value === AppsmithFunction.jsFunction,
);
if (option) {
@ -586,18 +580,6 @@ function useIntegrationsOptionTree() {
);
}
type ActionCreatorProps = {
value: string;
onValueChange: (newValue: string, isUpdatedViaKeyboard: boolean) => void;
additionalAutoComplete?: Record<string, Record<string, unknown>>;
pageDropdownOptions: TreeDropdownOption[];
};
const NAVIGATE_TO_TAB_OPTIONS = {
PAGE_NAME: "page-name",
URL: "url",
};
const isValueValidURL = (value: string) => {
if (value) {
const indices = [];
@ -613,7 +595,7 @@ const isValueValidURL = (value: string) => {
const ActionCreator = React.forwardRef(
(props: ActionCreatorProps, ref: any) => {
const NAVIGATE_TO_TAB_SWITCHER: Array<Switch> = [
const NAVIGATE_TO_TAB_SWITCHER: Array<SwitchType> = [
{
id: "page-name",
text: "Page Name",

View File

@ -0,0 +1,8 @@
export const FUNC_ARGS_REGEX = /((["][^"]*["])|([\[][\s\S]*[\]])|([\{][\s\S]*[\}])|(['][^']*['])|([\(][\s\S]*[\)][ ]*=>[ ]*[{][\s\S]*[}])|([^'",][^,"+]*[^'",]*))*/gi;
//Old Regex:: /\(\) => ([\s\S]*?)(\([\s\S]*?\))/g;
export const ACTION_TRIGGER_REGEX = /^{{([\s\S]*?)\(([\s\S]*?)\)}}$/g;
export const ACTION_ANONYMOUS_FUNC_REGEX = /\(\) => (({[\s\S]*?})|([\s\S]*?)(\([\s\S]*?\)))/g;
export const IS_URL_OR_MODAL = /^'.*'$/;

View File

@ -0,0 +1,58 @@
import { SwitcherProps, TreeDropdownOption } from "design-system";
import React from "react";
import { FieldType, ViewTypes, AppsmithFunction } from "./constants";
export type GenericFunction = (...args: any[]) => any;
export type SwitchType = {
id: string;
text: string;
action: () => void;
};
export type ActionType = typeof AppsmithFunction[keyof typeof AppsmithFunction];
export type ViewType = typeof ViewTypes[keyof typeof ViewTypes];
export type ViewProps = {
label: string;
get: GenericFunction;
set: GenericFunction;
value: string;
};
export type SelectorViewProps = ViewProps & {
options: TreeDropdownOption[];
defaultText: string;
getDefaults?: (value?: any) => any;
displayValue?: string;
selectedLabelModifier?: (
option: TreeDropdownOption,
displayValue?: string,
) => React.ReactNode;
index?: number;
};
export type KeyValueViewProps = ViewProps;
export type TextViewProps = ViewProps & {
index?: number;
additionalAutoComplete?: Record<string, Record<string, unknown>>;
};
export type TabViewProps = Omit<ViewProps, "get" | "set"> & SwitcherProps;
export type FieldConfig = {
getter: GenericFunction;
setter: GenericFunction;
view: ViewType;
};
export type FieldConfigs = Partial<Record<FieldType, FieldConfig>>;
export type ActionCreatorProps = {
value: string;
onValueChange: (newValue: string, isUpdatedViaKeyboard: boolean) => void;
additionalAutoComplete?: Record<string, Record<string, unknown>>;
pageDropdownOptions: TreeDropdownOption[];
};

View File

@ -0,0 +1,334 @@
jest.mock("sagas/ActionExecution/NavigateActionSaga", () => ({
__esModule: true,
default: "",
NavigationTargetType: { SAME_WINDOW: "" },
}));
import {
argsStringToArray,
enumTypeSetter,
enumTypeGetter,
JSToString,
modalGetter,
modalSetter,
stringToJS,
textGetter,
textSetter,
} from "./utils";
describe("Test argStringToArray", () => {
const cases = [
{ index: 0, input: "", expected: [""] },
{ index: 1, input: "'a'", expected: ["'a'"] },
{ index: 2, input: "a", expected: ["a"] },
{ index: 3, input: "'a,b,c'", expected: ["'a,b,c'"] },
{ index: 4, input: "a,b,c", expected: ["a", "b", "c"] },
{ index: 5, input: "a, b, c", expected: ["a", " b", " c"] },
{ index: 6, input: "a , b , c", expected: ["a ", " b ", " c"] },
{ index: 7, input: "[a,b,c]", expected: ["[a,b,c]"] },
{ index: 8, input: "[a, b, c]", expected: ["[a, b, c]"] },
{
index: 9,
input: "[\n\ta,\n\tb,\n\tc\n]",
expected: ["[\n\ta,\n\tb,\n\tc\n]"],
},
{ index: 10, input: "{a:1,b:2,c:3}", expected: ["{a:1,b:2,c:3}"] },
{
index: 11,
input: '{"a":1,"b":2,"c":3}',
expected: ['{"a":1,"b":2,"c":3}'],
},
{
index: 12,
input: "{\n\ta:1,\n\tb:2,\n\tc:3}",
expected: ["{\n\ta:1,\n\tb:2,\n\tc:3}"],
},
{
index: 13,
input: "()=>{}",
expected: ["()=>{}"],
},
{
index: 14,
input: "(a, b)=>{return a+b}",
expected: ["(a, b)=>{return a+b}"],
},
{
index: 15,
input: "(a, b)=>{\n\treturn a+b;\n\t}",
expected: ["(a, b)=>{\n\treturn a+b;\n\t}"],
},
{
index: 16,
input: "(\n\ta,\n\tb\n)=>{\n\treturn a+b;\n\t}",
expected: ["(\n\ta,\n\tb\n)=>{\n\treturn a+b;\n\t}"],
},
{
index: 17,
input: `() => {return 5}`,
expected: ["() => {return 5}"],
},
{
index: 19,
input: `(a) => {return a + 1}`,
expected: ["(a) => {return a + 1}"],
},
{
index: 19,
input: `(a, b) => {return a + b}`,
expected: ["(a, b) => {return a + b}"],
},
];
test.each(cases.map((x) => [x.index, x.input, x.expected]))(
"test case %d",
(_, input, expected) => {
const result = argsStringToArray(input as string);
expect(result).toStrictEqual(expected);
},
);
});
describe("Test stringToJS", () => {
const cases = [
{ index: 1, input: "{{'a'}}", expected: "'a'" },
{ index: 2, input: "{{a}}", expected: "a" },
{ index: 3, input: "{{'a,b,c'}}", expected: "'a,b,c'" },
{ index: 4, input: "{{a,b,c}}", expected: "a,b,c" },
{ index: 5, input: "{{a, b, c}}", expected: "a, b, c" },
{ index: 6, input: "{{a , b , c}}", expected: "a , b , c" },
{ index: 7, input: "{{[a,b,c]}}", expected: "[a,b,c]" },
{ index: 8, input: "{{[a, b, c]}}", expected: "[a, b, c]" },
{
index: 9,
input: "{{[\n\ta,\n\tb,\n\tc\n]}}",
expected: "[\n\ta,\n\tb,\n\tc\n]",
},
{ index: 10, input: "{{{a:1,b:2,c:3}}}", expected: "{a:1,b:2,c:3}" },
{
index: 11,
input: '{{{"a":1,"b":2,"c":3}}}',
expected: '{"a":1,"b":2,"c":3}',
},
{
index: 12,
input: "{{{\n\ta:1,\n\tb:2,\n\tc:3}}}",
expected: "{\n\ta:1,\n\tb:2,\n\tc:3}",
},
{
index: 13,
input: "{{()=>{}}}",
expected: "()=>{}",
},
{
index: 14,
input: "{{(a, b)=>{return a+b}}}",
expected: "(a, b)=>{return a+b}",
},
{
index: 15,
input: "{{(a, b)=>{\n\treturn a+b;\n\t}}}",
expected: "(a, b)=>{\n\treturn a+b;\n\t}",
},
{
index: 16,
input: "{{(\n\ta,\n\tb\n)=>{\n\treturn a+b;\n\t}}}",
expected: "(\n\ta,\n\tb\n)=>{\n\treturn a+b;\n\t}",
},
{
index: 17,
input: "{{() => {return 5}}}",
expected: "() => {return 5}",
},
{
index: 18,
input: "{{(a) => {return a + 1}}}",
expected: "(a) => {return a + 1}",
},
{
index: 19,
input: "{{(a, b) => {return a + b}}}",
expected: "(a, b) => {return a + b}",
},
];
test.each(cases.map((x) => [x.index, x.input, x.expected]))(
"test case %d",
(_, input, expected) => {
const result = stringToJS(input as string);
expect(result).toStrictEqual(expected);
},
);
});
describe("Test JSToString", () => {
const cases = [
{ index: 1, input: "'a'", expected: "a" },
{ index: 2, input: "a", expected: "{{a}}" },
{ index: 3, input: "'a,b,c'", expected: "a,b,c" },
{ index: 4, input: "a,b,c", expected: "{{a,b,c}}" },
{ index: 5, input: "a, b, c", expected: "{{a, b, c}}" },
{ index: 6, input: "a , b , c", expected: "{{a , b , c}}" },
{ index: 7, input: "[a,b,c]", expected: "{{[a,b,c]}}" },
{ index: 8, input: "[a, b, c]", expected: "{{[a, b, c]}}" },
{
index: 9,
input: "[\n\ta,\n\tb,\n\tc\n]",
expected: "{{[\n\ta,\n\tb,\n\tc\n]}}",
},
{ index: 10, input: "{a:1,b:2,c:3}", expected: "{{{a:1,b:2,c:3}}}" },
{
index: 11,
input: '{"a":1,"b":2,"c":3}',
expected: '{{{"a":1,"b":2,"c":3}}}',
},
{
index: 12,
input: "{\n\ta:1,\n\tb:2,\n\tc:3}",
expected: "{{{\n\ta:1,\n\tb:2,\n\tc:3}}}",
},
{
index: 13,
input: "()=>{}",
expected: "{{()=>{}}}",
},
{
index: 14,
input: "(a, b)=>{return a+b}",
expected: "{{(a, b)=>{return a+b}}}",
},
{
index: 15,
input: "(a, b)=>{\n\treturn a+b;\n\t}",
expected: "{{(a, b)=>{\n\treturn a+b;\n\t}}}",
},
{
index: 16,
input: "(\n\ta,\n\tb\n)=>{\n\treturn a+b;\n\t}",
expected: "{{(\n\ta,\n\tb\n)=>{\n\treturn a+b;\n\t}}}",
},
{
index: 17,
input: "() => {return 5}",
expected: "{{() => {return 5}}}",
},
];
test.each(cases.map((x) => [x.index, x.input, x.expected]))(
"test case %d",
(_, input, expected) => {
const result = JSToString(input as string);
expect(result).toStrictEqual(expected);
},
);
});
describe("Test modalSetter", () => {
const result = modalSetter("Modal1", "{{closeModal()}}");
expect(result).toStrictEqual("{{closeModal('Modal1')}}");
});
describe("Test modalGetter", () => {
const result = modalGetter("{{showModal('Modal1')}}");
expect(result).toStrictEqual("Modal1");
});
describe("Test textSetter", () => {
const result = textSetter(
"google.com",
"{{navigateTo('', {},NEW_WINDOW)}}",
0,
);
expect(result).toStrictEqual("{{navigateTo('google.com', {},NEW_WINDOW)}}");
});
describe("Test textGetter", () => {
const cases = [
{
index: 0,
input: "{{navigateTo('google.com', {}, NEW_WINDOW)}}",
expected: "google.com",
},
{
index: 1,
input: "{{navigateTo('google.com', {}, NEW_WINDOW)}}",
expected: "{{{}}}",
},
];
test.each(cases.map((x) => [x.index, x.input, x.expected]))(
"test case %d",
(index, input, expected) => {
const result = textGetter(input as string, index as number);
expect(result).toStrictEqual(expected);
},
);
});
describe("Test enumTypeSetter", () => {
const cases = [
{
index: 0,
value: "info",
input: "{{showAlert('hi')}}",
expected: "{{showAlert('hi',info)}}",
argNum: 1,
},
{
index: 1,
value: "info",
input: "{{showAlert('hi','error')}}",
expected: "{{showAlert('hi',info)}}",
argNum: 1,
},
{
index: 2,
value: "info",
input: "{{showAlert(,'')}}",
expected: "{{showAlert(,info)}}",
argNum: 1,
},
];
test.each(
cases.map((x) => [x.index, x.input, x.expected, x.value, x.argNum]),
)("test case %d", (index, input, expected, value, argNum) => {
const result = enumTypeSetter(
value as string,
input as string,
argNum as number,
);
expect(result).toStrictEqual(expected);
});
});
describe("Test enumTypeGetter", () => {
const cases = [
{
index: 0,
value: "success",
input: "{{showAlert('hi','info')}}",
expected: "{{showAlert('hi','info')}}",
argNum: 1,
},
{
index: 1,
value: "info",
input: "{{showAlert(,'error')}}",
expected: "{{showAlert(,'error')}}",
argNum: 1,
},
{
index: 2,
value: "info",
input: "{{showAlert()}}",
expected: "{{showAlert()}}",
argNum: 1,
},
];
test.each(
cases.map((x) => [x.index, x.input, x.expected, x.value, x.argNum]),
)("test case %d", (index, input, expected, value, argNum) => {
const result = enumTypeGetter(
value as string,
argNum as number,
input as string,
);
expect(result).toStrictEqual(expected);
});
});

View File

@ -0,0 +1,145 @@
import {
ACTION_TRIGGER_REGEX,
FUNC_ARGS_REGEX,
IS_URL_OR_MODAL,
} from "./regex";
import {
getDynamicBindings,
isDynamicValue,
} from "../../../utils/DynamicBindingUtils";
export const stringToJS = (string: string): string => {
const { jsSnippets, stringSegments } = getDynamicBindings(string);
return stringSegments
.map((segment, index) => {
if (jsSnippets[index] && jsSnippets[index].length > 0) {
return jsSnippets[index];
} else {
return `'${segment}'`;
}
})
.join(" + ");
};
export const JSToString = (js: string): string => {
const segments = js.split(" + ");
return segments
.map((segment) => {
if (segment.charAt(0) === "'") {
return segment.substring(1, segment.length - 1);
} else return "{{" + segment + "}}";
})
.join("");
};
export const argsStringToArray = (funcArgs: string): string[] => {
const argsplitMatches = [...funcArgs.matchAll(FUNC_ARGS_REGEX)];
const arr: string[] = [];
let isPrevUndefined = true;
for (const match of argsplitMatches) {
const matchVal = match[0];
if (!matchVal || matchVal === "") {
if (isPrevUndefined) {
arr.push(matchVal);
}
isPrevUndefined = true;
} else {
isPrevUndefined = false;
arr.push(matchVal);
}
}
return arr;
};
export const modalSetter = (changeValue: any, currentValue: string) => {
const matches = [...currentValue.matchAll(ACTION_TRIGGER_REGEX)];
let args: string[] = [];
if (matches.length) {
args = matches[0][2].split(",");
if (isDynamicValue(changeValue)) {
args[0] = `${changeValue.substring(2, changeValue.length - 2)}`;
} else {
args[0] = `'${changeValue}'`;
}
}
return currentValue.replace(
ACTION_TRIGGER_REGEX,
`{{$1(${args.join(",")})}}`,
);
};
export const modalGetter = (value: string) => {
const matches = [...value.matchAll(ACTION_TRIGGER_REGEX)];
let name = "none";
if (!matches.length) {
return name;
} else {
const modalName = matches[0][2].split(",")[0];
if (IS_URL_OR_MODAL.test(modalName) || modalName === "") {
name = modalName.substring(1, modalName.length - 1);
} else {
name = `{{${modalName}}}`;
}
return name;
}
};
export const textSetter = (
changeValue: any,
currentValue: string,
argNum: number,
): string => {
const matches = [...currentValue.matchAll(ACTION_TRIGGER_REGEX)];
let args: string[] = [];
if (matches.length) {
args = argsStringToArray(matches[0][2]);
args[argNum] = stringToJS(changeValue);
}
return currentValue.replace(
ACTION_TRIGGER_REGEX,
`{{$1(${args.join(",")})}}`,
);
};
export const textGetter = (value: string, argNum: number) => {
const matches = [...value.matchAll(ACTION_TRIGGER_REGEX)];
if (!matches.length) {
return "";
} else {
const args = argsStringToArray(matches[0][2]);
const arg = args[argNum];
return arg ? JSToString(arg.trim()) : arg;
}
};
export const enumTypeSetter = (
changeValue: any,
currentValue: string,
argNum: number,
): string => {
const matches = [...currentValue.matchAll(ACTION_TRIGGER_REGEX)];
let args: string[] = [];
if (matches.length) {
args = argsStringToArray(matches[0][2]);
args[argNum] = changeValue as string;
}
return currentValue.replace(
ACTION_TRIGGER_REGEX,
`{{$1(${args.join(",")})}}`,
);
};
export const enumTypeGetter = (
value: string,
argNum: number,
defaultValue = "",
): string => {
const matches = [...value.matchAll(ACTION_TRIGGER_REGEX)];
if (!matches.length) {
return defaultValue;
} else {
const args = argsStringToArray(matches[0][2]);
const arg = args[argNum];
return arg ? arg.trim() : defaultValue;
}
};

View File

@ -15,7 +15,7 @@ import { isString } from "utils/helpers";
import {
JSToString,
stringToJS,
} from "components/editorComponents/ActionCreator/Fields";
} from "components/editorComponents/ActionCreator/utils";
import CodeEditor from "components/editorComponents/LazyCodeEditorWrapper";
const PromptMessage = styled.span`

View File

@ -17,7 +17,7 @@ import { isString } from "utils/helpers";
import {
JSToString,
stringToJS,
} from "components/editorComponents/ActionCreator/Fields";
} from "components/editorComponents/ActionCreator/utils";
import { AdditionalDynamicDataTree } from "utils/autocomplete/customTreeTypeDefCreator";
const PromptMessage = styled.span`

View File

@ -17,7 +17,7 @@ import { isString } from "utils/helpers";
import {
JSToString,
stringToJS,
} from "components/editorComponents/ActionCreator/Fields";
} from "components/editorComponents/ActionCreator/utils";
import { AdditionalDynamicDataTree } from "utils/autocomplete/customTreeTypeDefCreator";
import {
ORIGINAL_INDEX_KEY,